editor-manager-context.tsx 22 KB

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