Skip to content

Commit 2fbf113

Browse files
authored
Merge pull request #302 from eccenca/fix/codeMirror-CMEM-6728
Fix instable CodeMirror component
2 parents 2571510 + b4ca4ba commit 2fbf113

4 files changed

Lines changed: 125 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ This is a major release, and it might be not compatible with your current usage
1414
- `<CodeAutocompleteField />:
1515
- outerDivAttributes parameter: Allows to set parameter of the container div element of the code complete field.
1616

17+
### Fixed
18+
19+
- <CodeMirror />:
20+
- Editor is re-created after certain property changes and is reset, i.e. loses it current state.
21+
- <CodeAutocompleteField />:
22+
- Read-only mode does not work correctly. It is still possible to change the value via pressing Enter (in multiline mode) or clicking the clear button.
23+
24+
1725
## [24.3.0] - 2025-06-05
1826

1927
### Added

src/components/AutoSuggestion/AutoSuggestion.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ const AutoSuggestion = ({
676676
showScrollBar,
677677
multiline,
678678
handleInputMouseDown,
679+
readOnly
679680
]);
680681

681682
const hasError = !!value.current && !pathIsValid && !pathValidationPending;
@@ -720,6 +721,7 @@ const AutoSuggestion = ({
720721
data-test-id={"value-path-clear-btn"}
721722
name="operation-clear"
722723
text={clearIconText}
724+
disabled={readOnly}
723725
onClick={handleInputEditorClear}
724726
/>
725727
</span>

src/extensions/codemirror/CodeMirror.tsx

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useMemo, useRef } from "react";
22
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
33
import { foldKeymap } from "@codemirror/language";
4-
import { EditorState, Extension } from "@codemirror/state";
4+
import { EditorState, Extension, Compartment } from "@codemirror/state";
55
import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
66
import { minimalSetup } from "codemirror";
77

@@ -30,7 +30,7 @@ import {
3030
adaptedHighlightSpecialChars,
3131
adaptedLineNumbers,
3232
adaptedLintGutter,
33-
adaptedPlaceholder,
33+
adaptedPlaceholder, compartment,
3434
} from "./tests/codemirrorTestHelper";
3535
import { ExtensionCreator } from "./types";
3636

@@ -221,7 +221,24 @@ export const CodeEditor = ({
221221
}: CodeEditorProps) => {
222222
const parent = useRef<any>(undefined);
223223
const [view, setView] = React.useState<EditorView | undefined>();
224+
const currentView = React.useRef<EditorView>()
225+
currentView.current = view
226+
const currentReadOnly = React.useRef(readOnly)
227+
currentReadOnly.current = readOnly
224228
const [showPreview, setShowPreview] = React.useState<boolean>(false);
229+
// CodeMirror Compartments in order to allow for re-configuration after initialization
230+
const readOnlyCompartment = React.useRef<Compartment>(compartment())
231+
const wrapLinesCompartment = React.useRef<Compartment>(compartment())
232+
const preventLineNumbersCompartment = React.useRef<Compartment>(compartment())
233+
const shouldHaveMinimalSetupCompartment = React.useRef<Compartment>(compartment())
234+
const placeholderCompartment = React.useRef<Compartment>(compartment())
235+
const modeCompartment = React.useRef<Compartment>(compartment())
236+
const keyMapConfigsCompartment = React.useRef<Compartment>(compartment())
237+
const tabIntentSizeCompartment = React.useRef<Compartment>(compartment())
238+
const disabledCompartment = React.useRef<Compartment>(compartment())
239+
const supportCodeFoldingCompartment = React.useRef<Compartment>(compartment())
240+
const useLintingCompartment = React.useRef<Compartment>(compartment())
241+
const shouldHighlightActiveLineCompartment = React.useRef<Compartment>(compartment())
225242

226243
const linters = useMemo(() => {
227244
if (!mode) {
@@ -240,7 +257,7 @@ export const CodeEditor = ({
240257

241258
const onKeyDownHandler = (event: KeyboardEvent, view: EditorView) => {
242259
if (onKeyDown && !onKeyDown(event)) {
243-
if (event.key === "Enter") {
260+
if (event.key === "Enter" && !currentReadOnly.current) {
244261
const cursor = view.state.selection.main.head;
245262
const cursorLine = view.state.doc.lineAt(cursor).number;
246263
const offsetFromFirstLine = view.state.doc.line(cursorLine).to;
@@ -265,14 +282,17 @@ export const CodeEditor = ({
265282
return false;
266283
};
267284

268-
React.useEffect(() => {
285+
const createKeyMapConfigs = () => {
269286
const tabIndent =
270287
!!(tabIntentStyle === "tab" && mode && !(tabForceSpaceForModes ?? []).includes(mode)) || enableTab;
271-
const keyMapConfigs = [
288+
return [
272289
defaultKeymap as KeyBinding,
273290
...addToKeyMapConfigFor(supportCodeFolding, foldKeymap),
274291
...addToKeyMapConfigFor(tabIndent, indentWithTab),
275292
];
293+
}
294+
295+
React.useEffect(() => {
276296
const domEventHandlers = {
277297
...addHandlersFor(!!onScroll, "scroll", onScroll),
278298
...addHandlersFor(
@@ -286,13 +306,13 @@ export const CodeEditor = ({
286306
} as DOMEventHandlers<any>;
287307
const extensions = [
288308
markField,
289-
adaptedPlaceholder(placeholder),
309+
placeholderCompartment.current.of(adaptedPlaceholder(placeholder)),
290310
adaptedHighlightSpecialChars(),
291-
useCodeMirrorModeExtension(mode),
292-
keymap?.of(keyMapConfigs),
293-
EditorState?.tabSize.of(tabIntentSize),
294-
EditorState?.readOnly.of(readOnly),
295-
EditorView?.editable.of(!disabled),
311+
modeCompartment.current.of(useCodeMirrorModeExtension(mode)),
312+
keyMapConfigsCompartment.current.of(keymap?.of(createKeyMapConfigs())),
313+
tabIntentSizeCompartment.current.of(EditorState?.tabSize.of(tabIntentSize)),
314+
readOnlyCompartment.current.of(EditorState?.readOnly.of(readOnly)),
315+
disabledCompartment.current.of(EditorView?.editable.of(!disabled)),
296316
AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
297317
EditorView?.updateListener.of((v: ViewUpdate) => {
298318
if (disabled) return;
@@ -328,12 +348,12 @@ export const CodeEditor = ({
328348
}
329349
}
330350
}),
331-
addExtensionsFor(shouldHaveMinimalSetup, minimalSetup),
332-
addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()),
333-
addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine()),
334-
addExtensionsFor(wrapLines, EditorView?.lineWrapping),
335-
addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding()),
336-
addExtensionsFor(useLinting, ...linters),
351+
shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
352+
preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
353+
shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
354+
wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
355+
supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
356+
useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
337357
additionalExtensions,
338358
];
339359

@@ -375,7 +395,64 @@ export const CodeEditor = ({
375395
setView(undefined);
376396
}
377397
};
378-
}, [parent.current, mode, preventLineNumbers, wrapLines]);
398+
}, [parent.current]);
399+
400+
// Updates an extension for a specific parameter that has changed after the initialization
401+
const updateExtension = (extension: Extension | undefined, parameterCompartment: Compartment): void => {
402+
if(extension) {
403+
currentView.current?.dispatch({
404+
effects: parameterCompartment.reconfigure(extension)
405+
})
406+
}
407+
}
408+
409+
React.useEffect(() => {
410+
updateExtension(EditorState?.readOnly.of(readOnly!), readOnlyCompartment.current)
411+
}, [readOnly])
412+
413+
React.useEffect(() => {
414+
updateExtension(adaptedPlaceholder(placeholder), placeholderCompartment.current)
415+
}, [placeholder])
416+
417+
React.useEffect(() => {
418+
updateExtension(useCodeMirrorModeExtension(mode), modeCompartment.current)
419+
}, [mode])
420+
421+
React.useEffect(() => {
422+
updateExtension(keymap?.of(createKeyMapConfigs()), keyMapConfigsCompartment.current)
423+
}, [supportCodeFolding, mode, tabIntentStyle, (tabForceSpaceForModes ?? []).join(", "), enableTab])
424+
425+
React.useEffect(() => {
426+
updateExtension(EditorState?.tabSize.of(tabIntentSize ?? 2), tabIntentSizeCompartment.current)
427+
}, [tabIntentSize])
428+
429+
React.useEffect(() => {
430+
updateExtension(EditorView?.editable.of(!disabled), disabledCompartment.current)
431+
}, [disabled])
432+
433+
React.useEffect(() => {
434+
updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
435+
}, [shouldHaveMinimalSetup])
436+
437+
React.useEffect(() => {
438+
updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
439+
}, [preventLineNumbers])
440+
441+
React.useEffect(() => {
442+
updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
443+
}, [shouldHighlightActiveLine])
444+
445+
React.useEffect(() => {
446+
updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
447+
}, [wrapLines])
448+
449+
React.useEffect(() => {
450+
updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
451+
}, [supportCodeFolding])
452+
453+
React.useEffect(() => {
454+
updateExtension(addExtensionsFor(useLinting ?? false, ...linters), useLintingCompartment.current)
455+
}, [mode, useLinting])
379456

380457
const hasToolbarSupport = mode && ModeToolbarSupport.indexOf(mode) > -1 && useToolbar;
381458

src/extensions/codemirror/tests/codemirrorTestHelper.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import { EditorView, placeholder, highlightSpecialChars, lineNumbers, highlightActiveLine } from "@codemirror/view";
1111
import { syntaxHighlighting, foldGutter, codeFolding } from "@codemirror/language";
12-
import { Extension } from "@codemirror/state";
12+
import {Extension, Compartment, StateEffect, EditorState} from "@codemirror/state";
1313
import { lintGutter } from "@codemirror/lint";
1414

1515
/** placeholder extension, current error '_view.placeholder is not a function' */
@@ -34,6 +34,25 @@ export const AdaptedEditorView = isConstructor(EditorView)
3434
destroy() {}
3535
} as any);
3636

37+
/** Creates a new compartment or a mock of a compartment. */
38+
export const compartment = () => {
39+
if(isConstructor(Compartment)) {
40+
return new Compartment()
41+
} else {
42+
let extension: Extension | undefined = undefined
43+
return {
44+
of: (ext: Extension): Extension => {
45+
extension = ext
46+
return ext
47+
},
48+
reconfigure: (_content: Extension): StateEffect<unknown> => {
49+
return {} as StateEffect<any>
50+
},
51+
get: (_state: EditorState): Extension | undefined => extension
52+
}
53+
}
54+
}
55+
3756
const emptyExtension = (() => {}) as any;
3857
/** extension adding event handlers, current error '(view, domEventHandlers) is not a function' */
3958
export const AdaptedEditorViewDomEventHandlers =

0 commit comments

Comments
 (0)