Kaynağa Gözat

Add visual preview (#26947)

GitOrigin-RevId: f77b59219909971b11416f196783b3ab7198ed91
Alf Eaton 1 yıl önce
ebeveyn
işleme
d617fd0754

+ 1 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -334,6 +334,7 @@ const _ProjectController = {
 
     const splitTests = [
       'compile-log-events',
+      'visual-preview',
       'external-socket-heartbeat',
       'null-test-share-modal',
       'populate-clsi-cache',

+ 6 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-preview.tsx

@@ -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 />
 }
 

+ 6 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.tsx

@@ -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

+ 165 - 0
services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx

@@ -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>
+  )
+}

+ 3 - 1
services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx

@@ -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}

+ 1 - 1
services/web/frontend/js/features/source-editor/extensions/language.ts

@@ -20,7 +20,7 @@ type Options = {
   syntaxValidation: boolean
 }
 
-type Metadata = {
+export type Metadata = {
   labels: Set<string>
   packageNames: Set<string>
   commands: Command[]

+ 1 - 1
services/web/frontend/js/features/source-editor/extensions/visual/visual.ts

@@ -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 = () => {