added a line number column that syncs to content and scroll

This commit is contained in:
krut_ni 2026-07-08 22:24:43 +02:00
parent eed11fd9c6
commit 6c3906b04c
No known key found for this signature in database
GPG Key ID: A5C0151B4DDB172C
4 changed files with 70 additions and 12 deletions

View File

@ -1 +1,4 @@
<div @ref="this.editorElement" class="code-editor @this.Class" style="@this.CodeEditorThemeStyle"></div>
<div class="code-editor @this.Class" style="@this.CodeEditorThemeStyle">
<div @ref="this.lineNumbersElement" class="code-editor-line-numbers" aria-hidden="true"></div>
<div @ref="this.editorElement" class="code-editor-input"></div>
</div>

View File

@ -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<IJSObjectReference>("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<string> GetCodeAsync()

View File

@ -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;
}

View File

@ -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 ?? '');
}