EditorManager.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import _ from 'lodash'
  2. /* eslint-disable
  3. camelcase,
  4. 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:deleted', (event, entity) => {
  48. if (this.$scope.editor.open_doc_id === entity.id) {
  49. if (!this.$scope.project.rootDoc_id) {
  50. this.$scope.ui.view = null
  51. return
  52. }
  53. const doc = this.ide.fileTreeManager.findEntityById(
  54. this.$scope.project.rootDoc_id
  55. )
  56. if (doc == null) {
  57. this.$scope.ui.view = null
  58. return
  59. }
  60. return this.openDoc(doc)
  61. }
  62. })
  63. let initialized = false
  64. this.$scope.$on('file-tree:initialized', () => {
  65. if (!initialized) {
  66. initialized = true
  67. return this.autoOpenDoc()
  68. }
  69. })
  70. this.$scope.$on('flush-changes', () => {
  71. return Document.flushAll()
  72. })
  73. window.addEventListener('blur', () => {
  74. // The browser may put the tab into sleep as it looses focus.
  75. // Flushing the documents should help with keeping the documents in
  76. // sync: we can use any new version of the doc that the server may
  77. // present us. There should be no need to insert local changes into
  78. // the doc history as the user comes back.
  79. sl_console.log('[EditorManager] forcing flush onblur')
  80. Document.flushAll()
  81. })
  82. this.$scope.$watch('editor.wantTrackChanges', value => {
  83. if (value == null) {
  84. return
  85. }
  86. return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
  87. })
  88. }
  89. showRichText() {
  90. return (
  91. this.localStorage(`editor.mode.${this.$scope.project_id}`) ===
  92. 'rich-text'
  93. )
  94. }
  95. autoOpenDoc() {
  96. const open_doc_id =
  97. this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
  98. this.$scope.project.rootDoc_id
  99. if (open_doc_id == null) {
  100. return
  101. }
  102. const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
  103. if (doc == null) {
  104. return
  105. }
  106. return this.openDoc(doc)
  107. }
  108. openDocId(doc_id, options) {
  109. if (options == null) {
  110. options = {}
  111. }
  112. const doc = this.ide.fileTreeManager.findEntityById(doc_id)
  113. if (doc == null) {
  114. return
  115. }
  116. return this.openDoc(doc, options)
  117. }
  118. jumpToLine(options) {
  119. return this.$scope.$broadcast(
  120. 'editor:gotoLine',
  121. options.gotoLine,
  122. options.gotoColumn,
  123. options.syncToPdf
  124. )
  125. }
  126. openDoc(doc, options) {
  127. if (options == null) {
  128. options = {}
  129. }
  130. sl_console.log(`[openDoc] Opening ${doc.id}`)
  131. this.$scope.ui.view = 'editor'
  132. const done = isNewDoc => {
  133. this.$scope.$broadcast('doc:after-opened', { isNewDoc })
  134. if (options.gotoLine != null) {
  135. // allow Ace to display document before moving, delay until next tick
  136. // added delay to make this happen later that gotoStoredPosition in
  137. // CursorPositionManager
  138. return setTimeout(() => this.jumpToLine(options), 0)
  139. } else if (options.gotoOffset != null) {
  140. return setTimeout(() => {
  141. return this.$scope.$broadcast(
  142. 'editor:gotoOffset',
  143. options.gotoOffset
  144. )
  145. }, 0)
  146. }
  147. }
  148. // If we already have the document open we can return at this point.
  149. // Note: only use forceReopen:true to override this when the document is
  150. // is out of sync and needs to be reloaded from the server.
  151. if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
  152. // automatically update the file tree whenever the file is opened
  153. this.ide.fileTreeManager.selectEntity(doc)
  154. this.$scope.$apply(() => {
  155. return done(false)
  156. })
  157. return
  158. }
  159. // We're now either opening a new document or reloading a broken one.
  160. this.$scope.editor.open_doc_id = doc.id
  161. this.$scope.editor.open_doc_name = doc.name
  162. this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
  163. this.ide.fileTreeManager.selectEntity(doc)
  164. this.$scope.editor.opening = true
  165. return this._openNewDocument(doc, (error, sharejs_doc) => {
  166. if (error && error.message === 'another document was loaded') {
  167. sl_console.log(
  168. `[openDoc] another document was loaded while ${doc.id} was loading`
  169. )
  170. return
  171. }
  172. if (error != null) {
  173. this.ide.showGenericMessageModal(
  174. 'Error opening document',
  175. 'Sorry, something went wrong opening this document. Please try again.'
  176. )
  177. return
  178. }
  179. this._syncTrackChangesState(sharejs_doc)
  180. this.$scope.$broadcast('doc:opened')
  181. return this.$scope.$apply(() => {
  182. this.$scope.editor.opening = false
  183. this.$scope.editor.sharejs_doc = sharejs_doc
  184. return done(true)
  185. })
  186. })
  187. }
  188. _openNewDocument(doc, callback) {
  189. if (callback == null) {
  190. callback = function(error, sharejs_doc) {}
  191. }
  192. sl_console.log('[_openNewDocument] Opening...')
  193. const current_sharejs_doc = this.$scope.editor.sharejs_doc
  194. const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
  195. // Leave the current document only when we are opening a different new
  196. // one, to avoid race conditions between leaving and joining the same
  197. // document.
  198. if (
  199. current_sharejs_doc != null &&
  200. current_sharejs_doc !== new_sharejs_doc
  201. ) {
  202. sl_console.log('[_openNewDocument] Leaving existing open doc...')
  203. current_sharejs_doc.leaveAndCleanUp()
  204. this._unbindFromDocumentEvents(current_sharejs_doc)
  205. }
  206. const editorOpenDocEpoch = ++this.editorOpenDocEpoch
  207. return new_sharejs_doc.join(error => {
  208. if (error != null) {
  209. return callback(error)
  210. }
  211. if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
  212. sl_console.log(
  213. `[openNewDocument] editorOpenDocEpoch mismatch ${
  214. this.editorOpenDocEpoch
  215. } vs ${editorOpenDocEpoch}`
  216. )
  217. return callback(new Error('another document was loaded'))
  218. }
  219. this._bindToDocumentEvents(doc, new_sharejs_doc)
  220. return callback(null, new_sharejs_doc)
  221. })
  222. }
  223. _bindToDocumentEvents(doc, sharejs_doc) {
  224. sharejs_doc.on('error', (error, meta) => {
  225. let message
  226. if ((error != null ? error.message : undefined) != null) {
  227. ;({ message } = error)
  228. } else if (typeof error === 'string') {
  229. message = error
  230. } else {
  231. message = ''
  232. }
  233. if (/maxDocLength/.test(message)) {
  234. this.ide.showGenericMessageModal(
  235. 'Document Too Long',
  236. 'Sorry, this file is too long to be edited manually. Please upload it directly.'
  237. )
  238. } else if (/too many comments or tracked changes/.test(message)) {
  239. this.ide.showGenericMessageModal(
  240. 'Too many comments or tracked changes',
  241. 'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
  242. )
  243. } else {
  244. this.ide.socket.disconnect()
  245. this.ide.reportError(error, meta)
  246. this.ide.showOutOfSyncModal(
  247. 'Out of sync',
  248. "Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
  249. sharejs_doc.doc._doc.snapshot
  250. )
  251. }
  252. const removeHandler = this.$scope.$on('project:joined', () => {
  253. this.openDoc(doc, { forceReopen: true })
  254. removeHandler()
  255. })
  256. })
  257. return sharejs_doc.on('externalUpdate', update => {
  258. if (this._ignoreExternalUpdates) {
  259. return
  260. }
  261. if (
  262. _.property(['meta', 'type'])(update) === 'external' &&
  263. _.property(['meta', 'source'])(update) === 'git-bridge'
  264. ) {
  265. return
  266. }
  267. return this.ide.showGenericMessageModal(
  268. 'Document Updated Externally',
  269. '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.'
  270. )
  271. })
  272. }
  273. _unbindFromDocumentEvents(document) {
  274. return document.off()
  275. }
  276. getCurrentDocValue() {
  277. return this.$scope.editor.sharejs_doc != null
  278. ? this.$scope.editor.sharejs_doc.getSnapshot()
  279. : undefined
  280. }
  281. getCurrentDocId() {
  282. return this.$scope.editor.open_doc_id
  283. }
  284. startIgnoringExternalUpdates() {
  285. return (this._ignoreExternalUpdates = true)
  286. }
  287. stopIgnoringExternalUpdates() {
  288. return (this._ignoreExternalUpdates = false)
  289. }
  290. _syncTrackChangesState(doc) {
  291. let tryToggle
  292. if (doc == null) {
  293. return
  294. }
  295. if (this._syncTimeout != null) {
  296. clearTimeout(this._syncTimeout)
  297. this._syncTimeout = null
  298. }
  299. const want = this.$scope.editor.wantTrackChanges
  300. const have = doc.getTrackingChanges()
  301. if (want === have) {
  302. this.$scope.editor.trackChanges = want
  303. return
  304. }
  305. return (tryToggle = () => {
  306. const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
  307. if (saved) {
  308. doc.setTrackingChanges(want)
  309. return this.$scope.$apply(() => {
  310. return (this.$scope.editor.trackChanges = want)
  311. })
  312. } else {
  313. return (this._syncTimeout = setTimeout(tryToggle, 100))
  314. }
  315. })()
  316. }
  317. }
  318. EditorManager.initClass()
  319. return EditorManager
  320. })())