editor-manager-context.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. import {
  2. createContext,
  3. FC,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. } from 'react'
  11. import { sendMB } from '@/infrastructure/event-tracking'
  12. import useScopeValue from '@/shared/hooks/use-scope-value'
  13. import { useIdeContext } from '@/shared/context/ide-context'
  14. import { OpenDocuments } from '@/features/ide-react/editor/open-documents'
  15. import EditorWatchdogManager from '@/features/ide-react/connection/editor-watchdog-manager'
  16. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  17. import { useConnectionContext } from '@/features/ide-react/context/connection-context'
  18. import { debugConsole } from '@/utils/debugging'
  19. import { DocumentContainer } from '@/features/ide-react/editor/document-container'
  20. import { useLayoutContext } from '@/shared/context/layout-context'
  21. import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
  22. import { Doc } from '../../../../../types/doc'
  23. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  24. import {
  25. findDocEntityById,
  26. findFileRefEntityById,
  27. } from '@/features/ide-react/util/find-doc-entity-by-id'
  28. import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
  29. import { useModalsContext } from '@/features/ide-react/context/modals-context'
  30. import { useTranslation } from 'react-i18next'
  31. import customLocalStorage from '@/infrastructure/local-storage'
  32. import useEventListener from '@/shared/hooks/use-event-listener'
  33. import { EditorType } from '@/features/ide-react/editor/types/editor-type'
  34. import { DocId } from '../../../../../types/project-settings'
  35. import { Update } from '@/features/history/services/types/update'
  36. import { useDebugDiffTracker } from '../hooks/use-debug-diff-tracker'
  37. import { useEditorContext } from '@/shared/context/editor-context'
  38. import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
  39. import { BinaryFile } from '@/features/file-view/types/binary-file'
  40. import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
  41. export interface GotoOffsetOptions {
  42. gotoOffset: number
  43. }
  44. interface OpenDocOptions
  45. extends Partial<GotoLineOptions>,
  46. Partial<GotoOffsetOptions> {
  47. gotoOffset?: number
  48. forceReopen?: boolean
  49. keepCurrentView?: boolean
  50. }
  51. export type EditorManager = {
  52. getEditorType: () => EditorType | null
  53. showSymbolPalette: boolean
  54. currentDocument: DocumentContainer | null
  55. currentDocumentId: DocId | null
  56. getCurrentDocValue: () => string | null
  57. getCurrentDocumentId: () => DocId | null
  58. startIgnoringExternalUpdates: () => void
  59. stopIgnoringExternalUpdates: () => void
  60. openDocWithId: (docId: string, options?: OpenDocOptions) => void
  61. openDoc: (document: Doc, options?: OpenDocOptions) => void
  62. openDocs: OpenDocuments
  63. openFileWithId: (fileId: string) => void
  64. openInitialDoc: (docId: string) => void
  65. openDocName: string | null
  66. setOpenDocName: (openDocName: string) => void
  67. isLoading: boolean
  68. trackChanges: boolean
  69. jumpToLine: (options: GotoLineOptions) => void
  70. wantTrackChanges: boolean
  71. setWantTrackChanges: React.Dispatch<
  72. React.SetStateAction<EditorManager['wantTrackChanges']>
  73. >
  74. debugTimers: React.MutableRefObject<Record<string, number>>
  75. }
  76. function hasGotoLine(options: OpenDocOptions): options is GotoLineOptions {
  77. return typeof options.gotoLine === 'number'
  78. }
  79. function hasGotoOffset(options: OpenDocOptions): options is GotoOffsetOptions {
  80. return typeof options.gotoOffset === 'number'
  81. }
  82. export type EditorScopeValue = {
  83. showSymbolPalette: false
  84. toggleSymbolPalette: () => void
  85. sharejs_doc: DocumentContainer | null
  86. open_doc_id: string | null
  87. open_doc_name: string | null
  88. opening: boolean
  89. trackChanges: boolean
  90. wantTrackChanges: boolean
  91. showVisual: boolean
  92. newSourceEditor: boolean
  93. error_state: boolean
  94. }
  95. export const EditorManagerContext = createContext<EditorManager | undefined>(
  96. undefined
  97. )
  98. export const EditorManagerProvider: FC = ({ children }) => {
  99. const { t } = useTranslation()
  100. const { scopeStore } = useIdeContext()
  101. const { reportError, eventEmitter, projectId } = useIdeReactContext()
  102. const { setOutOfSync } = useEditorContext()
  103. const { socket, closeConnection, connectionState } = useConnectionContext()
  104. const { view, setView } = useLayoutContext()
  105. const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
  106. useModalsContext()
  107. const [showSymbolPalette, setShowSymbolPalette] = useScopeValue<boolean>(
  108. 'editor.showSymbolPalette'
  109. )
  110. const [showVisual] = useScopeValue<boolean>('editor.showVisual')
  111. const [currentDocument, setCurrentDocument] =
  112. useScopeValue<DocumentContainer | null>('editor.sharejs_doc')
  113. const [currentDocumentId, setCurrentDocumentId] = useScopeValue<DocId | null>(
  114. 'editor.open_doc_id'
  115. )
  116. const [openDocName, setOpenDocName] = useScopeValue<string | null>(
  117. 'editor.open_doc_name'
  118. )
  119. const [opening, setOpening] = useScopeValue<boolean>('editor.opening')
  120. const [errorState, setIsInErrorState] =
  121. useScopeValue<boolean>('editor.error_state')
  122. const [trackChanges, setTrackChanges] = useScopeValue<boolean>(
  123. 'editor.trackChanges'
  124. )
  125. const [wantTrackChanges, setWantTrackChanges] = useScopeValue<boolean>(
  126. 'editor.wantTrackChanges'
  127. )
  128. const wantTrackChangesRef = useRef(wantTrackChanges)
  129. useEffect(() => {
  130. wantTrackChangesRef.current = wantTrackChanges
  131. }, [wantTrackChanges])
  132. const goToLineEmitter = useScopeEventEmitter('editor:gotoLine')
  133. const { fileTreeData } = useFileTreeData()
  134. const [ignoringExternalUpdates, setIgnoringExternalUpdates] = useState(false)
  135. const { createDebugDiff, debugTimers } = useDebugDiffTracker(
  136. projectId,
  137. currentDocument
  138. )
  139. const [globalEditorWatchdogManager] = useState(
  140. () =>
  141. new EditorWatchdogManager({
  142. onTimeoutHandler: (meta: Record<string, any>) => {
  143. let diffSize: number | null = null
  144. createDebugDiff()
  145. .then(calculatedDiffSize => {
  146. diffSize = calculatedDiffSize
  147. })
  148. .finally(() => {
  149. sendMB('losing-edits', {
  150. ...meta,
  151. diffSize,
  152. timers: debugTimers.current,
  153. })
  154. reportError('losing-edits', {
  155. ...meta,
  156. diffSize,
  157. timers: debugTimers.current,
  158. })
  159. })
  160. },
  161. })
  162. )
  163. // Store the most recent document error and consume it in an effect, which
  164. // prevents circular dependencies in useCallbacks
  165. const [docError, setDocError] = useState<{
  166. doc: Doc
  167. document: DocumentContainer
  168. error: Error | string
  169. meta?: Record<string, any>
  170. editorContent?: string
  171. } | null>(null)
  172. const [docTooLongErrorShown, setDocTooLongErrorShown] = useState(false)
  173. useEffect(() => {
  174. if (!genericModalVisible) {
  175. setDocTooLongErrorShown(false)
  176. }
  177. }, [genericModalVisible])
  178. const [openDocs] = useState(
  179. () => new OpenDocuments(socket, globalEditorWatchdogManager, eventEmitter)
  180. )
  181. const currentDocumentIdStorageKey = `doc.open_id.${projectId}`
  182. // Persist the open document ID to local storage
  183. useEffect(() => {
  184. if (currentDocumentId) {
  185. customLocalStorage.setItem(currentDocumentIdStorageKey, currentDocumentId)
  186. }
  187. }, [currentDocumentId, currentDocumentIdStorageKey])
  188. const editorOpenDocEpochRef = useRef(0)
  189. // TODO: This looks dodgy because it wraps a state setter and is itself
  190. // stored in React state in the scope store. The problem is that it needs to
  191. // be exposed via the scope store because some components access it that way;
  192. // it would be better to simply access it from a context, but the current
  193. // implementation in EditorManager interacts with Angular scope to update
  194. // the layout. Once Angular is gone, this can become a context method.
  195. useEffect(() => {
  196. scopeStore.set('editor.toggleSymbolPalette', () => {
  197. setShowSymbolPalette(show => {
  198. const newValue = !show
  199. sendMB(newValue ? 'symbol-palette-show' : 'symbol-palette-hide')
  200. return newValue
  201. })
  202. })
  203. }, [scopeStore, setShowSymbolPalette])
  204. const getEditorType = useCallback((): EditorType | null => {
  205. if (!currentDocument) {
  206. return null
  207. }
  208. return showVisual ? 'cm6-rich-text' : 'cm6'
  209. }, [currentDocument, showVisual])
  210. const getCurrentDocValue = useCallback(() => {
  211. return currentDocument?.getSnapshot() ?? null
  212. }, [currentDocument])
  213. const getCurrentDocumentId = useCallback(
  214. () => currentDocumentId,
  215. [currentDocumentId]
  216. )
  217. const startIgnoringExternalUpdates = useCallback(
  218. () => setIgnoringExternalUpdates(true),
  219. []
  220. )
  221. const stopIgnoringExternalUpdates = useCallback(
  222. () => setIgnoringExternalUpdates(false),
  223. []
  224. )
  225. const jumpToLine = useCallback(
  226. (options: GotoLineOptions) => {
  227. goToLineEmitter(options)
  228. },
  229. [goToLineEmitter]
  230. )
  231. const unbindFromDocumentEvents = (document: DocumentContainer) => {
  232. document.off()
  233. }
  234. const attachErrorHandlerToDocument = useCallback(
  235. (doc: Doc, document: DocumentContainer) => {
  236. document.on(
  237. 'error',
  238. (
  239. error: Error | string,
  240. meta?: Record<string, any>,
  241. editorContent?: string
  242. ) => {
  243. setDocError({ doc, document, error, meta, editorContent })
  244. }
  245. )
  246. },
  247. []
  248. )
  249. const bindToDocumentEvents = useCallback(
  250. (doc: Doc, document: DocumentContainer) => {
  251. attachErrorHandlerToDocument(doc, document)
  252. document.on('externalUpdate', (update: Update) => {
  253. if (ignoringExternalUpdates) {
  254. return
  255. }
  256. if (
  257. update.meta.type === 'external' &&
  258. update.meta.source === 'git-bridge'
  259. ) {
  260. return
  261. }
  262. if (
  263. update.meta.origin?.kind === 'file-restore' ||
  264. update.meta.origin?.kind === 'project-restore'
  265. ) {
  266. return
  267. }
  268. showGenericMessageModal(
  269. t('document_updated_externally'),
  270. t('document_updated_externally_detail')
  271. )
  272. })
  273. },
  274. [
  275. attachErrorHandlerToDocument,
  276. ignoringExternalUpdates,
  277. showGenericMessageModal,
  278. t,
  279. ]
  280. )
  281. const syncTimeoutRef = useRef<number | null>(null)
  282. const syncTrackChangesState = useCallback(
  283. (doc: DocumentContainer) => {
  284. if (!doc) {
  285. return
  286. }
  287. if (syncTimeoutRef.current) {
  288. window.clearTimeout(syncTimeoutRef.current)
  289. syncTimeoutRef.current = null
  290. }
  291. const want = wantTrackChangesRef.current
  292. const have = doc.getTrackingChanges()
  293. if (want === have) {
  294. setTrackChanges(want)
  295. return
  296. }
  297. const tryToggle = () => {
  298. const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
  299. if (saved) {
  300. doc.setTrackingChanges(want)
  301. setTrackChanges(want)
  302. } else {
  303. syncTimeoutRef.current = window.setTimeout(tryToggle, 100)
  304. }
  305. }
  306. tryToggle()
  307. },
  308. [setTrackChanges]
  309. )
  310. const doOpenNewDocument = useCallback(
  311. (doc: Doc) =>
  312. new Promise<DocumentContainer>((resolve, reject) => {
  313. debugConsole.log('[doOpenNewDocument] Opening...')
  314. const newDocument = openDocs.getDocument(doc._id)
  315. if (!newDocument) {
  316. debugConsole.error(`No open document with ID '${doc._id}' found`)
  317. reject(new Error('no open document found'))
  318. return
  319. }
  320. const preJoinEpoch = ++editorOpenDocEpochRef.current
  321. newDocument.join(error => {
  322. if (error) {
  323. debugConsole.log(
  324. `[doOpenNewDocument] error joining doc ${doc._id}`,
  325. error
  326. )
  327. reject(error)
  328. return
  329. }
  330. if (editorOpenDocEpochRef.current !== preJoinEpoch) {
  331. debugConsole.log(
  332. `[doOpenNewDocument] editorOpenDocEpoch mismatch ${editorOpenDocEpochRef.current} vs ${preJoinEpoch}`
  333. )
  334. newDocument.leaveAndCleanUp()
  335. reject(new Error('another document was loaded'))
  336. return
  337. }
  338. bindToDocumentEvents(doc, newDocument)
  339. resolve(newDocument)
  340. })
  341. }),
  342. [bindToDocumentEvents, openDocs]
  343. )
  344. const openNewDocument = useCallback(
  345. async (doc: Doc): Promise<DocumentContainer> => {
  346. // Leave the current document
  347. // - when we are opening a different new one, to avoid race conditions
  348. // between leaving and joining the same document
  349. // - when the current one has pending ops that need flushing, to avoid
  350. // race conditions from cleanup
  351. const currentDocumentId = currentDocument?.doc_id
  352. const hasBufferedOps = currentDocument && currentDocument.hasBufferedOps()
  353. const changingDoc = currentDocument && currentDocumentId !== doc._id
  354. if (changingDoc || hasBufferedOps) {
  355. debugConsole.log('[openNewDocument] Leaving existing open doc...')
  356. // Do not trigger any UI changes from remote operations
  357. unbindFromDocumentEvents(currentDocument)
  358. // Keep listening for out-of-sync and similar errors.
  359. attachErrorHandlerToDocument(doc, currentDocument)
  360. // Teardown the Document -> ShareJsDoc -> sharejs doc
  361. // By the time this completes, the Document instance is no longer
  362. // registered in OpenDocuments and doOpenNewDocument can start
  363. // from scratch -- read: no corrupted internal state.
  364. const preLeaveEpoch = ++editorOpenDocEpochRef.current
  365. try {
  366. await currentDocument.leaveAndCleanUpPromise()
  367. } catch (error) {
  368. debugConsole.log(
  369. `[openNewDocument] error leaving doc ${currentDocumentId}`,
  370. error
  371. )
  372. throw error
  373. }
  374. if (editorOpenDocEpochRef.current !== preLeaveEpoch) {
  375. debugConsole.log(
  376. `[openNewDocument] editorOpenDocEpoch mismatch ${editorOpenDocEpochRef.current} vs ${preLeaveEpoch}`
  377. )
  378. throw new Error('another document was loaded')
  379. }
  380. }
  381. return doOpenNewDocument(doc)
  382. },
  383. [attachErrorHandlerToDocument, doOpenNewDocument, currentDocument]
  384. )
  385. const currentDocumentIdRef = useRef(currentDocumentId)
  386. useEffect(() => {
  387. currentDocumentIdRef.current = currentDocumentId
  388. }, [currentDocumentId])
  389. const openDoc = useCallback(
  390. async (doc: Doc, options: OpenDocOptions = {}) => {
  391. debugConsole.log(`[openDoc] Opening ${doc._id}`)
  392. const { promise, resolve, reject } = Promise.withResolvers<Doc>()
  393. if (view === 'editor') {
  394. // store position of previous doc before switching docs
  395. eventEmitter.emit('store-doc-position')
  396. }
  397. if (!options.keepCurrentView) {
  398. setView('editor')
  399. }
  400. const done = (isNewDoc: boolean) => {
  401. window.dispatchEvent(
  402. new CustomEvent('doc:after-opened', {
  403. detail: { isNewDoc, docId: doc._id },
  404. })
  405. )
  406. window.dispatchEvent(
  407. new CustomEvent('entity:opened', {
  408. detail: doc._id,
  409. })
  410. )
  411. if (hasGotoLine(options)) {
  412. window.setTimeout(() => jumpToLine(options))
  413. // Jump to the line again after a stored scroll position has been restored
  414. if (isNewDoc) {
  415. window.addEventListener(
  416. 'editor:scroll-position-restored',
  417. () => jumpToLine(options),
  418. { once: true }
  419. )
  420. }
  421. } else if (hasGotoOffset(options)) {
  422. window.setTimeout(() => {
  423. eventEmitter.emit('editor:gotoOffset', options)
  424. })
  425. }
  426. resolve(doc)
  427. }
  428. // If we already have the document open, or are opening the document, we can return at this point.
  429. // Note: only use forceReopen:true to override this when the document is
  430. // out of sync and needs to be reloaded from the server.
  431. if (doc._id === currentDocumentIdRef.current && !options.forceReopen) {
  432. done(false)
  433. return
  434. }
  435. // We're now either opening a new document or reloading a broken one.
  436. currentDocumentIdRef.current = doc._id as DocId
  437. setCurrentDocumentId(doc._id as DocId)
  438. setOpenDocName(doc.name)
  439. setOpening(true)
  440. try {
  441. const document = await openNewDocument(doc)
  442. syncTrackChangesState(document)
  443. setOpening(false)
  444. setCurrentDocument(document)
  445. done(true)
  446. } catch (error: any) {
  447. if (error?.message === 'another document was loaded') {
  448. debugConsole.log(
  449. `[openDoc] another document was loaded while ${doc._id} was loading`
  450. )
  451. return
  452. }
  453. debugConsole.error('Error opening document', error)
  454. showGenericMessageModal(
  455. t('error_opening_document'),
  456. t('error_opening_document_detail')
  457. )
  458. reject(error)
  459. }
  460. return promise
  461. },
  462. [
  463. eventEmitter,
  464. jumpToLine,
  465. openNewDocument,
  466. setCurrentDocument,
  467. setCurrentDocumentId,
  468. setOpenDocName,
  469. setOpening,
  470. setView,
  471. showGenericMessageModal,
  472. syncTrackChangesState,
  473. t,
  474. view,
  475. ]
  476. )
  477. const openDocWithId = useCallback(
  478. (docId: string, options: OpenDocOptions = {}) => {
  479. const doc = findDocEntityById(fileTreeData, docId)
  480. if (!doc) {
  481. return
  482. }
  483. openDoc(doc, options)
  484. },
  485. [fileTreeData, openDoc]
  486. )
  487. const [, setOpenFile] = useScopeValueSetterOnly<BinaryFile | null>('openFile')
  488. const openFileWithId = useCallback(
  489. (fileRefId: string) => {
  490. const fileRef = findFileRefEntityById(fileTreeData, fileRefId)
  491. if (!fileRef) {
  492. return
  493. }
  494. setOpenFile(convertFileRefToBinaryFile(fileRef))
  495. window.dispatchEvent(
  496. new CustomEvent('entity:opened', {
  497. detail: fileRef._id,
  498. })
  499. )
  500. },
  501. [fileTreeData, setOpenFile]
  502. )
  503. const openInitialDoc = useCallback(
  504. (fallbackDocId: string) => {
  505. const docId =
  506. customLocalStorage.getItem(currentDocumentIdStorageKey) || fallbackDocId
  507. if (docId) {
  508. openDocWithId(docId)
  509. }
  510. },
  511. [currentDocumentIdStorageKey, openDocWithId]
  512. )
  513. useEffect(() => {
  514. if (docError) {
  515. const { doc, document, error, meta } = docError
  516. let { editorContent } = docError
  517. const message = typeof error === 'string' ? error : (error?.message ?? '')
  518. // Clear document error so that it's only handled once
  519. setDocError(null)
  520. if (message.includes('maxDocLength')) {
  521. openDoc(doc, { forceReopen: true })
  522. const hasTrackedDeletes =
  523. document.ranges != null &&
  524. document.ranges.changes.some(change => 'd' in change.op)
  525. const explanation = hasTrackedDeletes
  526. ? `${t('document_too_long_detail')} ${t('document_too_long_tracked_deletes')}`
  527. : t('document_too_long_detail')
  528. showGenericMessageModal(t('document_too_long'), explanation)
  529. setDocTooLongErrorShown(true)
  530. } else if (/too many comments or tracked changes/.test(message)) {
  531. showGenericMessageModal(
  532. t('too_many_comments_or_tracked_changes'),
  533. t('too_many_comments_or_tracked_changes_detail')
  534. )
  535. } else if (!docTooLongErrorShown) {
  536. // Do not allow this doc to open another error modal.
  537. document.off('error')
  538. // Preserve the sharejs contents before the teardown.
  539. // eslint-disable-next-line no-unused-vars
  540. editorContent =
  541. typeof editorContent === 'string'
  542. ? editorContent
  543. : document.doc?._doc.snapshot
  544. // Tear down the ShareJsDoc.
  545. if (document.doc) document.doc.clearInflightAndPendingOps()
  546. // Do not re-join after re-connecting.
  547. document.leaveAndCleanUp()
  548. closeConnection('out-of-sync')
  549. reportError(error, meta)
  550. // Tell the user about the error state.
  551. setIsInErrorState(true)
  552. // Ensure that the editor is locked
  553. setOutOfSync(true)
  554. // Display the "out of sync" modal
  555. showOutOfSyncModal(editorContent || '')
  556. // Do not forceReopen the document.
  557. return
  558. }
  559. const handleProjectJoined = () => {
  560. openDoc(doc, { forceReopen: true })
  561. }
  562. eventEmitter.once('project:joined', handleProjectJoined)
  563. return () => {
  564. eventEmitter.off('project:joined', handleProjectJoined)
  565. }
  566. }
  567. }, [
  568. closeConnection,
  569. docError,
  570. docTooLongErrorShown,
  571. eventEmitter,
  572. openDoc,
  573. reportError,
  574. setIsInErrorState,
  575. showGenericMessageModal,
  576. showOutOfSyncModal,
  577. setOutOfSync,
  578. t,
  579. ])
  580. useEventListener(
  581. 'editor:insert-symbol',
  582. useCallback(() => {
  583. sendMB('symbol-palette-insert')
  584. }, [])
  585. )
  586. useEventListener(
  587. 'flush-changes',
  588. useCallback(() => {
  589. openDocs.flushAll()
  590. }, [openDocs])
  591. )
  592. useEventListener(
  593. 'blur',
  594. useCallback(() => {
  595. openDocs.flushAll()
  596. }, [openDocs])
  597. )
  598. // Flush changes before disconnecting
  599. useEffect(() => {
  600. if (connectionState.forceDisconnected) {
  601. openDocs.flushAll()
  602. }
  603. }, [connectionState.forceDisconnected, openDocs])
  604. // Watch for changes in wantTrackChanges
  605. const previousWantTrackChangesRef = useRef(wantTrackChanges)
  606. useEffect(() => {
  607. if (
  608. currentDocument &&
  609. wantTrackChanges !== previousWantTrackChangesRef.current
  610. ) {
  611. previousWantTrackChangesRef.current = wantTrackChanges
  612. syncTrackChangesState(currentDocument)
  613. }
  614. }, [currentDocument, syncTrackChangesState, wantTrackChanges])
  615. const isLoading = Boolean(
  616. (!currentDocument || opening) && !errorState && currentDocumentId
  617. )
  618. const value: EditorManager = useMemo(
  619. () => ({
  620. getEditorType,
  621. showSymbolPalette,
  622. currentDocument,
  623. currentDocumentId,
  624. getCurrentDocValue,
  625. getCurrentDocumentId,
  626. startIgnoringExternalUpdates,
  627. stopIgnoringExternalUpdates,
  628. openDocWithId,
  629. openDoc,
  630. openDocs,
  631. openDocName,
  632. setOpenDocName,
  633. trackChanges,
  634. isLoading,
  635. openFileWithId,
  636. openInitialDoc,
  637. jumpToLine,
  638. wantTrackChanges,
  639. setWantTrackChanges,
  640. debugTimers,
  641. }),
  642. [
  643. getEditorType,
  644. showSymbolPalette,
  645. currentDocument,
  646. currentDocumentId,
  647. getCurrentDocValue,
  648. getCurrentDocumentId,
  649. startIgnoringExternalUpdates,
  650. stopIgnoringExternalUpdates,
  651. openDocWithId,
  652. openDoc,
  653. openDocs,
  654. openFileWithId,
  655. openInitialDoc,
  656. openDocName,
  657. setOpenDocName,
  658. trackChanges,
  659. isLoading,
  660. jumpToLine,
  661. wantTrackChanges,
  662. setWantTrackChanges,
  663. debugTimers,
  664. ]
  665. )
  666. return (
  667. <EditorManagerContext.Provider value={value}>
  668. {children}
  669. </EditorManagerContext.Provider>
  670. )
  671. }
  672. export function useEditorManagerContext(): EditorManager {
  673. const context = useContext(EditorManagerContext)
  674. if (!context) {
  675. throw new Error(
  676. 'useEditorManagerContext is only available inside EditorManagerProvider'
  677. )
  678. }
  679. return context
  680. }