overleaf-cep/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx
Mathias Jakobsen 39b4581e1d Merge pull request #26735 from overleaf/mj-system-theme
[web] Add new system theme to the editor

GitOrigin-RevId: b65083c5e96abc493556e901c861689cb7e3bbf7
2025-07-14 08:05:37 +00:00

165 lines
5.1 KiB
TypeScript

import { FC, useEffect, useRef, useState } from 'react'
import {
CodeMirrorStateContext,
CodeMirrorViewContext,
} from '@/features/source-editor/components/codemirror-context'
import { EditorView } from '@codemirror/view'
import { EditorState, StateEffect } from '@codemirror/state'
import useIsMounted from '@/shared/hooks/use-is-mounted'
import { docName } from '@/features/source-editor/extensions/doc-name'
import {
language,
Metadata,
setMetadata,
} from '@/features/source-editor/extensions/language'
import { showContentWhenParsed } from '@/features/source-editor/extensions/visual/visual'
import { usePhrases } from '@/features/source-editor/hooks/use-phrases'
import {
setEditorTheme,
theme,
} from '@/features/source-editor/extensions/theme'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import {
visualHighlightStyle,
visualTheme,
} from '@/features/source-editor/extensions/visual/visual-theme'
import { tableGeneratorTheme } from '@/features/source-editor/extensions/visual/table-generator'
import { atomicDecorations } from '@/features/source-editor/extensions/visual/atomic-decorations'
import { markDecorations } from '@/features/source-editor/extensions/visual/mark-decorations'
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import { isValidTeXFile } from '@/main/is-valid-tex-file'
import { mousedown } from '@/features/source-editor/extensions/visual/selection'
export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => {
const [previewState, setPreviewState] = useState<EditorState>()
const { fileTreeData } = useFileTreeData()
const { previewByPath } = useFileTreePathContext()
const { currentDocument, openDocName } = useEditorOpenDocContext()
const phrases = usePhrases()
const isMountedRef = useIsMounted()
const viewRef = useRef<EditorView | undefined>()
const containerRef = useRef<HTMLDivElement>(null)
const previewByPathRef = useRef(previewByPath)
const metadataRef = useRef<Metadata>({
labels: new Set(),
packageNames: new Set(),
referenceKeys: new Set(),
commands: [],
fileTreeData,
})
useEffect(() => {
if (!currentDocument) {
return
}
const state = EditorState.create({
doc: view.state.doc,
extensions: [
EditorView.lineWrapping,
EditorState.readOnly.of(true),
EditorView.editable.of(false),
EditorState.phrases.of(phrases),
docName('main.tex'),
language('main.tex', metadataRef.current, { syntaxValidation: false }),
theme({
fontSize: 14,
fontFamily: 'monaco',
lineHeight: 'normal',
activeOverallTheme: 'light',
}),
EditorView.theme({
'&.cm-editor': {
background: '#fff',
},
'.ol-cm-preamble-wrapper, .ol-cm-end-document-widget': {
visibility: 'hidden',
},
}),
visualTheme,
visualHighlightStyle,
tableGeneratorTheme,
mousedown,
atomicDecorations({
previewByPath: previewByPathRef.current,
}),
markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
showContentWhenParsed,
EditorView.contentAttributes.of({ 'aria-label': 'Visual preview' }),
],
})
const preview = new EditorView({
state,
dispatchTransactions(trs) {
preview.update(trs)
if (isMountedRef.current) {
setPreviewState(preview.state)
}
},
scrollTo: EditorView.scrollIntoView(state.selection.main, {
y: 'center',
}),
})
setEditorTheme('overleaf').then(spec => {
preview.dispatch(spec)
})
containerRef.current?.replaceChildren(preview.dom)
viewRef.current = preview
view.dispatch({
effects: StateEffect.appendConfig.of([
EditorView.updateListener.of(update => {
if (update.docChanged) {
for (const tr of update.transactions) {
preview.dispatch({
changes: tr.changes,
})
}
}
if (update.selectionSet) {
preview.dispatch({
effects: EditorView.scrollIntoView(update.state.selection.main, {
y: 'center',
}),
})
}
}),
]),
})
}, [phrases, view, currentDocument, isMountedRef])
useEffect(() => {
if (fileTreeData) {
metadataRef.current.fileTreeData = fileTreeData
window.setTimeout(() => {
viewRef.current?.dispatch(setMetadata(metadataRef.current))
})
}
}, [fileTreeData, view])
useEffect(() => {
return () => {
viewRef.current?.destroy()
}
}, [view])
if (!openDocName || !isValidTeXFile(openDocName)) {
return null
}
return (
<CodeMirrorStateContext.Provider value={previewState}>
<CodeMirrorViewContext.Provider value={viewRef.current}>
<div ref={containerRef} style={{ height: '100%' }} />
</CodeMirrorViewContext.Provider>
</CodeMirrorStateContext.Provider>
)
}