Просмотр исходного кода

Update the CodeMirror language when the current file is renamed (#16342)

GitOrigin-RevId: 8b51df0d1acfeeb8b0323cebf6de78572c8cb95c
Alf Eaton 2 лет назад
Родитель
Сommit
4a7a24b44d

+ 0 - 1
services/web/frontend/js/features/ide-react/context/outline-context.tsx

@@ -114,7 +114,6 @@ export const OutlineProvider: FC = ({ children }) => {
     [flatOutline, currentlyHighlightedLine]
   )
 
-  // TODO: update when the file is renamed
   const [docName] = useScopeValue<string | null>('editor.open_doc_name')
   const isTexFile = useMemo(
     () => (docName ? isValidTeXFile(docName) : false),

+ 24 - 0
services/web/frontend/js/features/source-editor/extensions/doc-name.ts

@@ -0,0 +1,24 @@
+import { StateEffect, StateField } from '@codemirror/state'
+
+export const docName = (docName: string) =>
+  StateField.define<string>({
+    create() {
+      return docName
+    },
+    update(value, tr) {
+      for (const effect of tr.effects) {
+        if (effect.is(setDocNameEffect)) {
+          value = effect.value
+        }
+      }
+      return value
+    },
+  })
+
+export const setDocNameEffect = StateEffect.define<string>()
+
+export const setDocName = (docName: string) => {
+  return {
+    effects: setDocNameEffect.of(docName),
+  }
+}

+ 5 - 2
services/web/frontend/js/features/source-editor/extensions/index.ts

@@ -47,6 +47,7 @@ import { effectListeners } from './effect-listeners'
 import { highlightSpecialChars } from './highlight-special-chars'
 import { toolbarPanel } from './toolbar/toolbar-panel'
 import { geometryChangeEvent } from './geometry-change-event'
+import { docName } from '@/features/source-editor/extensions/doc-name'
 
 const moduleExtensions: Array<() => Extension> = importOverleafModules(
   'sourceEditorExtensions'
@@ -101,9 +102,11 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
   // precedence over language-specific keyboard shortcuts
   keybindings(),
 
+  docName(options.docName),
+
   // NOTE: `annotations` needs to be before `language`
   annotations(),
-  language(options.currentDoc, options.metadata, options.settings),
+  language(options.docName, options.metadata, options.settings),
   indentUnit.of('    '), // 4 spaces
   theme(options.theme),
   realtime(options.currentDoc, options.handleError),
@@ -121,7 +124,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
   // so the decorations are added in the correct order.
   emptyLineFiller(),
   trackChanges(options.currentDoc, options.changeManager),
-  visual(options.currentDoc, options.visual),
+  visual(options.visual),
   toolbarPanel(),
   verticalOverflow(),
   highlightActiveLine(options.visual.visual),

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

@@ -8,7 +8,6 @@ import { languages } from '../languages'
 import { ViewPlugin } from '@codemirror/view'
 import { indentUnit, LanguageDescription } from '@codemirror/language'
 import { Metadata } from '../../../../../types/metadata'
-import { CurrentDoc } from '../../../../../types/current-doc'
 import { updateHasEffect } from '../utils/effects'
 
 export const languageLoadedEffect = StateEffect.define()
@@ -35,18 +34,26 @@ export const metadataState = StateField.define<Metadata | undefined>({
   },
 })
 
+const languageCompartment = new Compartment()
+
 /**
  * The parser and support extensions for each supported language,
  * which are loaded dynamically as needed.
  */
 export const language = (
-  currentDoc: CurrentDoc,
+  docName: string,
   metadata: Metadata,
   { syntaxValidation }: Options
+) => languageCompartment.of(buildExtension(docName, metadata, syntaxValidation))
+
+const buildExtension = (
+  docName: string,
+  metadata: Metadata,
+  syntaxValidation: boolean
 ) => {
   const languageDescription = LanguageDescription.matchFilename(
     languages,
-    currentDoc.docName
+    docName
   )
 
   if (!languageDescription) {
@@ -88,6 +95,18 @@ export const language = (
   ]
 }
 
+export const setLanguage = (
+  docName: string,
+  metadata: Metadata,
+  syntaxValidation: boolean
+) => {
+  return {
+    effects: languageCompartment.reconfigure(
+      buildExtension(docName, metadata, syntaxValidation)
+    ),
+  }
+}
+
 export const setMetadataEffect = StateEffect.define<Metadata>()
 
 export const setMetadata = (values: Metadata): TransactionSpec => {

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

@@ -15,8 +15,6 @@ import { mousedown, mouseDownEffect } from './selection'
 import { forceParsing, syntaxTree } from '@codemirror/language'
 import { hasLanguageLoadedEffect } from '../language'
 import { restoreScrollPosition } from '../scroll-position'
-import { CurrentDoc } from '../../../../../../types/current-doc'
-import isValidTeXFile from '../../../../main/is-valid-tex-file'
 import { listItemMarker } from './list-item-marker'
 import { selectDecoratedArgument } from './select-decorated-argument'
 import { pasteHtml } from './paste-html'
@@ -51,11 +49,7 @@ const visualState = StateField.define<boolean>({
 const configureVisualExtensions = (options: Options) =>
   options.visual ? extension(options) : []
 
-export const visual = (currentDoc: CurrentDoc, options: Options): Extension => {
-  if (!isValidTeXFile(currentDoc.docName)) {
-    return []
-  }
-
+export const visual = (options: Options): Extension => {
   return [
     visualState.init(() => options.visual),
     visualConf.of(configureVisualExtensions(options)),

+ 28 - 9
services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts

@@ -21,7 +21,11 @@ import {
 } from '../extensions/annotations'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 import { setCursorHighlights } from '../extensions/cursor-highlights'
-import { setMetadata, setSyntaxValidation } from '../extensions/language'
+import {
+  setLanguage,
+  setMetadata,
+  setSyntaxValidation,
+} from '../extensions/language'
 import { useIdeContext } from '../../../shared/context/ide-context'
 import { restoreScrollPosition } from '../extensions/scroll-position'
 import { setEditable } from '../extensions/editable'
@@ -48,6 +52,8 @@ import { useErrorHandler } from 'react-error-boundary'
 import { setVisual } from '../extensions/visual/visual'
 import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
 import { useUserSettingsContext } from '@/shared/context/user-settings-context'
+import { setDocName } from '@/features/source-editor/extensions/doc-name'
+import isValidTexFile from '@/main/is-valid-tex-file'
 
 function useCodeMirrorScope(view: EditorView) {
   const ide = useIdeContext()
@@ -139,7 +145,6 @@ function useCodeMirrorScope(view: EditorView) {
 
   const currentDocRef = useRef({
     currentDoc,
-    docName,
     trackChanges,
     loadingThreads,
   })
@@ -150,11 +155,7 @@ function useCodeMirrorScope(view: EditorView) {
     }
   }, [view, currentDoc])
 
-  useEffect(() => {
-    if (docName) {
-      currentDocRef.current.docName = docName
-    }
-  }, [view, docName])
+  const docNameRef = useRef(docName)
 
   useEffect(() => {
     currentDocRef.current.loadingThreads = loadingThreads
@@ -258,6 +259,7 @@ function useCodeMirrorScope(view: EditorView) {
             ...currentDocRef.current,
             currentDoc,
           },
+          docName: docNameRef.current,
           theme: themeRef.current,
           metadata: metadataRef.current,
           settings: settingsRef.current,
@@ -298,14 +300,31 @@ function useCodeMirrorScope(view: EditorView) {
   }, [view, currentDoc, handleError])
 
   useEffect(() => {
-    visualRef.current.visual = visual
+    if (docName) {
+      docNameRef.current = docName
+
+      view.dispatch(
+        setDocName(docNameRef.current),
+        setLanguage(
+          docNameRef.current,
+          metadataRef.current,
+          settingsRef.current.syntaxValidation
+        )
+      )
+    }
+  }, [view, docName])
+
+  const showVisual = visual && isValidTexFile(docName)
+
+  useEffect(() => {
+    visualRef.current.visual = showVisual
     view.dispatch(setVisual(visualRef.current))
     view.dispatch({
       effects: EditorView.scrollIntoView(view.state.selection.main.head),
     })
     // clear performance measures and marks when switching between Source and Rich Text
     window.dispatchEvent(new Event('editor:visual-switch'))
-  }, [view, visual])
+  }, [view, showVisual])
 
   useEffect(() => {
     visualRef.current.previewByPath = previewByPath

+ 18 - 11
services/web/frontend/js/features/source-editor/utils/projection-state-field.ts

@@ -1,4 +1,4 @@
-import { ChangeSet, StateField } from '@codemirror/state'
+import { ChangeSet, EditorState, StateField } from '@codemirror/state'
 import {
   ProjectionItem,
   ProjectionResult,
@@ -6,6 +6,7 @@ import {
   EnterNodeFn,
   ProjectionStatus,
 } from './tree-operations/projection'
+import { languageLoadedEffect } from '@/features/source-editor/extensions/language'
 
 export function mergeChangeRanges(changes: ChangeSet) {
   let fromA = Number.MAX_VALUE
@@ -33,20 +34,26 @@ export function mergeChangeRanges(changes: ChangeSet) {
 export function makeProjectionStateField<T extends ProjectionItem>(
   enterNode: EnterNodeFn<T>
 ): StateField<ProjectionResult<T>> {
+  const initialiseProjection = (state: EditorState) =>
+    getUpdatedProjection(
+      state,
+      0,
+      state.doc.length,
+      0,
+      state.doc.length,
+      true,
+      enterNode
+    )
+
   const field = StateField.define<ProjectionResult<T>>({
     create(state) {
-      const projection = getUpdatedProjection<T>(
-        state,
-        0,
-        state.doc.length,
-        0,
-        state.doc.length,
-        true,
-        enterNode
-      )
-      return projection
+      return initialiseProjection(state)
     },
     update(currentProjection, transaction) {
+      if (transaction.effects.some(effect => effect.is(languageLoadedEffect))) {
+        return initialiseProjection(transaction.state)
+      }
+
       if (
         transaction.docChanged ||
         currentProjection.status !== ProjectionStatus.Complete

+ 16 - 7
services/web/frontend/js/shared/context/file-tree-data-context.jsx

@@ -17,6 +17,7 @@ import {
 import { countFiles } from '../../features/file-tree/util/count-in-tree'
 import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect'
 import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
+import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
 
 const FileTreeDataContext = createContext()
 
@@ -137,6 +138,8 @@ export function useFileTreeData(propTypes) {
 
 export function FileTreeDataProvider({ children }) {
   const [project] = useScopeValue('project')
+  const [openDocId] = useScopeValue('editor.open_doc_id')
+  const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name')
 
   const { rootFolder } = project || {}
 
@@ -187,13 +190,19 @@ export function FileTreeDataProvider({ children }) {
     })
   }, [])
 
-  const dispatchRename = useCallback((id, newName) => {
-    dispatch({
-      type: ACTION_TYPES.RENAME,
-      newName,
-      id,
-    })
-  }, [])
+  const dispatchRename = useCallback(
+    (id, newName) => {
+      dispatch({
+        type: ACTION_TYPES.RENAME,
+        newName,
+        id,
+      })
+      if (id === openDocId) {
+        setOpenDocName(newName)
+      }
+    },
+    [openDocId, setOpenDocName]
+  )
 
   const dispatchDelete = useCallback(id => {
     dispatch({ type: ACTION_TYPES.DELETE, id })