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