editor-manager-context.tsx 22 KB

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