From f02bc7dc1b5739d9c2a7ca82a68275cdc5885b0d Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Fri, 12 Jan 2024 10:09:29 +0000 Subject: [PATCH] Update the CodeMirror language when the current file is renamed (#16342) GitOrigin-RevId: 8b51df0d1acfeeb8b0323cebf6de78572c8cb95c --- .../ide-react/context/outline-context.tsx | 1 - .../source-editor/extensions/doc-name.ts | 24 ++++++++++++ .../source-editor/extensions/index.ts | 7 +++- .../source-editor/extensions/language.ts | 25 +++++++++++-- .../source-editor/extensions/visual/visual.ts | 8 +--- .../hooks/use-codemirror-scope.ts | 37 ++++++++++++++----- .../utils/projection-state-field.ts | 29 +++++++++------ .../shared/context/file-tree-data-context.jsx | 23 ++++++++---- 8 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/extensions/doc-name.ts diff --git a/services/web/frontend/js/features/ide-react/context/outline-context.tsx b/services/web/frontend/js/features/ide-react/context/outline-context.tsx index 825765162a..791e368494 100644 --- a/services/web/frontend/js/features/ide-react/context/outline-context.tsx +++ b/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('editor.open_doc_name') const isTexFile = useMemo( () => (docName ? isValidTeXFile(docName) : false), diff --git a/services/web/frontend/js/features/source-editor/extensions/doc-name.ts b/services/web/frontend/js/features/source-editor/extensions/doc-name.ts new file mode 100644 index 0000000000..d6c43300bc --- /dev/null +++ b/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({ + 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() + +export const setDocName = (docName: string) => { + return { + effects: setDocNameEffect.of(docName), + } +} diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts index 92e2aa1fce..87b11ad263 100644 --- a/services/web/frontend/js/features/source-editor/extensions/index.ts +++ b/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): 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): 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), diff --git a/services/web/frontend/js/features/source-editor/extensions/language.ts b/services/web/frontend/js/features/source-editor/extensions/language.ts index a03593bdb7..3eabeb3540 100644 --- a/services/web/frontend/js/features/source-editor/extensions/language.ts +++ b/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({ }, }) +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() export const setMetadata = (values: Metadata): TransactionSpec => { diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index abcf1b2ce4..94769f2e86 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/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({ 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)), diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index 47752ea02b..5346553e6f 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/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 diff --git a/services/web/frontend/js/features/source-editor/utils/projection-state-field.ts b/services/web/frontend/js/features/source-editor/utils/projection-state-field.ts index bd908947b7..9e6ce6033c 100644 --- a/services/web/frontend/js/features/source-editor/utils/projection-state-field.ts +++ b/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( enterNode: EnterNodeFn ): StateField> { + const initialiseProjection = (state: EditorState) => + getUpdatedProjection( + state, + 0, + state.doc.length, + 0, + state.doc.length, + true, + enterNode + ) + const field = StateField.define>({ create(state) { - const projection = getUpdatedProjection( - 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 diff --git a/services/web/frontend/js/shared/context/file-tree-data-context.jsx b/services/web/frontend/js/shared/context/file-tree-data-context.jsx index b0dac3dae5..758ade8898 100644 --- a/services/web/frontend/js/shared/context/file-tree-data-context.jsx +++ b/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 })