EditorManager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. import _ from 'lodash'
  2. /* eslint-disable
  3. camelcase,
  4. node/handle-callback-err,
  5. max-len,
  6. no-return-assign,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS206: Consider reworking classes to avoid initClass
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. import Document from './Document'
  18. import './components/spellMenu'
  19. import './directives/aceEditor'
  20. import './directives/toggleSwitch'
  21. import './controllers/SavingNotificationController'
  22. let EditorManager
  23. export default EditorManager = (function () {
  24. EditorManager = class EditorManager {
  25. static initClass() {
  26. this.prototype._syncTimeout = null
  27. }
  28. constructor(ide, $scope, localStorage) {
  29. this.ide = ide
  30. this.editorOpenDocEpoch = 0 // track pending document loads
  31. this.$scope = $scope
  32. this.localStorage = localStorage
  33. this.$scope.editor = {
  34. sharejs_doc: null,
  35. open_doc_id: null,
  36. open_doc_name: null,
  37. opening: true,
  38. trackChanges: false,
  39. wantTrackChanges: false,
  40. showRichText: this.showRichText(),
  41. }
  42. this.$scope.$on('entity:selected', (event, entity) => {
  43. if (this.$scope.ui.view !== 'history' && entity.type === 'doc') {
  44. return this.openDoc(entity)
  45. }
  46. })
  47. this.$scope.$on('entity:no-selection', () => {
  48. this.$scope.$apply(() => {
  49. this.$scope.ui.view = null
  50. })
  51. })
  52. this.$scope.$on('entity:deleted', (event, entity) => {
  53. if (this.$scope.editor.open_doc_id === entity.id) {
  54. if (!this.$scope.project.rootDoc_id) {
  55. this.$scope.ui.view = null
  56. return
  57. }
  58. const doc = this.ide.fileTreeManager.findEntityById(
  59. this.$scope.project.rootDoc_id
  60. )
  61. if (doc == null) {
  62. this.$scope.ui.view = null
  63. return
  64. }
  65. return this.openDoc(doc)
  66. }
  67. })
  68. let initialized = false
  69. this.$scope.$on('file-tree:initialized', () => {
  70. if (!initialized) {
  71. initialized = true
  72. return this.autoOpenDoc()
  73. }
  74. })
  75. this.$scope.$on('flush-changes', () => {
  76. return Document.flushAll()
  77. })
  78. window.addEventListener('blur', () => {
  79. // The browser may put the tab into sleep as it looses focus.
  80. // Flushing the documents should help with keeping the documents in
  81. // sync: we can use any new version of the doc that the server may
  82. // present us. There should be no need to insert local changes into
  83. // the doc history as the user comes back.
  84. sl_console.log('[EditorManager] forcing flush onblur')
  85. Document.flushAll()
  86. })
  87. this.$scope.$watch('editor.wantTrackChanges', value => {
  88. if (value == null) {
  89. return
  90. }
  91. return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
  92. })
  93. }
  94. showRichText() {
  95. return (
  96. this.localStorage(`editor.mode.${this.$scope.project_id}`) ===
  97. 'rich-text'
  98. )
  99. }
  100. autoOpenDoc() {
  101. const open_doc_id =
  102. this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
  103. this.$scope.project.rootDoc_id
  104. if (open_doc_id == null) {
  105. return
  106. }
  107. const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
  108. if (doc == null) {
  109. return
  110. }
  111. return this.openDoc(doc)
  112. }
  113. openDocId(doc_id, options) {
  114. if (options == null) {
  115. options = {}
  116. }
  117. const doc = this.ide.fileTreeManager.findEntityById(doc_id)
  118. if (doc == null) {
  119. return
  120. }
  121. return this.openDoc(doc, options)
  122. }
  123. jumpToLine(options) {
  124. return this.$scope.$broadcast(
  125. 'editor:gotoLine',
  126. options.gotoLine,
  127. options.gotoColumn,
  128. options.syncToPdf
  129. )
  130. }
  131. openDoc(doc, options) {
  132. if (options == null) {
  133. options = {}
  134. }
  135. sl_console.log(`[openDoc] Opening ${doc.id}`)
  136. this.$scope.ui.view = 'editor'
  137. const done = isNewDoc => {
  138. this.$scope.$broadcast('doc:after-opened', { isNewDoc })
  139. if (options.gotoLine != null) {
  140. // allow Ace to display document before moving, delay until next tick
  141. // added delay to make this happen later that gotoStoredPosition in
  142. // CursorPositionManager
  143. return setTimeout(() => this.jumpToLine(options), 0)
  144. } else if (options.gotoOffset != null) {
  145. return setTimeout(() => {
  146. return this.$scope.$broadcast(
  147. 'editor:gotoOffset',
  148. options.gotoOffset
  149. )
  150. }, 0)
  151. }
  152. }
  153. // If we already have the document open we can return at this point.
  154. // Note: only use forceReopen:true to override this when the document is
  155. // is out of sync and needs to be reloaded from the server.
  156. if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
  157. // automatically update the file tree whenever the file is opened
  158. this.ide.fileTreeManager.selectEntity(doc)
  159. this.$scope.$apply(() => {
  160. return done(false)
  161. })
  162. return
  163. }
  164. // We're now either opening a new document or reloading a broken one.
  165. this.$scope.editor.open_doc_id = doc.id
  166. this.$scope.editor.open_doc_name = doc.name
  167. this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
  168. this.ide.fileTreeManager.selectEntity(doc)
  169. this.$scope.editor.opening = true
  170. return this._openNewDocument(doc, (error, sharejs_doc) => {
  171. if (error && error.message === 'another document was loaded') {
  172. sl_console.log(
  173. `[openDoc] another document was loaded while ${doc.id} was loading`
  174. )
  175. return
  176. }
  177. if (error != null) {
  178. this.ide.showGenericMessageModal(
  179. 'Error opening document',
  180. 'Sorry, something went wrong opening this document. Please try again.'
  181. )
  182. return
  183. }
  184. this._syncTrackChangesState(sharejs_doc)
  185. this.$scope.$broadcast('doc:opened')
  186. return this.$scope.$apply(() => {
  187. this.$scope.editor.opening = false
  188. this.$scope.editor.sharejs_doc = sharejs_doc
  189. return done(true)
  190. })
  191. })
  192. }
  193. _openNewDocument(doc, callback) {
  194. // Leave the current document
  195. // - when we are opening a different new one, to avoid race conditions
  196. // between leaving and joining the same document
  197. // - when the current one has pending ops that need flushing, to avoid
  198. // race conditions from cleanup
  199. const current_sharejs_doc = this.$scope.editor.sharejs_doc
  200. const currentDocId = current_sharejs_doc && current_sharejs_doc.doc_id
  201. const hasBufferedOps =
  202. current_sharejs_doc && current_sharejs_doc.hasBufferedOps()
  203. const changingDoc = current_sharejs_doc && currentDocId !== doc.id
  204. if (changingDoc || hasBufferedOps) {
  205. sl_console.log('[_openNewDocument] Leaving existing open doc...')
  206. // Do not trigger any UI changes from remote operations
  207. this._unbindFromDocumentEvents(current_sharejs_doc)
  208. // Keep listening for out-of-sync and similar errors.
  209. this._attachErrorHandlerToDocument(doc, current_sharejs_doc)
  210. // Teardown the Document -> ShareJsDoc -> sharejs doc
  211. // By the time this completes, the Document instance is no longer
  212. // registered in Document.openDocs and _doOpenNewDocument can start
  213. // from scratch -- read: no corrupted internal state.
  214. const editorOpenDocEpoch = ++this.editorOpenDocEpoch
  215. current_sharejs_doc.leaveAndCleanUp(error => {
  216. if (error) {
  217. sl_console.log(
  218. `[_openNewDocument] error leaving doc ${currentDocId}`,
  219. error
  220. )
  221. return callback(error)
  222. }
  223. if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
  224. sl_console.log(
  225. `[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
  226. )
  227. return callback(new Error('another document was loaded'))
  228. }
  229. this._doOpenNewDocument(doc, callback)
  230. })
  231. } else {
  232. this._doOpenNewDocument(doc, callback)
  233. }
  234. }
  235. _doOpenNewDocument(doc, callback) {
  236. if (callback == null) {
  237. callback = function (error, sharejs_doc) {}
  238. }
  239. sl_console.log('[_doOpenNewDocument] Opening...')
  240. const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
  241. const editorOpenDocEpoch = ++this.editorOpenDocEpoch
  242. return new_sharejs_doc.join(error => {
  243. if (error != null) {
  244. sl_console.log(
  245. `[_doOpenNewDocument] error joining doc ${doc.id}`,
  246. error
  247. )
  248. return callback(error)
  249. }
  250. if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
  251. sl_console.log(
  252. `[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
  253. )
  254. new_sharejs_doc.leaveAndCleanUp()
  255. return callback(new Error('another document was loaded'))
  256. }
  257. this._bindToDocumentEvents(doc, new_sharejs_doc)
  258. return callback(null, new_sharejs_doc)
  259. })
  260. }
  261. _attachErrorHandlerToDocument(doc, sharejs_doc) {
  262. sharejs_doc.on('error', (error, meta, editorContent) => {
  263. let message
  264. if ((error != null ? error.message : undefined) != null) {
  265. ;({ message } = error)
  266. } else if (typeof error === 'string') {
  267. message = error
  268. } else {
  269. message = ''
  270. }
  271. if (/maxDocLength/.test(message)) {
  272. this.ide.showGenericMessageModal(
  273. 'Document Too Long',
  274. 'Sorry, this file is too long to be edited manually. Please upload it directly.'
  275. )
  276. } else if (/too many comments or tracked changes/.test(message)) {
  277. this.ide.showGenericMessageModal(
  278. 'Too many comments or tracked changes',
  279. 'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
  280. )
  281. } else {
  282. // Do not allow this doc to open another error modal.
  283. sharejs_doc.off('error')
  284. // Preserve the sharejs contents before the teardown.
  285. editorContent =
  286. typeof editorContent === 'string'
  287. ? editorContent
  288. : sharejs_doc.doc._doc.snapshot
  289. // Tear down the ShareJsDoc.
  290. if (sharejs_doc.doc) sharejs_doc.doc.clearInflightAndPendingOps()
  291. // Do not re-join after re-connecting.
  292. sharejs_doc.leaveAndCleanUp()
  293. this.ide.connectionManager.disconnect({ permanent: true })
  294. this.ide.reportError(error, meta)
  295. // Tell the user about the error state.
  296. this.$scope.editor.error_state = true
  297. this.ide.showOutOfSyncModal(
  298. 'Out of sync',
  299. "Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a target='_blank' rel='noopener noreferrer' href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
  300. editorContent
  301. )
  302. // Do not forceReopen the document.
  303. return
  304. }
  305. const removeHandler = this.$scope.$on('project:joined', () => {
  306. this.openDoc(doc, { forceReopen: true })
  307. removeHandler()
  308. })
  309. })
  310. }
  311. _bindToDocumentEvents(doc, sharejs_doc) {
  312. this._attachErrorHandlerToDocument(doc, sharejs_doc)
  313. return sharejs_doc.on('externalUpdate', update => {
  314. if (this._ignoreExternalUpdates) {
  315. return
  316. }
  317. if (
  318. _.property(['meta', 'type'])(update) === 'external' &&
  319. _.property(['meta', 'source'])(update) === 'git-bridge'
  320. ) {
  321. return
  322. }
  323. return this.ide.showGenericMessageModal(
  324. 'Document Updated Externally',
  325. 'This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history.'
  326. )
  327. })
  328. }
  329. _unbindFromDocumentEvents(document) {
  330. return document.off()
  331. }
  332. getCurrentDocValue() {
  333. return this.$scope.editor.sharejs_doc != null
  334. ? this.$scope.editor.sharejs_doc.getSnapshot()
  335. : undefined
  336. }
  337. getCurrentDocId() {
  338. return this.$scope.editor.open_doc_id
  339. }
  340. startIgnoringExternalUpdates() {
  341. return (this._ignoreExternalUpdates = true)
  342. }
  343. stopIgnoringExternalUpdates() {
  344. return (this._ignoreExternalUpdates = false)
  345. }
  346. _syncTrackChangesState(doc) {
  347. let tryToggle
  348. if (doc == null) {
  349. return
  350. }
  351. if (this._syncTimeout != null) {
  352. clearTimeout(this._syncTimeout)
  353. this._syncTimeout = null
  354. }
  355. const want = this.$scope.editor.wantTrackChanges
  356. const have = doc.getTrackingChanges()
  357. if (want === have) {
  358. this.$scope.editor.trackChanges = want
  359. return
  360. }
  361. return (tryToggle = () => {
  362. const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
  363. if (saved) {
  364. doc.setTrackingChanges(want)
  365. return this.$scope.$apply(() => {
  366. return (this.$scope.editor.trackChanges = want)
  367. })
  368. } else {
  369. return (this._syncTimeout = setTimeout(tryToggle, 100))
  370. }
  371. })()
  372. }
  373. }
  374. EditorManager.initClass()
  375. return EditorManager
  376. })()