2020-06-23 04:45:38 -04:00
import _ from 'lodash'
2018-11-05 05:06:39 -05:00
/ * e s l i n t - d i s a b l e
2021-01-12 06:24:10 -05:00
camelcase ,
2022-05-16 10:25:49 -04:00
n / handle - callback - err ,
2021-01-12 06:24:10 -05:00
max - len ,
no - return - assign ,
* /
2018-11-05 05:06:39 -05:00
// 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
* /
2020-05-19 05:02:56 -04:00
import Document from './Document'
2023-08-15 05:53:00 -04:00
import './directives/formattingButtons'
2020-05-19 05:02:56 -04:00
import './directives/toggleSwitch'
import './controllers/SavingNotificationController'
2021-11-30 09:54:14 -05:00
import './controllers/CompileButton'
2022-07-20 10:01:48 -04:00
import './controllers/SwitchToPDFButton'
2023-11-28 06:20:24 -05:00
import '../metadata/services/metadata'
2023-09-27 05:45:49 -04:00
import { debugConsole } from '@/utils/debugging'
2023-12-18 08:52:13 -05:00
import customLocalStorage from '@/infrastructure/local-storage'
2022-06-14 06:58:57 -04:00
2020-05-19 05:02:56 -04:00
let EditorManager
2021-04-14 09:17:21 -04:00
export default EditorManager = ( function ( ) {
2020-05-19 05:02:56 -04:00
EditorManager = class EditorManager {
static initClass ( ) {
this . prototype . _syncTimeout = null
}
2020-12-15 05:23:54 -05:00
2021-05-17 07:50:19 -04:00
constructor ( ide , $scope , localStorage , eventTracking ) {
2020-05-19 05:02:56 -04:00
this . ide = ide
2020-09-15 08:48:40 -04:00
this . editorOpenDocEpoch = 0 // track pending document loads
2020-05-19 05:02:56 -04:00
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 ,
2022-02-15 08:37:46 -05:00
docTooLongErrorShown : false ,
2022-12-12 06:17:41 -05:00
showVisual : this . showVisual ( ) ,
2021-05-17 07:50:19 -04:00
showSymbolPalette : false ,
toggleSymbolPalette : ( ) => {
const newValue = ! this . $scope . editor . showSymbolPalette
this . $scope . editor . showSymbolPalette = newValue
2022-08-18 08:27:13 -04:00
ide . $scope . $emit ( 'south-pane-toggled' , newValue )
2021-05-17 07:50:19 -04:00
eventTracking . sendMB (
newValue ? 'symbol-palette-show' : 'symbol-palette-hide'
)
} ,
insertSymbol : symbol => {
ide . $scope . $emit ( 'editor:replace-selection' , symbol . command )
eventTracking . sendMB ( 'symbol-palette-insert' )
} ,
2022-03-21 10:47:26 -04:00
multiSelectedCount : 0 ,
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
2022-01-10 08:56:36 -05:00
window . addEventListener ( 'editor:insert-symbol' , event => {
this . $scope . editor . insertSymbol ( event . detail )
} )
2020-05-19 05:02:56 -04:00
this . $scope . $on ( 'entity:selected' , ( event , entity ) => {
if ( this . $scope . ui . view !== 'history' && entity . type === 'doc' ) {
return this . openDoc ( entity )
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
} )
2018-11-05 05:06:39 -05:00
2021-01-07 09:22:53 -05:00
this . $scope . $on ( 'entity:no-selection' , ( ) => {
this . $scope . $apply ( ( ) => {
this . $scope . ui . view = null
} )
} )
2020-05-19 05:02:56 -04:00
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
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
const doc = this . ide . fileTreeManager . findEntityById (
this . $scope . project . rootDoc _id
)
if ( doc == null ) {
this . $scope . ui . view = null
return
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
return this . openDoc ( doc )
}
} )
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
let initialized = false
this . $scope . $on ( 'file-tree:initialized' , ( ) => {
if ( ! initialized ) {
initialized = true
return this . autoOpenDoc ( )
}
} )
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
this . $scope . $on ( 'flush-changes' , ( ) => {
return Document . flushAll ( )
} )
2021-09-30 07:29:25 -04:00
// event dispatched by pdf preview
window . addEventListener ( 'flush-changes' , ( ) => {
Document . flushAll ( )
} )
2020-05-19 05:02:56 -04:00
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.
2023-09-27 05:45:49 -04:00
debugConsole . log ( '[EditorManager] forcing flush onblur' )
2020-05-19 05:02:56 -04:00
Document . flushAll ( )
} )
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
this . $scope . $watch ( 'editor.wantTrackChanges' , value => {
if ( value == null ) {
return
}
return this . _syncTrackChangesState ( this . $scope . editor . sharejs _doc )
} )
2021-09-30 07:29:25 -04:00
window . addEventListener ( 'editor:open-doc' , event => {
const { doc , ... options } = event . detail
this . openDoc ( doc , options )
} )
2023-08-11 04:31:38 -04:00
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
}
}
} )
2020-05-19 05:02:56 -04:00
}
2022-03-01 04:54:13 -05:00
getEditorType ( ) {
if ( ! this . $scope . editor . sharejs _doc ) {
return null
}
2023-01-16 07:02:49 -05:00
let editorType = this . $scope . editor . sharejs _doc . editorType ( )
if ( editorType === 'cm6' && this . $scope . editor . showVisual ) {
editorType = 'cm6-rich-text'
}
return editorType
2022-03-01 04:54:13 -05:00
}
2022-12-12 06:17:41 -05:00
showVisual ( ) {
2023-12-18 08:52:13 -05:00
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'
}
2022-12-12 06:17:41 -05:00
}
2020-05-19 05:02:56 -04:00
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
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
return this . openDoc ( doc )
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
openDocId ( doc _id , options ) {
if ( options == null ) {
options = { }
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
const doc = this . ide . fileTreeManager . findEntityById ( doc _id )
if ( doc == null ) {
return
}
return this . openDoc ( doc , options )
}
2018-11-05 05:06:39 -05:00
2020-07-28 05:37:46 -04:00
jumpToLine ( options ) {
return this . $scope . $broadcast (
'editor:gotoLine' ,
options . gotoLine ,
2020-08-24 08:20:54 -04:00
options . gotoColumn ,
options . syncToPdf
2020-07-28 05:37:46 -04:00
)
}
2020-05-19 05:02:56 -04:00
openDoc ( doc , options ) {
if ( options == null ) {
options = { }
2018-11-05 05:06:39 -05:00
}
2023-09-27 05:45:49 -04:00
debugConsole . log ( ` [openDoc] Opening ${ doc . id } ` )
2022-07-27 09:24:16 -04:00
if ( this . $scope . ui . view === 'editor' ) {
// store position of previous doc before switching docs
this . $scope . $broadcast ( 'store-doc-position' )
}
2020-05-19 05:02:56 -04:00
this . $scope . ui . view = 'editor'
2018-11-05 05:06:39 -05:00
2020-08-18 09:08:49 -04:00
const done = isNewDoc => {
2022-02-03 04:38:07 -05:00
const eventName = 'doc:after-opened'
this . $scope . $broadcast ( eventName , { isNewDoc } )
2022-11-03 08:43:59 -04:00
window . dispatchEvent ( new CustomEvent ( eventName , { detail : isNewDoc } ) )
2020-05-19 05:02:56 -04:00
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
2023-03-24 08:07:23 -04:00
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 }
2020-05-19 05:02:56 -04:00
)
2023-03-24 08:07:23 -04:00
}
} else if ( options . gotoOffset != null ) {
setTimeout ( ( ) => {
this . $scope . $broadcast ( 'editor:gotoOffset' , options . gotoOffset )
} )
2018-11-05 05:06:39 -05:00
}
}
2020-05-19 05:02:56 -04:00
// 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 )
2022-04-12 06:44:20 -04:00
this . $scope . $broadcast ( 'file-tree.reselectDoc' , doc . id )
2020-05-19 05:02:56 -04:00
this . $scope . $apply ( ( ) => {
2020-08-18 09:08:49 -04:00
return done ( false )
2020-05-19 05:02:56 -04:00
} )
return
}
2018-11-05 05:06:39 -05:00
2022-11-03 09:02:56 -04:00
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
2018-11-05 05:06:39 -05:00
2022-11-03 09:02:56 -04:00
this . ide . localStorage ( ` doc.open_id. ${ this . $scope . project _id } ` , doc . id )
this . ide . fileTreeManager . selectEntity ( doc )
2020-05-19 05:02:56 -04:00
2022-11-03 09:02:56 -04:00
this . $scope . editor . opening = true
return this . _openNewDocument ( doc , ( error , sharejs _doc ) => {
if ( error && error . message === 'another document was loaded' ) {
2023-09-27 05:45:49 -04:00
debugConsole . log (
2022-11-03 09:02:56 -04:00
` [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
}
2018-11-05 05:06:39 -05:00
2022-11-03 09:02:56 -04:00
this . _syncTrackChangesState ( sharejs _doc )
2018-11-05 05:06:39 -05:00
2022-11-03 09:02:56 -04:00
this . $scope . $broadcast ( 'doc:opened' )
2018-11-05 05:06:39 -05:00
2022-11-03 09:02:56 -04:00
return this . $scope . $applyAsync ( ( ) => {
this . $scope . editor . opening = false
this . $scope . editor . sharejs _doc = sharejs _doc
return done ( true )
} )
2020-05-19 05:02:56 -04:00
} )
} )
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
_openNewDocument ( doc , callback ) {
2020-12-14 04:20:42 -05:00
// 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
2020-05-19 05:02:56 -04:00
const current _sharejs _doc = this . $scope . editor . sharejs _doc
2020-12-14 04:20:42 -05:00
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 ) {
2023-09-27 05:45:49 -04:00
debugConsole . log ( '[_openNewDocument] Leaving existing open doc...' )
2020-12-14 04:20:42 -05:00
// Do not trigger any UI changes from remote operations
2020-05-19 05:02:56 -04:00
this . _unbindFromDocumentEvents ( current _sharejs _doc )
2020-12-14 04:21:04 -05:00
// Keep listening for out-of-sync and similar errors.
this . _attachErrorHandlerToDocument ( doc , current _sharejs _doc )
2020-12-14 04:20:42 -05:00
// 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 ) {
2023-09-27 05:45:49 -04:00
debugConsole . log (
2020-12-14 04:20:42 -05:00
` [_openNewDocument] error leaving doc ${ currentDocId } ` ,
error
)
return callback ( error )
}
if ( this . editorOpenDocEpoch !== editorOpenDocEpoch ) {
2023-09-27 05:45:49 -04:00
debugConsole . log (
2020-12-15 05:23:54 -05:00
` [openNewDocument] editorOpenDocEpoch mismatch ${ this . editorOpenDocEpoch } vs ${ editorOpenDocEpoch } `
2020-12-14 04:20:42 -05:00
)
return callback ( new Error ( 'another document was loaded' ) )
}
this . _doOpenNewDocument ( doc , callback )
} )
} else {
this . _doOpenNewDocument ( doc , callback )
}
}
_doOpenNewDocument ( doc , callback ) {
if ( callback == null ) {
2021-10-27 05:49:18 -04:00
callback = function ( ) { }
2020-05-19 05:02:56 -04:00
}
2023-09-27 05:45:49 -04:00
debugConsole . log ( '[_doOpenNewDocument] Opening...' )
2020-12-14 04:20:42 -05:00
const new _sharejs _doc = Document . getDocument ( this . ide , doc . id )
2020-09-15 08:48:40 -04:00
const editorOpenDocEpoch = ++ this . editorOpenDocEpoch
2020-05-19 05:02:56 -04:00
return new _sharejs _doc . join ( error => {
if ( error != null ) {
2023-09-27 05:45:49 -04:00
debugConsole . log (
2020-12-14 04:20:42 -05:00
` [_doOpenNewDocument] error joining doc ${ doc . id } ` ,
2020-09-16 05:55:33 -04:00
error
)
2020-05-19 05:02:56 -04:00
return callback ( error )
}
2020-09-15 08:48:40 -04:00
if ( this . editorOpenDocEpoch !== editorOpenDocEpoch ) {
2023-09-27 05:45:49 -04:00
debugConsole . log (
2020-12-15 05:23:54 -05:00
` [openNewDocument] editorOpenDocEpoch mismatch ${ this . editorOpenDocEpoch } vs ${ editorOpenDocEpoch } `
2020-09-15 08:48:40 -04:00
)
2020-12-14 04:28:51 -05:00
new _sharejs _doc . leaveAndCleanUp ( )
2020-09-15 08:48:40 -04:00
return callback ( new Error ( 'another document was loaded' ) )
}
2020-05-19 05:02:56 -04:00
this . _bindToDocumentEvents ( doc , new _sharejs _doc )
return callback ( null , new _sharejs _doc )
} )
}
2018-11-05 05:06:39 -05:00
2020-12-14 04:21:04 -05:00
_attachErrorHandlerToDocument ( doc , sharejs _doc ) {
2020-11-04 04:55:49 -05:00
sharejs _doc . on ( 'error' , ( error , meta , editorContent ) => {
2020-05-19 05:02:56 -04:00
let message
if ( ( error != null ? error . message : undefined ) != null ) {
; ( { message } = error )
} else if ( typeof error === 'string' ) {
message = error
} else {
message = ''
}
if ( /maxDocLength/ . test ( message ) ) {
2022-02-15 08:37:46 -05:00
this . $scope . docTooLongErrorShown = true
this . openDoc ( doc , { forceReopen : true } )
const genericMessageModal = this . ide . showGenericMessageModal (
2020-05-19 05:02:56 -04:00
'Document Too Long' ,
'Sorry, this file is too long to be edited manually. Please upload it directly.'
)
2022-02-15 08:37:46 -05:00
genericMessageModal . result . finally ( ( ) => {
this . $scope . docTooLongErrorShown = false
} )
2020-05-19 05:02:56 -04:00
} 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.'
)
2022-02-15 08:37:46 -05:00
} else if ( ! this . $scope . docTooLongErrorShown ) {
2020-12-14 04:29:08 -05:00
// 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 ( )
2020-12-14 04:34:57 -05:00
this . ide . connectionManager . disconnect ( { permanent : true } )
2020-05-19 05:02:56 -04:00
this . ide . reportError ( error , meta )
2020-12-14 04:29:45 -05:00
// Tell the user about the error state.
this . $scope . editor . error _state = true
2020-05-19 05:02:56 -04:00
this . ide . showOutOfSyncModal (
'Out of sync' ,
2021-01-12 06:24:10 -05:00
"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>" ,
2020-12-14 04:29:08 -05:00
editorContent
2020-05-19 05:02:56 -04:00
)
2020-12-14 04:29:08 -05:00
// Do not forceReopen the document.
return
2020-05-19 05:02:56 -04:00
}
const removeHandler = this . $scope . $on ( 'project:joined' , ( ) => {
this . openDoc ( doc , { forceReopen : true } )
removeHandler ( )
2018-11-05 05:06:39 -05:00
} )
2020-05-19 05:02:56 -04:00
} )
2020-12-14 04:21:04 -05:00
}
_bindToDocumentEvents ( doc , sharejs _doc ) {
this . _attachErrorHandlerToDocument ( doc , sharejs _doc )
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
return sharejs _doc . on ( 'externalUpdate' , update => {
if ( this . _ignoreExternalUpdates ) {
return
2018-11-05 05:06:39 -05:00
}
2020-01-29 10:06:29 -05:00
if (
2020-05-19 05:02:56 -04:00
_ . property ( [ 'meta' , 'type' ] ) ( update ) === 'external' &&
_ . property ( [ 'meta' , 'source' ] ) ( update ) === 'git-bridge'
2020-01-29 10:06:29 -05:00
) {
2020-05-19 05:02:56 -04:00
return
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
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.'
)
} )
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
_unbindFromDocumentEvents ( document ) {
return document . off ( )
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
getCurrentDocValue ( ) {
return this . $scope . editor . sharejs _doc != null
? this . $scope . editor . sharejs _doc . getSnapshot ( )
: undefined
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
getCurrentDocId ( ) {
return this . $scope . editor . open _doc _id
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
startIgnoringExternalUpdates ( ) {
return ( this . _ignoreExternalUpdates = true )
}
2018-11-05 05:06:39 -05:00
2020-05-19 05:02:56 -04:00
stopIgnoringExternalUpdates ( ) {
return ( this . _ignoreExternalUpdates = false )
}
2020-12-15 05:23:54 -05:00
2020-05-19 05:02:56 -04:00
_syncTrackChangesState ( doc ) {
let tryToggle
if ( doc == null ) {
return
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
if ( this . _syncTimeout != null ) {
clearTimeout ( this . _syncTimeout )
this . _syncTimeout = null
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
const want = this . $scope . editor . wantTrackChanges
const have = doc . getTrackingChanges ( )
if ( want === have ) {
this . $scope . editor . trackChanges = want
return
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
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 ) )
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
} ) ( )
2018-11-05 05:06:39 -05:00
}
2020-05-19 05:02:56 -04:00
}
EditorManager . initClass ( )
return EditorManager
2020-12-15 05:23:54 -05:00
} ) ( )