editor-manager-context.tsx 22 KB

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