visual-preview.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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. export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => {
  34. const [previewState, setPreviewState] = useState<EditorState>()
  35. const { fileTreeData } = useFileTreeData()
  36. const { previewByPath } = useFileTreePathContext()
  37. const { currentDocument, openDocName } = useEditorOpenDocContext()
  38. const phrases = usePhrases()
  39. const isMountedRef = useIsMounted()
  40. const viewRef = useRef<EditorView | undefined>()
  41. const containerRef = useRef<HTMLDivElement>(null)
  42. const previewByPathRef = useRef(previewByPath)
  43. const metadataRef = useRef<Metadata>({
  44. labels: new Set(),
  45. packageNames: new Set(),
  46. referenceKeys: new Set(),
  47. commands: [],
  48. fileTreeData,
  49. })
  50. useEffect(() => {
  51. if (!currentDocument) {
  52. return
  53. }
  54. const state = EditorState.create({
  55. doc: view.state.doc,
  56. extensions: [
  57. EditorView.lineWrapping,
  58. EditorState.readOnly.of(true),
  59. EditorView.editable.of(false),
  60. EditorState.phrases.of(phrases),
  61. docName('main.tex'),
  62. language('main.tex', metadataRef.current, { syntaxValidation: false }),
  63. theme({
  64. fontSize: 14,
  65. fontFamily: 'monaco',
  66. lineHeight: 'normal',
  67. activeOverallTheme: 'light',
  68. }),
  69. EditorView.theme({
  70. '&.cm-editor': {
  71. background: '#fff',
  72. },
  73. '.ol-cm-preamble-wrapper, .ol-cm-end-document-widget': {
  74. visibility: 'hidden',
  75. },
  76. }),
  77. visualTheme,
  78. visualHighlightStyle,
  79. tableGeneratorTheme,
  80. mousedown,
  81. atomicDecorations({
  82. previewByPath: previewByPathRef.current,
  83. }),
  84. markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
  85. showContentWhenParsed,
  86. EditorView.contentAttributes.of({ 'aria-label': 'Visual preview' }),
  87. ],
  88. })
  89. const preview = new EditorView({
  90. state,
  91. dispatchTransactions(trs) {
  92. preview.update(trs)
  93. if (isMountedRef.current) {
  94. setPreviewState(preview.state)
  95. }
  96. },
  97. scrollTo: EditorView.scrollIntoView(state.selection.main, {
  98. y: 'center',
  99. }),
  100. })
  101. setEditorTheme('overleaf').then(spec => {
  102. preview.dispatch(spec)
  103. })
  104. containerRef.current?.replaceChildren(preview.dom)
  105. viewRef.current = preview
  106. view.dispatch({
  107. effects: StateEffect.appendConfig.of([
  108. EditorView.updateListener.of(update => {
  109. if (update.docChanged) {
  110. for (const tr of update.transactions) {
  111. preview.dispatch({
  112. changes: tr.changes,
  113. })
  114. }
  115. }
  116. if (update.selectionSet) {
  117. preview.dispatch({
  118. effects: EditorView.scrollIntoView(update.state.selection.main, {
  119. y: 'center',
  120. }),
  121. })
  122. }
  123. }),
  124. ]),
  125. })
  126. }, [phrases, view, currentDocument, isMountedRef])
  127. useEffect(() => {
  128. if (fileTreeData) {
  129. metadataRef.current.fileTreeData = fileTreeData
  130. window.setTimeout(() => {
  131. viewRef.current?.dispatch(setMetadata(metadataRef.current))
  132. })
  133. }
  134. }, [fileTreeData, view])
  135. useEffect(() => {
  136. return () => {
  137. viewRef.current?.destroy()
  138. }
  139. }, [view])
  140. if (!openDocName || !isValidTeXFile(openDocName)) {
  141. return null
  142. }
  143. return (
  144. <CodeMirrorStateContext.Provider value={previewState}>
  145. <CodeMirrorViewContext.Provider value={viewRef.current}>
  146. <div ref={containerRef} style={{ height: '100%' }} />
  147. </CodeMirrorViewContext.Provider>
  148. </CodeMirrorStateContext.Provider>
  149. )
  150. }