editor-manager-context.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  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. }
  44. export type EditorManager = {
  45. getEditorType: () => EditorType | null
  46. showSymbolPalette: boolean
  47. currentDocument: DocumentContainer
  48. currentDocumentId: DocId | null
  49. getCurrentDocValue: () => string | null
  50. getCurrentDocId: () => DocId | null
  51. startIgnoringExternalUpdates: () => void
  52. stopIgnoringExternalUpdates: () => void
  53. openDocId: (docId: string, options?: OpenDocOptions) => void
  54. openDoc: (document: Doc, options?: OpenDocOptions) => void
  55. openDocs: OpenDocuments
  56. openInitialDoc: (docId: string) => void
  57. jumpToLine: (options: GotoLineOptions) => void
  58. wantTrackChanges: boolean
  59. setWantTrackChanges: React.Dispatch<
  60. React.SetStateAction<EditorManager['wantTrackChanges']>
  61. >
  62. debugTimers: React.MutableRefObject<Record<string, number>>
  63. }
  64. function hasGotoLine(options: OpenDocOptions): options is GotoLineOptions {
  65. return typeof options.gotoLine === 'number'
  66. }
  67. function hasGotoOffset(options: OpenDocOptions): options is GotoOffsetOptions {
  68. return typeof options.gotoOffset === 'number'
  69. }
  70. export type EditorScopeValue = {
  71. showSymbolPalette: false
  72. toggleSymbolPalette: () => void
  73. sharejs_doc: DocumentContainer | null
  74. open_doc_id: string | null
  75. open_doc_name: string | null
  76. opening: boolean
  77. trackChanges: boolean
  78. wantTrackChanges: boolean
  79. showVisual: boolean
  80. newSourceEditor: boolean
  81. error_state: boolean
  82. }
  83. const EditorManagerContext = createContext<EditorManager | undefined>(undefined)
  84. export const EditorManagerProvider: FC = ({ children }) => {
  85. const { t } = useTranslation()
  86. const ide = useIdeContext()
  87. const { projectId } = useIdeReactContext()
  88. const { reportError, eventEmitter, eventLog } = useIdeReactContext()
  89. const { setOutOfSync } = useEditorContext()
  90. const { socket, disconnect, connectionState } = useConnectionContext()
  91. const { view, setView } = useLayoutContext()
  92. const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
  93. useModalsContext()
  94. const [showSymbolPalette, setShowSymbolPalette] = useScopeValue<boolean>(
  95. 'editor.showSymbolPalette'
  96. )
  97. const [showVisual] = useScopeValue<boolean>('editor.showVisual')
  98. const [currentDocument, setCurrentDocument] =
  99. useScopeValue<DocumentContainer>('editor.sharejs_doc')
  100. const [openDocId, setOpenDocId] = useScopeValue<DocId | null>(
  101. 'editor.open_doc_id'
  102. )
  103. const [, setOpenDocName] = useScopeValue<string | null>(
  104. 'editor.open_doc_name'
  105. )
  106. const [, setOpening] = useScopeValue<boolean>('editor.opening')
  107. const [, setIsInErrorState] = useScopeValue<boolean>('editor.error_state')
  108. const [, setTrackChanges] = useScopeValue<boolean>('editor.trackChanges')
  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. () =>
  164. new OpenDocuments(
  165. socket,
  166. globalEditorWatchdogManager,
  167. eventEmitter,
  168. eventLog
  169. )
  170. )
  171. const openDocIdStorageKey = `doc.open_id.${projectId}`
  172. // Persist the open document ID to local storage
  173. useEffect(() => {
  174. if (openDocId) {
  175. customLocalStorage.setItem(openDocIdStorageKey, openDocId)
  176. }
  177. }, [openDocId, openDocIdStorageKey])
  178. const editorOpenDocEpochRef = useRef(0)
  179. // TODO: This looks dodgy because it wraps a state setter and is itself
  180. // stored in React state in the scope store. The problem is that it needs to
  181. // be exposed via the scope store because some components access it that way;
  182. // it would be better to simply access it from a context, but the current
  183. // implementation in EditorManager interacts with Angular scope to update
  184. // the layout. Once Angular is gone, this can become a context method.
  185. useEffect(() => {
  186. ide.scopeStore.set('editor.toggleSymbolPalette', () => {
  187. setShowSymbolPalette(show => {
  188. const newValue = !show
  189. sendMB(newValue ? 'symbol-palette-show' : 'symbol-palette-hide')
  190. return newValue
  191. })
  192. })
  193. }, [ide.scopeStore, setShowSymbolPalette])
  194. const getEditorType = useCallback((): EditorType | null => {
  195. if (!currentDocument) {
  196. return null
  197. }
  198. return showVisual ? 'cm6-rich-text' : 'cm6'
  199. }, [currentDocument, showVisual])
  200. const getCurrentDocValue = useCallback(() => {
  201. return currentDocument?.getSnapshot() ?? null
  202. }, [currentDocument])
  203. const getCurrentDocId = useCallback(() => openDocId, [openDocId])
  204. const startIgnoringExternalUpdates = useCallback(
  205. () => setIgnoringExternalUpdates(true),
  206. []
  207. )
  208. const stopIgnoringExternalUpdates = useCallback(
  209. () => setIgnoringExternalUpdates(false),
  210. []
  211. )
  212. const jumpToLine = useCallback(
  213. (options: GotoLineOptions) => {
  214. goToLineEmitter(
  215. options.gotoLine,
  216. options.gotoColumn ?? 0,
  217. options.syncToPdf ?? false
  218. )
  219. },
  220. [goToLineEmitter]
  221. )
  222. const unbindFromDocumentEvents = (document: DocumentContainer) => {
  223. document.off()
  224. }
  225. const attachErrorHandlerToDocument = useCallback(
  226. (doc: Doc, document: DocumentContainer) => {
  227. document.on(
  228. 'error',
  229. (
  230. error: Error | string,
  231. meta?: Record<string, any>,
  232. editorContent?: string
  233. ) => {
  234. setDocError({ doc, document, error, meta, editorContent })
  235. }
  236. )
  237. },
  238. []
  239. )
  240. const bindToDocumentEvents = useCallback(
  241. (doc: Doc, document: DocumentContainer) => {
  242. attachErrorHandlerToDocument(doc, document)
  243. document.on('externalUpdate', (update: Update) => {
  244. if (ignoringExternalUpdates) {
  245. return
  246. }
  247. if (
  248. update.meta.type === 'external' &&
  249. update.meta.source === 'git-bridge'
  250. ) {
  251. return
  252. }
  253. showGenericMessageModal(
  254. t('document_updated_externally'),
  255. t('document_updated_externally_detail')
  256. )
  257. })
  258. },
  259. [
  260. attachErrorHandlerToDocument,
  261. ignoringExternalUpdates,
  262. showGenericMessageModal,
  263. t,
  264. ]
  265. )
  266. const syncTimeoutRef = useRef<number | null>(null)
  267. const syncTrackChangesState = useCallback(
  268. (doc: DocumentContainer) => {
  269. if (!doc) {
  270. return
  271. }
  272. if (syncTimeoutRef.current) {
  273. window.clearTimeout(syncTimeoutRef.current)
  274. syncTimeoutRef.current = null
  275. }
  276. const want = wantTrackChangesRef.current
  277. const have = doc.getTrackingChanges()
  278. if (want === have) {
  279. setTrackChanges(want)
  280. return
  281. }
  282. const tryToggle = () => {
  283. const saved = doc.getInflightOp() == null && doc.getPendingOp() == null
  284. if (saved) {
  285. doc.setTrackingChanges(want)
  286. setTrackChanges(want)
  287. } else {
  288. syncTimeoutRef.current = window.setTimeout(tryToggle, 100)
  289. }
  290. }
  291. tryToggle()
  292. },
  293. [setTrackChanges]
  294. )
  295. const doOpenNewDocument = useCallback(
  296. (doc: Doc) =>
  297. new Promise<DocumentContainer>((resolve, reject) => {
  298. debugConsole.log('[doOpenNewDocument] Opening...')
  299. const newDocument = openDocs.getDocument(doc._id)
  300. if (!newDocument) {
  301. debugConsole.error(`No open document with ID '${doc._id}' found`)
  302. reject(new Error('no open document found'))
  303. return
  304. }
  305. const preJoinEpoch = ++editorOpenDocEpochRef.current
  306. newDocument.join(error => {
  307. if (error) {
  308. debugConsole.log(
  309. `[doOpenNewDocument] error joining doc ${doc._id}`,
  310. error
  311. )
  312. reject(error)
  313. return
  314. }
  315. if (editorOpenDocEpochRef.current !== preJoinEpoch) {
  316. debugConsole.log(
  317. `[doOpenNewDocument] editorOpenDocEpoch mismatch ${editorOpenDocEpochRef.current} vs ${preJoinEpoch}`
  318. )
  319. newDocument.leaveAndCleanUp()
  320. reject(new Error('another document was loaded'))
  321. }
  322. bindToDocumentEvents(doc, newDocument)
  323. resolve(newDocument)
  324. })
  325. }),
  326. [bindToDocumentEvents, openDocs]
  327. )
  328. const openNewDocument = useCallback(
  329. async (doc: Doc): Promise<DocumentContainer> => {
  330. // Leave the current document
  331. // - when we are opening a different new one, to avoid race conditions
  332. // between leaving and joining the same document
  333. // - when the current one has pending ops that need flushing, to avoid
  334. // race conditions from cleanup
  335. const currentDocId = currentDocument?.doc_id
  336. const hasBufferedOps = currentDocument?.hasBufferedOps()
  337. const changingDoc = currentDocument && currentDocId !== doc._id
  338. if (changingDoc || hasBufferedOps) {
  339. debugConsole.log('[openNewDocument] Leaving existing open doc...')
  340. // Do not trigger any UI changes from remote operations
  341. unbindFromDocumentEvents(currentDocument)
  342. // Keep listening for out-of-sync and similar errors.
  343. attachErrorHandlerToDocument(doc, currentDocument)
  344. // Teardown the Document -> ShareJsDoc -> sharejs doc
  345. // By the time this completes, the Document instance is no longer
  346. // registered in OpenDocuments and doOpenNewDocument can start
  347. // from scratch -- read: no corrupted internal state.
  348. const preLeaveEpoch = ++editorOpenDocEpochRef.current
  349. try {
  350. await currentDocument.leaveAndCleanUpPromise()
  351. } catch (error) {
  352. debugConsole.log(
  353. `[openNewDocument] error leaving doc ${currentDocId}`,
  354. error
  355. )
  356. throw error
  357. }
  358. if (editorOpenDocEpochRef.current !== preLeaveEpoch) {
  359. debugConsole.log(
  360. `[openNewDocument] editorOpenDocEpoch mismatch ${editorOpenDocEpochRef.current} vs ${preLeaveEpoch}`
  361. )
  362. throw new Error('another document was loaded')
  363. }
  364. }
  365. return doOpenNewDocument(doc)
  366. },
  367. [attachErrorHandlerToDocument, doOpenNewDocument, currentDocument]
  368. )
  369. const openDocIdRef = useRef(openDocId)
  370. useEffect(() => {
  371. openDocIdRef.current = openDocId
  372. }, [openDocId])
  373. const openDoc = useCallback(
  374. async (doc: Doc, options: OpenDocOptions = {}) => {
  375. debugConsole.log(`[openDoc] Opening ${doc._id}`)
  376. if (view === 'editor') {
  377. // store position of previous doc before switching docs
  378. eventEmitter.emit('store-doc-position')
  379. }
  380. setView('editor')
  381. const done = (isNewDoc: boolean) => {
  382. window.dispatchEvent(
  383. new CustomEvent('doc:after-opened', {
  384. detail: { isNewDoc, docId: 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.gotoOffset)
  400. })
  401. }
  402. }
  403. // If we already have the document open, or are opening the document, we can return at this point.
  404. // Note: only use forceReopen:true to override this when the document is
  405. // out of sync and needs to be reloaded from the server.
  406. if (doc._id === openDocIdRef.current && !options.forceReopen) {
  407. done(false)
  408. return
  409. }
  410. // We're now either opening a new document or reloading a broken one.
  411. openDocIdRef.current = doc._id as DocId
  412. setOpenDocId(doc._id as DocId)
  413. setOpenDocName(doc.name)
  414. setOpening(true)
  415. try {
  416. const document = await openNewDocument(doc)
  417. syncTrackChangesState(document)
  418. eventEmitter.emit('doc:opened')
  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. }
  435. },
  436. [
  437. eventEmitter,
  438. jumpToLine,
  439. openNewDocument,
  440. setCurrentDocument,
  441. setOpenDocId,
  442. setOpenDocName,
  443. setOpening,
  444. setView,
  445. showGenericMessageModal,
  446. syncTrackChangesState,
  447. t,
  448. view,
  449. ]
  450. )
  451. const openDocWithId = useCallback(
  452. (docId: string, options: OpenDocOptions = {}) => {
  453. const doc = findDocEntityById(fileTreeData, docId)
  454. if (!doc) {
  455. return
  456. }
  457. openDoc(doc, options)
  458. },
  459. [fileTreeData, openDoc]
  460. )
  461. const openInitialDoc = useCallback(
  462. (fallbackDocId: string) => {
  463. const docId =
  464. customLocalStorage.getItem(openDocIdStorageKey) || fallbackDocId
  465. if (docId) {
  466. openDocWithId(docId)
  467. }
  468. },
  469. [openDocIdStorageKey, openDocWithId]
  470. )
  471. useEffect(() => {
  472. if (docError) {
  473. const { doc, document, error, meta } = docError
  474. let { editorContent } = docError
  475. const message = typeof error === 'string' ? error : error?.message ?? ''
  476. // Clear document error so that it's only handled once
  477. setDocError(null)
  478. if (message.includes('maxDocLength')) {
  479. openDoc(doc, { forceReopen: true })
  480. showGenericMessageModal(
  481. t('document_too_long'),
  482. t('document_too_long_detail')
  483. )
  484. setDocTooLongErrorShown(true)
  485. } else if (/too many comments or tracked changes/.test(message)) {
  486. showGenericMessageModal(
  487. t('too_many_comments_or_tracked_changes'),
  488. t('too_many_comments_or_tracked_changes_detail')
  489. )
  490. } else if (!docTooLongErrorShown) {
  491. // Do not allow this doc to open another error modal.
  492. document.off('error')
  493. // Preserve the sharejs contents before the teardown.
  494. // eslint-disable-next-line no-unused-vars
  495. editorContent =
  496. typeof editorContent === 'string'
  497. ? editorContent
  498. : document.doc?._doc.snapshot
  499. // Tear down the ShareJsDoc.
  500. if (document.doc) document.doc.clearInflightAndPendingOps()
  501. // Do not re-join after re-connecting.
  502. document.leaveAndCleanUp()
  503. disconnect()
  504. reportError(error, meta)
  505. // Tell the user about the error state.
  506. setIsInErrorState(true)
  507. // Ensure that the editor is locked
  508. setOutOfSync(true)
  509. // Display the "out of sync" modal
  510. showOutOfSyncModal(editorContent || '')
  511. // Do not forceReopen the document.
  512. return
  513. }
  514. const handleProjectJoined = () => {
  515. openDoc(doc, { forceReopen: true })
  516. }
  517. eventEmitter.once('project:joined', handleProjectJoined)
  518. return () => {
  519. eventEmitter.off('project:joined', handleProjectJoined)
  520. }
  521. }
  522. }, [
  523. disconnect,
  524. docError,
  525. docTooLongErrorShown,
  526. eventEmitter,
  527. openDoc,
  528. reportError,
  529. setIsInErrorState,
  530. showGenericMessageModal,
  531. showOutOfSyncModal,
  532. setOutOfSync,
  533. t,
  534. ])
  535. useEventListener('editor:insert-symbol', () => {
  536. sendMB('symbol-palette-insert')
  537. })
  538. useEventListener('flush-changes', () => {
  539. openDocs.flushAll()
  540. })
  541. useEventListener('blur', () => {
  542. openDocs.flushAll()
  543. })
  544. // Flush changes before disconnecting
  545. useEffect(() => {
  546. if (connectionState.forceDisconnected) {
  547. openDocs.flushAll()
  548. }
  549. }, [connectionState.forceDisconnected, openDocs])
  550. // Watch for changes in wantTrackChanges
  551. const previousWantTrackChangesRef = useRef(wantTrackChanges)
  552. useEffect(() => {
  553. if (wantTrackChanges !== previousWantTrackChangesRef.current) {
  554. previousWantTrackChangesRef.current = wantTrackChanges
  555. syncTrackChangesState(currentDocument)
  556. }
  557. }, [currentDocument, syncTrackChangesState, wantTrackChanges])
  558. const editorManager = useMemo(
  559. () => ({
  560. getEditorType,
  561. showSymbolPalette,
  562. currentDocument,
  563. currentDocumentId: openDocId,
  564. getCurrentDocValue,
  565. getCurrentDocId,
  566. startIgnoringExternalUpdates,
  567. stopIgnoringExternalUpdates,
  568. openDocId: openDocWithId,
  569. openDoc,
  570. openDocs,
  571. openInitialDoc,
  572. jumpToLine,
  573. wantTrackChanges,
  574. setWantTrackChanges,
  575. debugTimers,
  576. }),
  577. [
  578. getEditorType,
  579. showSymbolPalette,
  580. currentDocument,
  581. openDocId,
  582. getCurrentDocValue,
  583. getCurrentDocId,
  584. startIgnoringExternalUpdates,
  585. stopIgnoringExternalUpdates,
  586. openDocWithId,
  587. openDoc,
  588. openDocs,
  589. openInitialDoc,
  590. jumpToLine,
  591. wantTrackChanges,
  592. setWantTrackChanges,
  593. debugTimers,
  594. ]
  595. )
  596. // Expose editorManager via ide object because some React code relies on it,
  597. // for now
  598. ide.editorManager = editorManager
  599. return (
  600. <EditorManagerContext.Provider value={editorManager}>
  601. {children}
  602. </EditorManagerContext.Provider>
  603. )
  604. }
  605. export function useEditorManagerContext(): EditorManager {
  606. const context = useContext(EditorManagerContext)
  607. if (!context) {
  608. throw new Error(
  609. 'useEditorManagerContext is only available inside EditorManagerProvider'
  610. )
  611. }
  612. return context
  613. }