| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- import _ from 'lodash'
- /* eslint-disable
- camelcase,
- n/handle-callback-err,
- max-len,
- no-return-assign,
- */
- // TODO: This file was created by bulk-decaffeinate.
- // Fix any style issues and re-enable lint.
- /*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
- import Document from './Document'
- import './directives/formattingButtons'
- import './directives/toggleSwitch'
- import './controllers/SavingNotificationController'
- import './controllers/CompileButton'
- import './controllers/SwitchToPDFButton'
- import '../metadata/services/metadata'
- import { debugConsole } from '@/utils/debugging'
- import customLocalStorage from '@/infrastructure/local-storage'
- let EditorManager
- export default EditorManager = (function () {
- EditorManager = class EditorManager {
- static initClass() {
- this.prototype._syncTimeout = null
- }
- constructor(ide, $scope, localStorage, eventTracking) {
- this.ide = ide
- this.editorOpenDocEpoch = 0 // track pending document loads
- this.$scope = $scope
- this.localStorage = localStorage
- this.$scope.editor = {
- sharejs_doc: null,
- open_doc_id: null,
- open_doc_name: null,
- opening: true,
- trackChanges: false,
- wantTrackChanges: false,
- docTooLongErrorShown: false,
- showVisual: this.showVisual(),
- showSymbolPalette: false,
- toggleSymbolPalette: () => {
- const newValue = !this.$scope.editor.showSymbolPalette
- this.$scope.editor.showSymbolPalette = newValue
- ide.$scope.$emit('south-pane-toggled', newValue)
- eventTracking.sendMB(
- newValue ? 'symbol-palette-show' : 'symbol-palette-hide'
- )
- },
- insertSymbol: symbol => {
- ide.$scope.$emit('editor:replace-selection', symbol.command)
- eventTracking.sendMB('symbol-palette-insert')
- },
- multiSelectedCount: 0,
- }
- window.addEventListener('editor:insert-symbol', event => {
- this.$scope.editor.insertSymbol(event.detail)
- })
- this.$scope.$on('entity:selected', (event, entity) => {
- if (this.$scope.ui.view !== 'history' && entity.type === 'doc') {
- return this.openDoc(entity)
- }
- })
- this.$scope.$on('entity:no-selection', () => {
- this.$scope.$apply(() => {
- this.$scope.ui.view = null
- })
- })
- this.$scope.$on('entity:deleted', (event, entity) => {
- if (this.$scope.editor.open_doc_id === entity.id) {
- if (!this.$scope.project.rootDoc_id) {
- this.$scope.ui.view = null
- return
- }
- const doc = this.ide.fileTreeManager.findEntityById(
- this.$scope.project.rootDoc_id
- )
- if (doc == null) {
- this.$scope.ui.view = null
- return
- }
- return this.openDoc(doc)
- }
- })
- let initialized = false
- this.$scope.$on('file-tree:initialized', () => {
- if (!initialized) {
- initialized = true
- return this.autoOpenDoc()
- }
- })
- this.$scope.$on('flush-changes', () => {
- return Document.flushAll()
- })
- // event dispatched by pdf preview
- window.addEventListener('flush-changes', () => {
- Document.flushAll()
- })
- window.addEventListener('blur', () => {
- // The browser may put the tab into sleep as it looses focus.
- // Flushing the documents should help with keeping the documents in
- // sync: we can use any new version of the doc that the server may
- // present us. There should be no need to insert local changes into
- // the doc history as the user comes back.
- debugConsole.log('[EditorManager] forcing flush onblur')
- Document.flushAll()
- })
- this.$scope.$watch('editor.wantTrackChanges', value => {
- if (value == null) {
- return
- }
- return this._syncTrackChangesState(this.$scope.editor.sharejs_doc)
- })
- window.addEventListener('editor:open-doc', event => {
- const { doc, ...options } = event.detail
- this.openDoc(doc, options)
- })
- window.addEventListener('editor:open-file', event => {
- const { name, ...options } = event.detail
- for (const extension of ['', '.tex']) {
- const path = `${name}${extension}`
- const doc = ide.fileTreeManager.findEntityByPath(path)
- if (doc) {
- this.openDoc(doc, options)
- break
- }
- }
- })
- }
- getEditorType() {
- if (!this.$scope.editor.sharejs_doc) {
- return null
- }
- let editorType = this.$scope.editor.sharejs_doc.editorType()
- if (editorType === 'cm6' && this.$scope.editor.showVisual) {
- editorType = 'cm6-rich-text'
- }
- return editorType
- }
- showVisual() {
- const editorModeKey = `editor.mode.${this.$scope.project_id}`
- const editorModeVal = this.localStorage(editorModeKey)
- if (editorModeVal) {
- // clean up the old key
- customLocalStorage.removeItem(editorModeKey)
- }
- const lastUsedMode = this.localStorage(`editor.lastUsedMode`)
- if (lastUsedMode) {
- return lastUsedMode === 'visual'
- } else {
- return editorModeVal === 'rich-text'
- }
- }
- autoOpenDoc() {
- const open_doc_id =
- this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`) ||
- this.$scope.project.rootDoc_id
- if (open_doc_id == null) {
- return
- }
- const doc = this.ide.fileTreeManager.findEntityById(open_doc_id)
- if (doc == null) {
- return
- }
- return this.openDoc(doc)
- }
- openDocId(doc_id, options) {
- if (options == null) {
- options = {}
- }
- const doc = this.ide.fileTreeManager.findEntityById(doc_id)
- if (doc == null) {
- return
- }
- return this.openDoc(doc, options)
- }
- jumpToLine(options) {
- return this.$scope.$broadcast(
- 'editor:gotoLine',
- options.gotoLine,
- options.gotoColumn,
- options.syncToPdf
- )
- }
- openDoc(doc, options) {
- if (options == null) {
- options = {}
- }
- debugConsole.log(`[openDoc] Opening ${doc.id}`)
- if (this.$scope.ui.view === 'editor') {
- // store position of previous doc before switching docs
- this.$scope.$broadcast('store-doc-position')
- }
- this.$scope.ui.view = 'editor'
- const done = isNewDoc => {
- const eventName = 'doc:after-opened'
- this.$scope.$broadcast(eventName, { isNewDoc })
- window.dispatchEvent(new CustomEvent(eventName, { detail: isNewDoc }))
- if (options.gotoLine != null) {
- // allow Ace to display document before moving, delay until next tick
- // added delay to make this happen later that gotoStoredPosition in
- // CursorPositionManager
- setTimeout(() => this.jumpToLine(options))
- // when opening a doc in CM6, jump to the line again after a stored scroll position has been restored
- if (isNewDoc) {
- window.addEventListener(
- 'editor:scroll-position-restored',
- () => this.jumpToLine(options),
- { once: true }
- )
- }
- } else if (options.gotoOffset != null) {
- setTimeout(() => {
- this.$scope.$broadcast('editor:gotoOffset', options.gotoOffset)
- })
- }
- }
- // If we already have the document open we can return at this point.
- // Note: only use forceReopen:true to override this when the document is
- // is out of sync and needs to be reloaded from the server.
- if (doc.id === this.$scope.editor.open_doc_id && !options.forceReopen) {
- // automatically update the file tree whenever the file is opened
- this.ide.fileTreeManager.selectEntity(doc)
- this.$scope.$broadcast('file-tree.reselectDoc', doc.id)
- this.$scope.$apply(() => {
- return done(false)
- })
- return
- }
- this.$scope.$applyAsync(() => {
- // We're now either opening a new document or reloading a broken one.
- this.$scope.editor.open_doc_id = doc.id
- this.$scope.editor.open_doc_name = doc.name
- this.ide.localStorage(`doc.open_id.${this.$scope.project_id}`, doc.id)
- this.ide.fileTreeManager.selectEntity(doc)
- this.$scope.editor.opening = true
- return this._openNewDocument(doc, (error, sharejs_doc) => {
- if (error && error.message === 'another document was loaded') {
- debugConsole.log(
- `[openDoc] another document was loaded while ${doc.id} was loading`
- )
- return
- }
- if (error != null) {
- this.ide.showGenericMessageModal(
- 'Error opening document',
- 'Sorry, something went wrong opening this document. Please try again.'
- )
- return
- }
- this._syncTrackChangesState(sharejs_doc)
- this.$scope.$broadcast('doc:opened')
- return this.$scope.$applyAsync(() => {
- this.$scope.editor.opening = false
- this.$scope.editor.sharejs_doc = sharejs_doc
- return done(true)
- })
- })
- })
- }
- _openNewDocument(doc, callback) {
- // Leave the current document
- // - when we are opening a different new one, to avoid race conditions
- // between leaving and joining the same document
- // - when the current one has pending ops that need flushing, to avoid
- // race conditions from cleanup
- const current_sharejs_doc = this.$scope.editor.sharejs_doc
- const currentDocId = current_sharejs_doc && current_sharejs_doc.doc_id
- const hasBufferedOps =
- current_sharejs_doc && current_sharejs_doc.hasBufferedOps()
- const changingDoc = current_sharejs_doc && currentDocId !== doc.id
- if (changingDoc || hasBufferedOps) {
- debugConsole.log('[_openNewDocument] Leaving existing open doc...')
- // Do not trigger any UI changes from remote operations
- this._unbindFromDocumentEvents(current_sharejs_doc)
- // Keep listening for out-of-sync and similar errors.
- this._attachErrorHandlerToDocument(doc, current_sharejs_doc)
- // Teardown the Document -> ShareJsDoc -> sharejs doc
- // By the time this completes, the Document instance is no longer
- // registered in Document.openDocs and _doOpenNewDocument can start
- // from scratch -- read: no corrupted internal state.
- const editorOpenDocEpoch = ++this.editorOpenDocEpoch
- current_sharejs_doc.leaveAndCleanUp(error => {
- if (error) {
- debugConsole.log(
- `[_openNewDocument] error leaving doc ${currentDocId}`,
- error
- )
- return callback(error)
- }
- if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
- debugConsole.log(
- `[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
- )
- return callback(new Error('another document was loaded'))
- }
- this._doOpenNewDocument(doc, callback)
- })
- } else {
- this._doOpenNewDocument(doc, callback)
- }
- }
- _doOpenNewDocument(doc, callback) {
- if (callback == null) {
- callback = function () {}
- }
- debugConsole.log('[_doOpenNewDocument] Opening...')
- const new_sharejs_doc = Document.getDocument(this.ide, doc.id)
- const editorOpenDocEpoch = ++this.editorOpenDocEpoch
- return new_sharejs_doc.join(error => {
- if (error != null) {
- debugConsole.log(
- `[_doOpenNewDocument] error joining doc ${doc.id}`,
- error
- )
- return callback(error)
- }
- if (this.editorOpenDocEpoch !== editorOpenDocEpoch) {
- debugConsole.log(
- `[openNewDocument] editorOpenDocEpoch mismatch ${this.editorOpenDocEpoch} vs ${editorOpenDocEpoch}`
- )
- new_sharejs_doc.leaveAndCleanUp()
- return callback(new Error('another document was loaded'))
- }
- this._bindToDocumentEvents(doc, new_sharejs_doc)
- return callback(null, new_sharejs_doc)
- })
- }
- _attachErrorHandlerToDocument(doc, sharejs_doc) {
- sharejs_doc.on('error', (error, meta, editorContent) => {
- let message
- if ((error != null ? error.message : undefined) != null) {
- ;({ message } = error)
- } else if (typeof error === 'string') {
- message = error
- } else {
- message = ''
- }
- if (/maxDocLength/.test(message)) {
- this.$scope.docTooLongErrorShown = true
- this.openDoc(doc, { forceReopen: true })
- const genericMessageModal = this.ide.showGenericMessageModal(
- 'Document Too Long',
- 'Sorry, this file is too long to be edited manually. Please upload it directly.'
- )
- genericMessageModal.result.finally(() => {
- this.$scope.docTooLongErrorShown = false
- })
- } else if (/too many comments or tracked changes/.test(message)) {
- this.ide.showGenericMessageModal(
- 'Too many comments or tracked changes',
- 'Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments.'
- )
- } else if (!this.$scope.docTooLongErrorShown) {
- // Do not allow this doc to open another error modal.
- sharejs_doc.off('error')
- // Preserve the sharejs contents before the teardown.
- editorContent =
- typeof editorContent === 'string'
- ? editorContent
- : sharejs_doc.doc._doc.snapshot
- // Tear down the ShareJsDoc.
- if (sharejs_doc.doc) sharejs_doc.doc.clearInflightAndPendingOps()
- // Do not re-join after re-connecting.
- sharejs_doc.leaveAndCleanUp()
- this.ide.connectionManager.disconnect({ permanent: true })
- this.ide.reportError(error, meta)
- // Tell the user about the error state.
- this.$scope.editor.error_state = true
- this.ide.showOutOfSyncModal(
- 'Out of sync',
- "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>",
- editorContent
- )
- // Do not forceReopen the document.
- return
- }
- const removeHandler = this.$scope.$on('project:joined', () => {
- this.openDoc(doc, { forceReopen: true })
- removeHandler()
- })
- })
- }
- _bindToDocumentEvents(doc, sharejs_doc) {
- this._attachErrorHandlerToDocument(doc, sharejs_doc)
- return sharejs_doc.on('externalUpdate', update => {
- if (this._ignoreExternalUpdates) {
- return
- }
- if (
- _.property(['meta', 'type'])(update) === 'external' &&
- _.property(['meta', 'source'])(update) === 'git-bridge'
- ) {
- return
- }
- if (update?.meta?.source === 'file-revert') {
- return
- }
- return this.ide.showGenericMessageModal(
- 'Document Updated Externally',
- '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.'
- )
- })
- }
- _unbindFromDocumentEvents(document) {
- return document.off()
- }
- getCurrentDocValue() {
- return this.$scope.editor.sharejs_doc != null
- ? this.$scope.editor.sharejs_doc.getSnapshot()
- : undefined
- }
- getCurrentDocId() {
- return this.$scope.editor.open_doc_id
- }
- startIgnoringExternalUpdates() {
- return (this._ignoreExternalUpdates = true)
- }
- stopIgnoringExternalUpdates() {
- return (this._ignoreExternalUpdates = false)
- }
- _syncTrackChangesState(doc) {
- let tryToggle
- if (doc == null) {
- return
- }
- if (this._syncTimeout != null) {
- clearTimeout(this._syncTimeout)
- this._syncTimeout = null
- }
- const want = this.$scope.editor.wantTrackChanges
- const have = doc.getTrackingChanges()
- if (want === have) {
- this.$scope.editor.trackChanges = want
- return
- }
- return (tryToggle = () => {
- const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
- if (saved) {
- doc.setTrackingChanges(want)
- return this.$scope.$apply(() => {
- return (this.$scope.editor.trackChanges = want)
- })
- } else {
- return (this._syncTimeout = setTimeout(tryToggle, 100))
- }
- })()
- }
- }
- EditorManager.initClass()
- return EditorManager
- })()
|