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