visual-preview.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { FC, useEffect, useRef, useState } from 'react'
  2. import {
  3. CodeMirrorStateContext,
  4. CodeMirrorViewContext,
  5. } from '@/features/source-editor/components/codemirror-context'
  6. import { EditorView } from '@codemirror/view'
  7. import { EditorState, StateEffect } from '@codemirror/state'
  8. import useIsMounted from '@/shared/hooks/use-is-mounted'
  9. import { docName } from '@/features/source-editor/extensions/doc-name'
  10. import {
  11. language,
  12. Metadata,
  13. setMetadata,
  14. } from '@/features/source-editor/extensions/language'
  15. import { showContentWhenParsed } from '@/features/source-editor/extensions/visual/visual'
  16. import { usePhrases } from '@/features/source-editor/hooks/use-phrases'
  17. import {
  18. setEditorTheme,
  19. theme,
  20. } from '@/features/source-editor/extensions/theme'
  21. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  22. import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
  23. import {
  24. visualHighlightStyle,
  25. visualTheme,
  26. } from '@/features/source-editor/extensions/visual/visual-theme'
  27. import { tableGeneratorTheme } from '@/features/source-editor/extensions/visual/table-generator'
  28. import { atomicDecorations } from '@/features/source-editor/extensions/visual/atomic-decorations'
  29. import { markDecorations } from '@/features/source-editor/extensions/visual/mark-decorations'
  30. import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
  31. import { isValidTeXFile } from '@/main/is-valid-tex-file'
  32. import { mousedown } from '@/features/source-editor/extensions/visual/selection'
  33. const visualPreviewTheme = EditorView.theme({
  34. '&.cm-editor': {
  35. background: '#fff',
  36. },
  37. '.ol-cm-preamble-wrapper, .ol-cm-end-document-widget': {
  38. visibility: 'hidden',
  39. },
  40. })
  41. export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => {
  42. const [previewState, setPreviewState] = useState<EditorState>()
  43. const { fileTreeData } = useFileTreeData()
  44. const { previewByPath } = useFileTreePathContext()
  45. const { currentDocument, openDocName } = useEditorOpenDocContext()
  46. const phrases = usePhrases()
  47. const isMountedRef = useIsMounted()
  48. const viewRef = useRef<EditorView | undefined>()
  49. const containerRef = useRef<HTMLDivElement>(null)
  50. const previewByPathRef = useRef(previewByPath)
  51. const metadataRef = useRef<Metadata>({
  52. labels: new Set(),
  53. packageNames: new Set(),
  54. referenceKeys: new Set(),
  55. searchLocalReferences() {
  56. return Promise.resolve({ hits: [] })
  57. },
  58. commands: [],
  59. fileTreeData,
  60. })
  61. useEffect(() => {
  62. if (!currentDocument) {
  63. return
  64. }
  65. const state = EditorState.create({
  66. doc: view.state.doc,
  67. extensions: [
  68. EditorView.lineWrapping,
  69. EditorState.readOnly.of(true),
  70. EditorView.editable.of(false),
  71. EditorState.phrases.of(phrases),
  72. docName('main.tex'),
  73. language('main.tex', metadataRef.current, { syntaxValidation: false }),
  74. theme({
  75. fontSize: 14,
  76. fontFamily: 'monaco',
  77. lineHeight: 'normal',
  78. activeOverallTheme: 'light',
  79. }),
  80. visualPreviewTheme,
  81. visualTheme,
  82. visualHighlightStyle,
  83. tableGeneratorTheme,
  84. mousedown,
  85. atomicDecorations({
  86. previewByPath: previewByPathRef.current,
  87. }),
  88. markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
  89. showContentWhenParsed,
  90. EditorView.contentAttributes.of({ 'aria-label': 'Visual preview' }),
  91. ],
  92. })
  93. const preview = new EditorView({
  94. state,
  95. dispatchTransactions(trs) {
  96. preview.update(trs)
  97. if (isMountedRef.current) {
  98. setPreviewState(preview.state)
  99. }
  100. },
  101. scrollTo: EditorView.scrollIntoView(state.selection.main, {
  102. y: 'center',
  103. }),
  104. })
  105. setEditorTheme('overleaf').then(spec => {
  106. preview.dispatch(spec)
  107. })
  108. containerRef.current?.replaceChildren(preview.dom)
  109. viewRef.current = preview
  110. view.dispatch({
  111. effects: StateEffect.appendConfig.of([
  112. EditorView.updateListener.of(update => {
  113. if (update.docChanged) {
  114. for (const tr of update.transactions) {
  115. preview.dispatch({
  116. changes: tr.changes,
  117. })
  118. }
  119. }
  120. if (update.selectionSet) {
  121. preview.dispatch({
  122. effects: EditorView.scrollIntoView(update.state.selection.main, {
  123. y: 'center',
  124. }),
  125. })
  126. }
  127. }),
  128. ]),
  129. })
  130. }, [phrases, view, currentDocument, isMountedRef])
  131. useEffect(() => {
  132. if (fileTreeData) {
  133. metadataRef.current.fileTreeData = fileTreeData
  134. window.setTimeout(() => {
  135. viewRef.current?.dispatch(setMetadata(metadataRef.current))
  136. })
  137. }
  138. }, [fileTreeData, view])
  139. useEffect(() => {
  140. return () => {
  141. viewRef.current?.destroy()
  142. }
  143. }, [view])
  144. if (!openDocName || !isValidTeXFile(openDocName)) {
  145. return null
  146. }
  147. return (
  148. <CodeMirrorStateContext.Provider value={previewState}>
  149. <CodeMirrorViewContext.Provider value={viewRef.current}>
  150. <div ref={containerRef} style={{ height: '100%' }} />
  151. </CodeMirrorViewContext.Provider>
  152. </CodeMirrorStateContext.Provider>
  153. )
  154. }