mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-04 06:39:02 +02:00
Add visual preview (#26947)
GitOrigin-RevId: f77b59219909971b11416f196783b3ab7198ed91
This commit is contained in:
@@ -334,6 +334,7 @@ const _ProjectController = {
|
||||
|
||||
const splitTests = [
|
||||
'compile-log-events',
|
||||
'visual-preview',
|
||||
'external-socket-heartbeat',
|
||||
'null-test-share-modal',
|
||||
'populate-clsi-cache',
|
||||
|
||||
@@ -3,10 +3,16 @@ import { memo } from 'react'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import PdfPreviewErrorBoundaryFallback from './pdf-preview-error-boundary-fallback'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { VisualPreview } from './visual-preview'
|
||||
import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function PdfPreview() {
|
||||
const { detachRole } = useLayoutContext()
|
||||
const { view } = useEditorViewContext()
|
||||
const visualPreviewEnabled = useFeatureFlag('visual-preview')
|
||||
if (detachRole === 'detacher') return null
|
||||
if (visualPreviewEnabled && view) return <VisualPreview view={view} />
|
||||
return <PdfPreviewPane />
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { Spinner } from 'react-bootstrap'
|
||||
import { Placement } from 'react-bootstrap/types'
|
||||
import useSynctex from '../hooks/use-synctex'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
const GoToCodeButton = memo(function GoToCodeButton({
|
||||
syncToCode,
|
||||
@@ -135,6 +136,11 @@ function PdfSynctexControls() {
|
||||
syncToPdfInFlight,
|
||||
canSyncToPdf,
|
||||
} = useSynctex()
|
||||
const visualPreviewEnabled = useFeatureFlag('visual-preview')
|
||||
|
||||
if (visualPreviewEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!position) {
|
||||
return null
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
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',
|
||||
overallTheme: '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>
|
||||
)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-
|
||||
import Breadcrumbs from '@/features/ide-redesign/components/breadcrumbs'
|
||||
import classNames from 'classnames'
|
||||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export const CodeMirrorToolbar = () => {
|
||||
const view = useCodeMirrorViewContext()
|
||||
@@ -45,6 +46,7 @@ const Toolbar = memo(function Toolbar() {
|
||||
const {
|
||||
userSettings: { breadcrumbs },
|
||||
} = useUserSettingsContext()
|
||||
const visualPreviewEnabled = useFeatureFlag('visual-preview')
|
||||
|
||||
const [overflowed, setOverflowed] = useState(false)
|
||||
|
||||
@@ -157,7 +159,7 @@ const Toolbar = memo(function Toolbar() {
|
||||
className="ol-cm-toolbar toolbar-editor"
|
||||
ref={elementRef}
|
||||
>
|
||||
<EditorSwitch />
|
||||
{!visualPreviewEnabled && <EditorSwitch />}
|
||||
{showActions && (
|
||||
<ToolbarItems
|
||||
state={state}
|
||||
|
||||
@@ -20,7 +20,7 @@ type Options = {
|
||||
syntaxValidation: boolean
|
||||
}
|
||||
|
||||
type Metadata = {
|
||||
export type Metadata = {
|
||||
labels: Set<string>
|
||||
packageNames: Set<string>
|
||||
commands: Command[]
|
||||
|
||||
@@ -94,7 +94,7 @@ const parsedAttributesConf = new Compartment()
|
||||
* A view plugin which shows the editor content, makes it focusable,
|
||||
* and restores the scroll position, once the initial decorations have been applied.
|
||||
*/
|
||||
const showContentWhenParsed = [
|
||||
export const showContentWhenParsed = [
|
||||
parsedAttributesConf.of([EditorView.editable.of(false)]),
|
||||
ViewPlugin.define(view => {
|
||||
const showContent = () => {
|
||||
|
||||
Reference in New Issue
Block a user