diff --git a/app/MindWork AI Studio/Components/CodeEditor.razor b/app/MindWork AI Studio/Components/CodeEditor.razor index 9dc1692d..8863142d 100644 --- a/app/MindWork AI Studio/Components/CodeEditor.razor +++ b/app/MindWork AI Studio/Components/CodeEditor.razor @@ -1 +1,4 @@ -
+
+ +
+
diff --git a/app/MindWork AI Studio/Components/CodeEditor.razor.cs b/app/MindWork AI Studio/Components/CodeEditor.razor.cs index ac7e019a..f6fceff0 100644 --- a/app/MindWork AI Studio/Components/CodeEditor.razor.cs +++ b/app/MindWork AI Studio/Components/CodeEditor.razor.cs @@ -23,8 +23,9 @@ public partial class CodeEditor : ComponentBase, IAsyncDisposable public string Class { get; set; } = string.Empty; private readonly string editorId = $"code-editor-{Guid.NewGuid():N}"; - private const string CODE_EDITOR_MODULE = "./system/CodeEditor/code-editor.js?v=20260707-3"; + private const string CODE_EDITOR_MODULE = "./system/CodeEditor/code-editor.js?v=20260708-1"; private ElementReference editorElement; + private ElementReference lineNumbersElement; private IJSObjectReference? module; private string CodeEditorThemeStyle => this.GetCodeEditorThemeStyle(); @@ -34,7 +35,7 @@ public partial class CodeEditor : ComponentBase, IAsyncDisposable return; this.module = await this.JsRuntime.InvokeAsync("import", CODE_EDITOR_MODULE); - await this.module.InvokeVoidAsync("init", this.editorId, this.editorElement, this.Value, this.Language.ToString()); + await this.module.InvokeVoidAsync("init", this.editorId, this.editorElement, this.lineNumbersElement, this.Value, this.Language.ToString()); } public async ValueTask GetCodeAsync() diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index aa53a34b..3a6a646a 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -204,21 +204,45 @@ } .code-editor { + display: grid; + grid-template-columns: minmax(2.8rem, auto) minmax(0, 1fr); min-height: 32rem; height: 62vh; width: 100%; - overflow: auto; - padding: 0.8rem; + overflow: hidden; border: 1px solid var(--mw-code-editor-border, rgba(0,0,0,0.11764705882352941)); border-radius: 4px; background: var(--mw-code-editor-background, rgba(255,255,255,1)); color: var(--mw-code-editor-foreground, rgba(66,66,66,1)); - caret-color: var(--mw-code-editor-foreground, rgba(66,66,66,1)); font-family: "JetBrains Mono", monospace; font-size: 0.65rem; line-height: 1.45; - white-space: pre; tab-size: 4; +} + +.code-editor-line-numbers { + overflow: hidden; + padding: 0.8rem 0.65rem 0.8rem 0.5rem; + border-right: 1px solid var(--mw-code-editor-border, rgba(0,0,0,0.11764705882352941)); + color: var(--mw-code-editor-foreground, rgba(66,66,66,1)); + opacity: 0.55; + text-align: right; + white-space: pre; + user-select: none; + font: inherit; + font-variant-numeric: tabular-nums; +} + +.code-editor-input { + min-width: 0; + height: 100%; + overflow: auto; + padding: 0.8rem; + color: var(--mw-code-editor-foreground, rgba(66,66,66,1)); + caret-color: var(--mw-code-editor-foreground, rgba(66,66,66,1)); + font: inherit; + line-height: inherit; + tab-size: inherit; outline: none; } diff --git a/app/MindWork AI Studio/wwwroot/system/CodeEditor/code-editor.js b/app/MindWork AI Studio/wwwroot/system/CodeEditor/code-editor.js index fdbcd8be..cb5397c6 100644 --- a/app/MindWork AI Studio/wwwroot/system/CodeEditor/code-editor.js +++ b/app/MindWork AI Studio/wwwroot/system/CodeEditor/code-editor.js @@ -27,21 +27,25 @@ const LUA_BUILT_INS = new Set([ * The highlighter callback receives the editor DOM node. It must write highlighted * HTML back into that node, so every token emitted by our highlighter is HTML-escaped. */ -export function init(id, element, code, language) { +export function init(id, element, lineNumbersElement, code, language) { const codeJar = CodeJar(element, getHighlighter(language), { tab: ' ', spellcheck: false }); + const scrollHandler = () => syncLineNumbersScroll(element, lineNumbersElement); codeJar.updateCode(code ?? ''); - editors.set(id, codeJar); + updateLineNumbers(lineNumbersElement, codeJar.toString()); + codeJar.onUpdate(updatedCode => updateLineNumbers(lineNumbersElement, updatedCode)); + element.addEventListener('scroll', scrollHandler); + editors.set(id, { codeJar, element, scrollHandler }); } /** * Returns the current plain text from a CodeJar instance. */ export function getCode(id) { - return editors.get(id)?.toString() ?? ''; + return editors.get(id)?.codeJar.toString() ?? ''; } /** @@ -49,17 +53,43 @@ export function getCode(id) { * state stays consistent with CodeJar's internal model. */ export function setCode(id, code) { - editors.get(id)?.updateCode(code ?? ''); + const editor = editors.get(id); + if (!editor) + return; + + editor.codeJar.updateCode(code ?? ''); } /** * Disposes one editor instance and removes it from the JS-side registry. */ export function destroy(id) { - editors.get(id)?.destroy(); + const editor = editors.get(id); + if (!editor) + return; + + editor.element.removeEventListener('scroll', editor.scrollHandler); + editor.codeJar.destroy(); editors.delete(id); } +function updateLineNumbers(lineNumbersElement, code) { + const lineCount = (code.match(/\n/g)?.length ?? 0) + 1; + let lineNumbers = ''; + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { + if (lineNumber > 1) + lineNumbers += '\n'; + + lineNumbers += lineNumber; + } + + lineNumbersElement.textContent = lineNumbers; +} + +function syncLineNumbersScroll(editorElement, lineNumbersElement) { + lineNumbersElement.scrollTop = editorElement.scrollTop; +} + function highlightLua(editor) { editor.innerHTML = highlightLuaCode(editor.textContent ?? ''); }