editor-manager-context.tsx 22 KB

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