visual-preview.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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. commands: [],
  56. fileTreeData,
  57. })
  58. useEffect(() => {
  59. if (!currentDocument) {
  60. return
  61. }
  62. const state = EditorState.create({
  63. doc: view.state.doc,
  64. extensions: [
  65. EditorView.lineWrapping,
  66. EditorState.readOnly.of(true),
  67. EditorView.editable.of(false),
  68. EditorState.phrases.of(phrases),
  69. docName('main.tex'),
  70. language('main.tex', metadataRef.current, { syntaxValidation: false }),
  71. theme({
  72. fontSize: 14,
  73. fontFamily: 'monaco',
  74. lineHeight: 'normal',
  75. activeOverallTheme: 'light',
  76. }),
  77. visualPreviewTheme,
  78. visualTheme,
  79. visualHighlightStyle,
  80. tableGeneratorTheme,
  81. mousedown,
  82. atomicDecorations({
  83. previewByPath: previewByPathRef.current,
  84. }),
  85. markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
  86. showContentWhenParsed,
  87. EditorView.contentAttributes.of({ 'aria-label': 'Visual preview' }),
  88. ],
  89. })
  90. const preview = new EditorView({
  91. state,
  92. dispatchTransactions(trs) {
  93. preview.update(trs)
  94. if (isMountedRef.current) {
  95. setPreviewState(preview.state)
  96. }
  97. },
  98. scrollTo: EditorView.scrollIntoView(state.selection.main, {
  99. y: 'center',
  100. }),
  101. })
  102. setEditorTheme('overleaf').then(spec => {
  103. preview.dispatch(spec)
  104. })
  105. containerRef.current?.replaceChildren(preview.dom)
  106. viewRef.current = preview
  107. view.dispatch({
  108. effects: StateEffect.appendConfig.of([
  109. EditorView.updateListener.of(update => {
  110. if (update.docChanged) {
  111. for (const tr of update.transactions) {
  112. preview.dispatch({
  113. changes: tr.changes,
  114. })
  115. }
  116. }
  117. if (update.selectionSet) {
  118. preview.dispatch({
  119. effects: EditorView.scrollIntoView(update.state.selection.main, {
  120. y: 'center',
  121. }),
  122. })
  123. }
  124. }),
  125. ]),
  126. })
  127. }, [phrases, view, currentDocument, isMountedRef])
  128. useEffect(() => {
  129. if (fileTreeData) {
  130. metadataRef.current.fileTreeData = fileTreeData
  131. window.setTimeout(() => {
  132. viewRef.current?.dispatch(setMetadata(metadataRef.current))
  133. })
  134. }
  135. }, [fileTreeData, view])
  136. useEffect(() => {
  137. return () => {
  138. viewRef.current?.destroy()
  139. }
  140. }, [view])
  141. if (!openDocName || !isValidTeXFile(openDocName)) {
  142. return null
  143. }
  144. return (
  145. <CodeMirrorStateContext.Provider value={previewState}>
  146. <CodeMirrorViewContext.Provider value={viewRef.current}>
  147. <div ref={containerRef} style={{ height: '100%' }} />
  148. </CodeMirrorViewContext.Provider>
  149. </CodeMirrorStateContext.Provider>
  150. )
  151. }