feat(custom-widgets): improve authoring workbench (#6689)
* feat(custom-widgets): improve authoring workbench * fix(custom-widgets): address review and CI * refactor(custom-widgets): satisfy architecture checks * fix(custom-widgets): isolate workbench typing updates * fix(custom-widgets): harden workbench state
This commit is contained in:
1 parent
8b6387f321
commit
fcca57ddf7
47 files changed
+3602
-852
No files matched your search
@@ -63,6 +63,7 @@ export function CodeEditorToolbar({
|
||||
leftSection={<IconArrowBackUp size={14} />}
|
||||
onClick={onUndo}
|
||||
disabled={!editorCreated || historyDepth.undo === 0}
|
||||
aria-keyshortcuts="Control+Z Meta+Z"
|
||||
>
|
||||
{props.messages.undo}
|
||||
</Button>
|
||||
@@ -73,6 +74,7 @@ export function CodeEditorToolbar({
|
||||
leftSection={<IconArrowForwardUp size={14} />}
|
||||
onClick={onRedo}
|
||||
disabled={!editorCreated || historyDepth.redo === 0}
|
||||
aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z"
|
||||
>
|
||||
{props.messages.redo}
|
||||
</Button>
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
.footer {
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
}
|
||||
.accessibleMuted {
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-4));
|
||||
}
|
||||
.accessibleMuted[data-invalid="true"] {
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-4));
|
||||
}
|
||||
.viewport {
|
||||
max-height: min(62vh, 680px);
|
||||
overflow: auto;
|
||||
@@ -45,6 +51,9 @@
|
||||
background: var(--mantine-color-default-hover);
|
||||
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
}
|
||||
.viewport :global(.cm-editor .cm-lineNumbers .cm-gutterElement) {
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-4));
|
||||
}
|
||||
.diagnosticButton {
|
||||
min-height: 44px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { indentSelection, redo, redoDepth, undo, undoDepth } from "@codemirror/commands";
|
||||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import type { EditorView as EditorViewType, ViewUpdate } from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { lazy, Suspense, useCallback, useEffect, useId, useMemo, useState } from "react";
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Badge, Collapse, Group, Input, Loader, Tabs, Text, useComputedColorScheme } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
@@ -24,11 +24,11 @@ export type {
|
||||
} from "./code-editor-types";
|
||||
|
||||
const EMPTY_DIAGNOSTICS: NonNullable<CustomWidgetCodeEditorProps["diagnostics"]> = [];
|
||||
const EMPTY_COMPLETIONS: NonNullable<CustomWidgetCodeEditorProps["completions"]> = [];
|
||||
const LazyCodeMirror = lazy(() => import("./direct-code-mirror"));
|
||||
const editorViews = new Map<string, EditorViewType>();
|
||||
|
||||
export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
const editorInstanceId = useId();
|
||||
const editorViewRef = useRef<EditorViewType>(null);
|
||||
const diagnostics = props.diagnostics ?? EMPTY_DIAGNOSTICS;
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -41,7 +41,7 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
setEditorReady(true);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const editorView = editorViews.get(editorInstanceId);
|
||||
const editorView = editorViewRef.current;
|
||||
if (!editorView || !props.revealText) return;
|
||||
const from = editorView.state.doc.toString().indexOf(props.revealText);
|
||||
if (from < 0) return;
|
||||
@@ -50,9 +50,9 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
effects: EditorView.scrollIntoView(from, { y: "center" }),
|
||||
});
|
||||
editorView.focus();
|
||||
}, [editorCreated, editorInstanceId, props.revealKey, props.revealText]);
|
||||
}, [editorCreated, props.revealKey, props.revealText]);
|
||||
useEffect(() => {
|
||||
const editorView = editorViews.get(editorInstanceId);
|
||||
const editorView = editorViewRef.current;
|
||||
if (!editorView || !props.insertText) return;
|
||||
const selection = editorView.state.selection.main;
|
||||
editorView.dispatch({
|
||||
@@ -61,30 +61,51 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
scrollIntoView: true,
|
||||
});
|
||||
editorView.focus();
|
||||
}, [editorCreated, editorInstanceId, props.insertKey, props.insertText]);
|
||||
const handleCreateEditor = useCallback(
|
||||
(view: EditorViewType) => {
|
||||
editorViews.set(editorInstanceId, view);
|
||||
window.setTimeout(() => setEditorCreated(true), 0);
|
||||
},
|
||||
[editorInstanceId],
|
||||
);
|
||||
const handleDestroyEditor = useCallback(() => {
|
||||
editorViews.delete(editorInstanceId);
|
||||
}, [editorInstanceId]);
|
||||
const selectDiagnostic = useCallback(
|
||||
(diagnostic: EditorDiagnostic) => {
|
||||
const editorView = editorViews.get(editorInstanceId);
|
||||
if (!editorView || diagnostic.index === undefined) return;
|
||||
const anchor = Math.min(diagnostic.index, editorView.state.doc.length);
|
||||
editorView.dispatch({ selection: { anchor }, scrollIntoView: true });
|
||||
editorView.focus();
|
||||
},
|
||||
[editorInstanceId],
|
||||
);
|
||||
}, [editorCreated, props.insertKey, props.insertText]);
|
||||
const handleCreateEditor = useCallback((view: EditorViewType) => {
|
||||
editorViewRef.current = view;
|
||||
window.setTimeout(() => {
|
||||
if (editorViewRef.current === view) setEditorCreated(true);
|
||||
}, 0);
|
||||
}, []);
|
||||
const handleDestroyEditor = useCallback((view: EditorViewType) => {
|
||||
if (editorViewRef.current !== view) return;
|
||||
editorViewRef.current = null;
|
||||
setEditorCreated(false);
|
||||
setCursor({ line: 1, column: 1 });
|
||||
setHistoryDepth({ undo: 0, redo: 0 });
|
||||
}, []);
|
||||
const handleUndo = useCallback(() => {
|
||||
const editorView = editorViewRef.current;
|
||||
if (!editorView) return;
|
||||
undo(editorView);
|
||||
editorView.focus();
|
||||
}, []);
|
||||
const handleRedo = useCallback(() => {
|
||||
const editorView = editorViewRef.current;
|
||||
if (!editorView) return;
|
||||
redo(editorView);
|
||||
editorView.focus();
|
||||
}, []);
|
||||
const handleUpdate = useCallback((update: ViewUpdate) => {
|
||||
if (!update.selectionSet && !update.docChanged) return;
|
||||
const head = update.state.selection.main.head;
|
||||
const line = update.state.doc.lineAt(head);
|
||||
setCursor({ line: line.number, column: head - line.from + 1 });
|
||||
setHistoryDepth({ undo: undoDepth(update.state), redo: redoDepth(update.state) });
|
||||
}, []);
|
||||
const selectDiagnostic = useCallback((diagnostic: EditorDiagnostic) => {
|
||||
const editorView = editorViewRef.current;
|
||||
if (!editorView || diagnostic.index === undefined) return;
|
||||
const anchor = Math.min(diagnostic.index, editorView.state.doc.length);
|
||||
editorView.dispatch({ selection: { anchor }, scrollIntoView: true });
|
||||
editorView.focus();
|
||||
}, []);
|
||||
const formattedValue = useMemo(() => formatCode(props.value, props.language), [props.language, props.value]);
|
||||
const errorCount = diagnostics.filter(({ severity }) => severity === "error").length;
|
||||
const warningCount = diagnostics.length - errorCount;
|
||||
const isCharacterLimitExceeded = props.maxLength !== undefined && props.value.length > props.maxLength;
|
||||
const isEditorInvalid = Boolean(props.error) || errorCount > 0 || isCharacterLimitExceeded;
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.value);
|
||||
@@ -101,6 +122,7 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
error={props.error}
|
||||
required={props.required}
|
||||
labelProps={{ id: `${props.id}-label`, htmlFor: props.id }}
|
||||
descriptionProps={{ className: classes.accessibleMuted }}
|
||||
inputWrapperOrder={["label", "description", "input", "error"]}
|
||||
>
|
||||
<div className={classes.root}>
|
||||
@@ -113,16 +135,10 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
referenceOpened={referenceOpened}
|
||||
onReferenceToggle={() => setReferenceOpened((value) => !value)}
|
||||
onCopy={() => void copy()}
|
||||
onUndo={() => {
|
||||
const editorView = editorViews.get(editorInstanceId);
|
||||
if (editorView) undo(editorView);
|
||||
}}
|
||||
onRedo={() => {
|
||||
const editorView = editorViews.get(editorInstanceId);
|
||||
if (editorView) redo(editorView);
|
||||
}}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
onFormat={() => {
|
||||
const editorView = editorViews.get(editorInstanceId);
|
||||
const editorView = editorViewRef.current;
|
||||
if (props.language === "jsx" && editorView) {
|
||||
const selection = editorView.state.selection.main;
|
||||
editorView.dispatch({ selection: { anchor: 0, head: editorView.state.doc.length } });
|
||||
@@ -157,23 +173,18 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
value={props.value}
|
||||
language={props.language}
|
||||
diagnostics={diagnostics}
|
||||
completions={props.completions ?? []}
|
||||
completions={props.completions ?? EMPTY_COMPLETIONS}
|
||||
label={props.label}
|
||||
placeholder={props.placeholder}
|
||||
theme={colorScheme}
|
||||
invalid={isEditorInvalid}
|
||||
height={props.height ?? (props.language === "jsx" ? "320px" : "220px")}
|
||||
readOnly={props.readOnly ?? false}
|
||||
diagnosticMessage={props.messages.diagnostic}
|
||||
onChange={props.readOnly ? () => undefined : props.onChange}
|
||||
onCreateEditor={handleCreateEditor}
|
||||
onDestroyEditor={handleDestroyEditor}
|
||||
onUpdate={(update) => {
|
||||
if (!update.selectionSet && !update.docChanged) return;
|
||||
const head = update.state.selection.main.head;
|
||||
const line = update.state.doc.lineAt(head);
|
||||
setCursor({ line: line.number, column: head - line.from + 1 });
|
||||
setHistoryDepth({ undo: undoDepth(update.state), redo: redoDepth(update.state) });
|
||||
}}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
@@ -193,16 +204,22 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
) : (
|
||||
<Group gap={4}>
|
||||
<IconCheck size={13} color="var(--mantine-color-green-6)" aria-hidden />
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" className={classes.accessibleMuted}>
|
||||
{props.messages.ready}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" className={classes.accessibleMuted}>
|
||||
{props.messages.position(cursor)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c={props.maxLength && props.value.length > props.maxLength ? "red" : "dimmed"}>
|
||||
<Text
|
||||
component="output"
|
||||
size="xs"
|
||||
className={classes.accessibleMuted}
|
||||
data-invalid={isCharacterLimitExceeded}
|
||||
aria-live="polite"
|
||||
>
|
||||
{props.messages.characters(props.value.length, props.maxLength)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -6,12 +6,13 @@ import { css } from "@codemirror/lang-css";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { linter } from "@codemirror/lint";
|
||||
import { EditorState, StateEffect } from "@codemirror/state";
|
||||
import { Annotation, Compartment, EditorSelection, EditorState, Transaction } from "@codemirror/state";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { EditorView, keymap, placeholder as codeMirrorPlaceholder } from "@codemirror/view";
|
||||
import type { ViewUpdate } from "@codemirror/view";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import type { Completion } from "@codemirror/autocomplete";
|
||||
|
||||
import { createCustomJsxCompletionSource, customJsxComponentHover } from "./code-language";
|
||||
@@ -28,11 +29,12 @@ interface DirectCodeMirrorProps {
|
||||
placeholder?: string;
|
||||
height: string;
|
||||
theme: "light" | "dark";
|
||||
invalid: boolean;
|
||||
readOnly: boolean;
|
||||
diagnosticMessage(diagnostic: EditorDiagnostic): string;
|
||||
onChange(value: string): void;
|
||||
onCreateEditor(view: EditorView): void;
|
||||
onDestroyEditor(): void;
|
||||
onDestroyEditor(view: EditorView): void;
|
||||
onUpdate(update: ViewUpdate): void;
|
||||
}
|
||||
|
||||
@@ -41,99 +43,203 @@ interface EditorCallbacks {
|
||||
onUpdate(update: ViewUpdate): void;
|
||||
}
|
||||
|
||||
interface EditorCompartments {
|
||||
language: Compartment;
|
||||
diagnostics: Compartment;
|
||||
accessibility: Compartment;
|
||||
editability: Compartment;
|
||||
placeholder: Compartment;
|
||||
height: Compartment;
|
||||
theme: Compartment;
|
||||
}
|
||||
|
||||
const editorViews = new Map<string, EditorView>();
|
||||
const editorCallbacks = new Map<string, EditorCallbacks>();
|
||||
const editorContainers = new Map<string, HTMLDivElement>();
|
||||
const controlledValueUpdate = Annotation.define<boolean>();
|
||||
|
||||
export default function DirectCodeMirror(props: DirectCodeMirrorProps) {
|
||||
const instanceId = useId();
|
||||
const [initialProps] = useState(props);
|
||||
const [containerReady, setContainerReady] = useState(false);
|
||||
const setContainer = useCallback(
|
||||
(container: HTMLDivElement | null) => {
|
||||
if (!container) {
|
||||
editorContainers.delete(instanceId);
|
||||
return;
|
||||
}
|
||||
editorContainers.set(instanceId, container);
|
||||
setContainerReady(true);
|
||||
},
|
||||
[instanceId],
|
||||
);
|
||||
const [compartments] = useState(createEditorCompartments);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
editorCallbacks.set(instanceId, { onChange: props.onChange, onUpdate: props.onUpdate });
|
||||
}, [instanceId, props.onChange, props.onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const parent = editorContainers.get(instanceId);
|
||||
const parent = containerRef.current;
|
||||
if (!parent) return;
|
||||
const view = new EditorView({
|
||||
parent,
|
||||
state: EditorState.create({ doc: initialProps.value, extensions: createExtensions(instanceId, initialProps) }),
|
||||
state: EditorState.create({
|
||||
doc: initialProps.value,
|
||||
extensions: createExtensions(instanceId, initialProps, compartments),
|
||||
}),
|
||||
});
|
||||
editorViews.set(instanceId, view);
|
||||
initialProps.onCreateEditor(view);
|
||||
return () => {
|
||||
editorViews.delete(instanceId);
|
||||
editorCallbacks.delete(instanceId);
|
||||
initialProps.onDestroyEditor();
|
||||
if (editorViews.get(instanceId) === view) {
|
||||
editorViews.delete(instanceId);
|
||||
editorCallbacks.delete(instanceId);
|
||||
}
|
||||
initialProps.onDestroyEditor(view);
|
||||
view.destroy();
|
||||
};
|
||||
}, [containerReady, initialProps, instanceId]);
|
||||
}, [compartments, initialProps, instanceId]);
|
||||
|
||||
useEffect(() => {
|
||||
const view = editorViews.get(instanceId);
|
||||
if (!view || view.state.doc.toString() === props.value) return;
|
||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: props.value } });
|
||||
const documentLength = props.value.length;
|
||||
const selection = EditorSelection.create(
|
||||
view.state.selection.ranges.map((range) =>
|
||||
EditorSelection.range(Math.min(range.anchor, documentLength), Math.min(range.head, documentLength)),
|
||||
),
|
||||
view.state.selection.mainIndex,
|
||||
);
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: props.value },
|
||||
selection,
|
||||
annotations: [Transaction.addToHistory.of(false), controlledValueUpdate.of(true)],
|
||||
});
|
||||
}, [instanceId, props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
const view = editorViews.get(instanceId);
|
||||
if (!view) return;
|
||||
view.dispatch({ effects: StateEffect.reconfigure.of(createExtensions(instanceId, props)) });
|
||||
}, [instanceId, props]);
|
||||
reconfigure(instanceId, compartments.language, createLanguageExtension(props.language, props.completions));
|
||||
}, [compartments.language, instanceId, props.completions, props.language]);
|
||||
useEffect(() => {
|
||||
reconfigure(
|
||||
instanceId,
|
||||
compartments.diagnostics,
|
||||
createDiagnosticsExtension(props.diagnostics, props.diagnosticMessage),
|
||||
);
|
||||
}, [compartments.diagnostics, instanceId, props.diagnosticMessage, props.diagnostics]);
|
||||
useEffect(() => {
|
||||
reconfigure(
|
||||
instanceId,
|
||||
compartments.accessibility,
|
||||
EditorView.contentAttributes.of({
|
||||
id: props.id,
|
||||
tabindex: "0",
|
||||
"aria-label": props.label,
|
||||
"aria-labelledby": props.labelledBy,
|
||||
"aria-invalid": props.invalid.toString(),
|
||||
}),
|
||||
);
|
||||
}, [compartments.accessibility, instanceId, props.id, props.invalid, props.label, props.labelledBy]);
|
||||
useEffect(() => {
|
||||
reconfigure(instanceId, compartments.editability, createEditabilityExtension(props.readOnly));
|
||||
}, [compartments.editability, instanceId, props.readOnly]);
|
||||
useEffect(() => {
|
||||
reconfigure(instanceId, compartments.placeholder, createPlaceholderExtension(props.placeholder));
|
||||
}, [compartments.placeholder, instanceId, props.placeholder]);
|
||||
useEffect(() => {
|
||||
reconfigure(instanceId, compartments.height, createHeightExtension(props.height));
|
||||
}, [compartments.height, instanceId, props.height]);
|
||||
useEffect(() => {
|
||||
reconfigure(instanceId, compartments.theme, createThemeExtension(props.theme));
|
||||
}, [compartments.theme, instanceId, props.theme]);
|
||||
|
||||
return <div ref={setContainer} />;
|
||||
return <div ref={containerRef} />;
|
||||
}
|
||||
|
||||
function createExtensions(instanceId: string, props: DirectCodeMirrorProps) {
|
||||
const language =
|
||||
props.language === "jsx"
|
||||
? [
|
||||
javascript({ jsx: true }),
|
||||
autocompletion({ override: [createCustomJsxCompletionSource(props.completions)] }),
|
||||
customJsxComponentHover,
|
||||
]
|
||||
: props.language === "css"
|
||||
? [css()]
|
||||
: [json()];
|
||||
function createEditorCompartments(): EditorCompartments {
|
||||
return {
|
||||
language: new Compartment(),
|
||||
diagnostics: new Compartment(),
|
||||
accessibility: new Compartment(),
|
||||
editability: new Compartment(),
|
||||
placeholder: new Compartment(),
|
||||
height: new Compartment(),
|
||||
theme: new Compartment(),
|
||||
};
|
||||
}
|
||||
|
||||
function createExtensions(instanceId: string, props: DirectCodeMirrorProps, compartments: EditorCompartments) {
|
||||
return [
|
||||
basicSetup,
|
||||
...language,
|
||||
linter((view) =>
|
||||
props.diagnostics.map((diagnostic) => {
|
||||
const from = Math.min(diagnostic.index ?? 0, view.state.doc.length);
|
||||
return {
|
||||
from,
|
||||
to: Math.min(from + 1, view.state.doc.length),
|
||||
severity: diagnostic.severity,
|
||||
message: props.diagnosticMessage(diagnostic),
|
||||
};
|
||||
compartments.language.of(createLanguageExtension(props.language, props.completions)),
|
||||
compartments.diagnostics.of(createDiagnosticsExtension(props.diagnostics, props.diagnosticMessage)),
|
||||
compartments.accessibility.of(
|
||||
EditorView.contentAttributes.of({
|
||||
id: props.id,
|
||||
tabindex: "0",
|
||||
"aria-label": props.label,
|
||||
"aria-labelledby": props.labelledBy,
|
||||
"aria-invalid": props.invalid.toString(),
|
||||
}),
|
||||
),
|
||||
EditorView.contentAttributes.of({ id: props.id, "aria-label": props.label, "aria-labelledby": props.labelledBy }),
|
||||
EditorView.editable.of(!props.readOnly),
|
||||
EditorState.readOnly.of(props.readOnly),
|
||||
keymap.of(props.readOnly ? [] : [indentWithTab]),
|
||||
props.placeholder ? codeMirrorPlaceholder(props.placeholder) : [],
|
||||
EditorView.theme({ "&": { height: props.height }, ".cm-scroller": { overflow: "auto" } }),
|
||||
props.theme === "dark" ? oneDark : [],
|
||||
compartments.editability.of(createEditabilityExtension(props.readOnly)),
|
||||
compartments.placeholder.of(createPlaceholderExtension(props.placeholder)),
|
||||
compartments.height.of(createHeightExtension(props.height)),
|
||||
compartments.theme.of(createThemeExtension(props.theme)),
|
||||
EditorView.updateListener.of((update) => {
|
||||
const callbacks = editorCallbacks.get(instanceId);
|
||||
if (!callbacks) return;
|
||||
if (update.docChanged) callbacks.onChange(update.state.doc.toString());
|
||||
const isControlledValueUpdate = update.transactions.some(
|
||||
(transaction) => transaction.annotation(controlledValueUpdate) === true,
|
||||
);
|
||||
if (update.docChanged && !isControlledValueUpdate) callbacks.onChange(update.state.doc.toString());
|
||||
callbacks.onUpdate(update);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function createLanguageExtension(language: DirectCodeMirrorProps["language"], completions: Completion[]): Extension {
|
||||
if (language === "jsx") {
|
||||
return [
|
||||
javascript({ jsx: true }),
|
||||
autocompletion({ override: [createCustomJsxCompletionSource(completions)] }),
|
||||
customJsxComponentHover,
|
||||
];
|
||||
}
|
||||
if (language === "css") return css();
|
||||
return json();
|
||||
}
|
||||
|
||||
function createDiagnosticsExtension(
|
||||
diagnostics: EditorDiagnostic[],
|
||||
diagnosticMessage: DirectCodeMirrorProps["diagnosticMessage"],
|
||||
): Extension {
|
||||
return linter((view) =>
|
||||
diagnostics.map((diagnostic) => {
|
||||
const from = Math.min(diagnostic.index ?? 0, view.state.doc.length);
|
||||
return {
|
||||
from,
|
||||
to: Math.min(from + 1, view.state.doc.length),
|
||||
severity: diagnostic.severity,
|
||||
message: diagnosticMessage(diagnostic),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function createEditabilityExtension(readOnly: boolean): Extension {
|
||||
return [
|
||||
EditorView.editable.of(!readOnly),
|
||||
EditorState.readOnly.of(readOnly),
|
||||
keymap.of(readOnly ? [] : [indentWithTab]),
|
||||
];
|
||||
}
|
||||
|
||||
function createPlaceholderExtension(placeholder: string | undefined): Extension {
|
||||
if (!placeholder) return [];
|
||||
return codeMirrorPlaceholder(placeholder);
|
||||
}
|
||||
|
||||
function createHeightExtension(height: string): Extension {
|
||||
return EditorView.theme({ "&": { height }, ".cm-scroller": { overflow: "auto" } });
|
||||
}
|
||||
|
||||
function createThemeExtension(theme: DirectCodeMirrorProps["theme"]): Extension {
|
||||
if (theme === "dark") return oneDark;
|
||||
return [];
|
||||
}
|
||||
|
||||
function reconfigure(instanceId: string, compartment: Compartment, extension: Extension) {
|
||||
const view = editorViews.get(instanceId);
|
||||
if (!view) return;
|
||||
view.dispatch({ effects: compartment.reconfigure(extension) });
|
||||
}
|
||||
Reference in new issue
Block a user