EditorManager.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. no-return-assign,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS206: Consider reworking classes to avoid initClass
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. import Document from './Document'
  17. import './components/spellMenu'
  18. import './directives/aceEditor'
  19. import './directives/toggleSwitch'
  20. import './controllers/SavingNotificationController'
  21. let EditorManager
  22. export default (EditorManager = (function() {
  23. EditorManager = class EditorManager {
  24. static initClass() {
  25. this.prototype._syncTimeout = null
  26. }
  27. constructor(ide, $scope, localStorage) {
  28. this.ide = ide
  29. this.$scope = $scope
  30. this.localStorage = localStorage
  31. this.$scope.editor = {
  32. sharejs_doc: null,
  33. open_doc_id: null,
  34. open_doc_name: null,
  35. opening: true,
  36. trackChanges: false,
  37. wantTrackChanges: false,
  38. showRichText: this.showRichText()
  39. }
  40. this.$scope.$on('entity:selected', (event, entity) => {
  41. if (this.$scope.ui.view !== 'history' && entity.type === 'doc') {
  42. return this.openDoc(entity)
  43. }
  44. })
  45. this.$scope.$on('entity:deleted', (event, entity) => {
  46. if (this.$scope.editor.open_doc_id === entity.id) {
  47. if (!this.$scope.project.rootDoc_id) {
  48. this.$scope.ui.view = null
  49. return
  50. }
  51. const doc = this.ide.fileTreeManager.findEntityById(
  52. this.$scope.project.rootDoc_id
  53. )
  54. if (doc == null) {
  55. this.$scope.ui.view = null
  56. return
  57. }
  58. return this.openDoc(doc)
  59. }
  60. })
  61. let initialized = false
  62. this.$scope.$on('file-tree:initialized', () => {
  63. if (!initialized) {
  64. initialized = true
  65. return this.autoOpenDoc()
  66. }
  67. })
  68. this.$scope.$on('flush-changes', () => {
  69. return Document.flushAll()
  70. })
  71. window.addEventListener('blur', () => {
  72. // The browser may put the tab into sleep as it looses focus.
  73. // Flushing the documents should help with keeping the documents in
  74. // sync: we can use any new version of the doc that the server may
  75. // present us. There should be no need to insert local changes into
  76. // the doc history as the user comes back.
  77. sl_console.log('[EditorManager] forcing flush onblur')
  78. Document.flushAll()
  79. })
  80. this.$scope.$watch('editor.wantTrackChanges', value => {
  81. if (value == null) {
  82. return
  83. }
  84. return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
  85. })
  86. }
  87. showRichText() {
  88. return (
  89. this.localStorage(`editor.mode.${this.$scope.project_id}`) ===
  90. 'rich-text'
  91. )
  92. }
  93. autoOpenDoc() {
  94. const open_doc_id =
  95. this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
  96. this.$scope.project.rootDoc_id
  97. if (open_doc_id == null) {
  98. return
  99. }
  100. const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
  101. if (doc == null) {
  102. return
  103. }
  104. return this.openDoc(doc)
  105. }
  106. openDocId(doc_id, options) {
  107. if (options == null) {
  108. options = {}
  109. }
  110. const doc = this.ide.fileTreeManager.findEntityById(doc_id)
  111. if (doc == null) {
  112. return
  113. }
  114. return this.openDoc(doc, options)
  115. }
  116. openDoc(doc, options) {
  117. if (options == null) {
  118. options = {}
  119. }
  120. sl_console.log(`[openDoc] Opening ${doc.id}`)
  121. this.$scope.ui.view = 'editor'
  122. const done = () => {
  123. if (options.gotoLine != null) {
  124. // allow Ace to display document before moving, delay until next tick
  125. // added delay to make this happen later that gotoStoredPosition in
  126. // CursorPositionManager
  127. return setTimeout(() => {
  128. return this.$scope.$broadcast(
  129. 'editor:gotoLine',
  130. options.gotoLine,
  131. options.gotoColumn
  132. )
  133. }, 0)
  134. } else if (options.gotoOffset != null) {
  135. return setTimeout(() => {
  136. return this.$scope.$broadcast(
  137. 'editor:gotoOffset',
  138. options.gotoOffset
  139. )
  140. }, 0)
  141. }
  142. }
  143. // If we already have the document open we can return at this point.
  144. // Note: only use forceReopen:true to override this when the document is
  145. // is out of sync and needs to be reloaded from the server.
  146. if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
  147. // automatically update the file tree whenever the file is opened
  148. this.ide.fileTreeManager.selectEntity(doc)
  149. this.$scope.$apply(() => {
  150. return done()
  151. })
  152. return
  153. }
  154. // We're now either opening a new document or reloading a broken one.
  155. this.$scope.editor.open_doc_id = doc.id
  156. this.$scope.editor.open_doc_name = doc.name
  157. this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
  158. this.ide.fileTreeManager.selectEntity(doc)
  159. this.$scope.editor.opening = true
  160. return this._openNewDocument(doc, (error, sharejs_doc) => {
  161. if (error != null) {
  162. this.ide.showGenericMessageModal(
  163. 'Error opening document',
  164. 'Sorry, something went wrong opening this document. Please try again.'
  165. )
  166. return
  167. }
  168. this._syncTrackChangesState(sharejs_doc)
  169. this.$scope.$broadcast('doc:opened')
  170. return this.$scope.$apply(() => {
  171. this.$scope.editor.opening = false
  172. this.$scope.editor.sharejs_doc = sharejs_doc
  173. return done()
  174. })
  175. })
  176. }
  177. _openNewDocument(doc, callback) {
  178. if (callback == null) {
  179. callback = function(error, sharejs_doc) {}
  180. }
  181. sl_console.log('[_openNewDocument] Opening...')
  182. const current_sharejs_doc = this.$scope.editor.sharejs_doc
  183. const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
  184. // Leave the current document only when we are opening a different new
  185. // one, to avoid race conditions between leaving and joining the same
  186. // document.
  187. if (
  188. current_sharejs_doc != null &&
  189. current_sharejs_doc !== new_sharejs_doc
  190. ) {
  191. sl_console.log('[_openNewDocument] Leaving existing open doc...')
  192. current_sharejs_doc.leaveAndCleanUp()
  193. this._unbindFromDocumentEvents(current_sharejs_doc)
  194. }
  195. return new_sharejs_doc.join(error => {
  196. if (error != null) {
  197. return callback(error)
  198. }
  199. this._bindToDocumentEvents(doc, new_sharejs_doc)
  200. return callback(null, new_sharejs_doc)
  201. })
  202. }
  203. _bindToDocumentEvents(doc, sharejs_doc) {
  204. sharejs_doc.on('error', (error, meta) => {
  205. let message
  206. if ((error != null ? error.message : undefined) != null) {
  207. ;({ message } = error)
  208. } else if (typeof error === 'string') {
  209. message = error
  210. } else {
  211. message = ''
  212. }
  213. if (/maxDocLength/.test(message)) {
  214. this.ide.showGenericMessageModal(
  215. 'Document Too Long',
  216. 'Sorry, this file is too long to be edited manually. Please upload it directly.'
  217. )
  218. } else if (/too many comments or tracked changes/.test(message)) {
  219. this.ide.showGenericMessageModal(
  220. 'Too many comments or tracked changes',
  221. 'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
  222. )
  223. } else {
  224. this.ide.socket.disconnect()
  225. this.ide.reportError(error, meta)
  226. this.ide.showOutOfSyncModal(
  227. 'Out of sync',
  228. "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>",
  229. sharejs_doc.doc._doc.snapshot
  230. )
  231. }
  232. const removeHandler = this.$scope.$on('project:joined', () => {
  233. this.openDoc(doc, { forceReopen: true })
  234. removeHandler()
  235. })
  236. })
  237. return sharejs_doc.on('externalUpdate', update => {
  238. if (this._ignoreExternalUpdates) {
  239. return
  240. }
  241. if (
  242. _.property(['meta', 'type'])(update) === 'external' &&
  243. _.property(['meta', 'source'])(update) === 'git-bridge'
  244. ) {
  245. return
  246. }
  247. return this.ide.showGenericMessageModal(
  248. 'Document Updated Externally',
  249. '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.'
  250. )
  251. })
  252. }
  253. _unbindFromDocumentEvents(document) {
  254. return document.off()
  255. }
  256. getCurrentDocValue() {
  257. return this.$scope.editor.sharejs_doc != null
  258. ? this.$scope.editor.sharejs_doc.getSnapshot()
  259. : undefined
  260. }
  261. getCurrentDocId() {
  262. return this.$scope.editor.open_doc_id
  263. }
  264. startIgnoringExternalUpdates() {
  265. return (this._ignoreExternalUpdates = true)
  266. }
  267. stopIgnoringExternalUpdates() {
  268. return (this._ignoreExternalUpdates = false)
  269. }
  270. _syncTrackChangesState(doc) {
  271. let tryToggle
  272. if (doc == null) {
  273. return
  274. }
  275. if (this._syncTimeout != null) {
  276. clearTimeout(this._syncTimeout)
  277. this._syncTimeout = null
  278. }
  279. const want = this.$scope.editor.wantTrackChanges
  280. const have = doc.getTrackingChanges()
  281. if (want === have) {
  282. this.$scope.editor.trackChanges = want
  283. return
  284. }
  285. return (tryToggle = () => {
  286. const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
  287. if (saved) {
  288. doc.setTrackingChanges(want)
  289. return this.$scope.$apply(() => {
  290. return (this.$scope.editor.trackChanges = want)
  291. })
  292. } else {
  293. return (this._syncTimeout = setTimeout(tryToggle, 100))
  294. }
  295. })()
  296. }
  297. }
  298. EditorManager.initClass()
  299. return EditorManager
  300. })())