editor-manager-context.tsx 22 KB

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