From 053f1e122d4ad9df1f99b7d835b20a107270f6fe Mon Sep 17 00:00:00 2001 From: Jeff Lee Date: Fri, 23 Oct 2026 10:12:22 -0400 Subject: [PATCH 1/2] feat(editor): add a markers prop Applications that run or lint the edited code could report where it failed only outside the editor. The editor now takes a list of {line, message, severity} and applies it with setModelMarkers, so the line is marked and the message appears on hover. Both clients accept it, and markers set before the editor mounts are applied once the model exists. --- react-components/src/components/Editor.tsx | 39 ++++++++++++++++++++++ src/trame_code/widgets/code.py | 1 + vue-components/src/components/Editor.js | 39 ++++++++++++++++++++++ 3 files changed, 79 insertions(+) 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/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(); } From a096e9e46bcfecdd6fbed22f24631fc645ecd5a9 Mon Sep 17 00:00:00 2001 From: Jeff Lee Date: Fri, 23 Oct 2026 10:23:28 -0400 Subject: [PATCH 2/2] test: cover the markers prop binding --- tests/test_language_features.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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