diff --git a/react-components/src/components/Editor.tsx b/react-components/src/components/Editor.tsx index 7f476b0..7a301bc 100644 --- a/react-components/src/components/Editor.tsx +++ b/react-components/src/components/Editor.tsx @@ -19,9 +19,18 @@ interface EditorProps { hover?: string; completionTriggerCharacters?: string[]; serveDirectory?: string; + markers?: EditorMarker[]; onChange?: (value: string) => void; } +interface EditorMarker { + line: number; + message: string; + column?: number; + endColumn?: number; + severity?: "error" | "warning" | "info" | "hint"; +} + function completionItemKind(kind?: string) { const K = monaco.languages.CompletionItemKind; const map: Record = { @@ -58,6 +67,7 @@ export default function Editor({ hover, completionTriggerCharacters = ["."], serveDirectory, + markers, onChange, }: EditorProps) { if (serveDirectory) { @@ -262,6 +272,35 @@ export default function Editor({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [language]); + useEffect(() => { + const editor = editorRef.current; + const model = editor?.getModel(); + if (!model) { + return; + } + const severities = { + error: monaco.MarkerSeverity.Error, + warning: monaco.MarkerSeverity.Warning, + info: monaco.MarkerSeverity.Info, + hint: monaco.MarkerSeverity.Hint, + }; + monaco.editor.setModelMarkers( + model, + "trame", + (markers || []).map((m) => { + const line = Math.max(1, Math.min(m.line, model.getLineCount())); + return { + startLineNumber: line, + endLineNumber: line, + startColumn: m.column ?? 1, + endColumn: m.endColumn ?? model.getLineMaxColumn(line), + message: m.message, + severity: severities[m.severity || "error"], + }; + }), + ); + }, [markers]); + useEffect(() => { editorRef.current?.updateOptions({ theme }); providerRef.current?.updateTheme(theme); diff --git a/src/trame_code/widgets/code.py b/src/trame_code/widgets/code.py index a33406d..7356b32 100644 --- a/src/trame_code/widgets/code.py +++ b/src/trame_code/widgets/code.py @@ -60,6 +60,7 @@ def __init__(self, completion=None, hover=None, **kwargs): ("completion_trigger_characters", "completionTriggerCharacters"), "completion", "hover", + "markers", ] if self.server.client_type == "react": # React has no v-model: binding is an explicit value/on_change pair. diff --git a/tests/test_language_features.py b/tests/test_language_features.py index abc3a01..c0501f9 100644 --- a/tests/test_language_features.py +++ b/tests/test_language_features.py @@ -28,3 +28,17 @@ def on_hover(code_text, line, column): ctrl = server.controller assert ctrl.trigger_fn(server.trigger_name(on_complete)) is on_complete assert ctrl.trigger_fn(server.trigger_name(on_hover)) is on_hover + + +def test_markers_bind_as_a_prop(): + """Markers are passed through to the editor so a host app can mark lines.""" + from trame.app import get_server + from trame.ui.html import DivLayout + + from trame.widgets import code + + server = get_server("test_markers") + with DivLayout(server): + editor = code.Editor(language="python", markers=("diagnostics",)) + + assert ':markers="diagnostics"' in editor.html diff --git a/vue-components/src/components/Editor.js b/vue-components/src/components/Editor.js index 311ebf8..ed39894 100644 --- a/vue-components/src/components/Editor.js +++ b/vue-components/src/components/Editor.js @@ -65,8 +65,18 @@ export default { type: Array, default: () => ["."], }, + markers: { + type: Array, + default: () => [], + }, }, watch: { + markers: { + deep: true, + handler() { + this.applyMarkers(); + }, + }, modelValue(v) { if (this.editor && v !== undefined && this.editor.getValue() !== v) { this.editor.setValue(v); @@ -109,6 +119,33 @@ export default { }, }, methods: { + applyMarkers() { + const model = this.editor && this.editor.getModel(); + if (!model) { + return; + } + const severities = { + error: monaco.MarkerSeverity.Error, + warning: monaco.MarkerSeverity.Warning, + info: monaco.MarkerSeverity.Info, + hint: monaco.MarkerSeverity.Hint, + }; + monaco.editor.setModelMarkers( + model, + "trame", + (this.markers || []).map((m) => { + const line = Math.max(1, Math.min(m.line, model.getLineCount())); + return { + startLineNumber: line, + endLineNumber: line, + startColumn: m.column || 1, + endColumn: m.endColumn || model.getLineMaxColumn(line), + message: m.message, + severity: severities[m.severity || "error"], + }; + }), + ); + }, registerTextmateLanguageDefinitions(languages, grammars, configs, inject) { this.provider = new SimpleLanguageInfoProvider({ monaco, @@ -267,6 +304,8 @@ export default { ...this.options, }); + this.applyMarkers(); + if (provider) { provider.injectCSS(); }