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; public string Class { get; set; } = string.Empty;
private readonly string editorId = $"code-editor-{Guid.NewGuid():N}"; 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 editorElement;
private ElementReference lineNumbersElement;
private IJSObjectReference? module; private IJSObjectReference? module;
private string CodeEditorThemeStyle => this.GetCodeEditorThemeStyle(); private string CodeEditorThemeStyle => this.GetCodeEditorThemeStyle();
@ -34,7 +35,7 @@ public partial class CodeEditor : ComponentBase, IAsyncDisposable
return; return;
this.module = await this.JsRuntime.InvokeAsync<IJSObjectReference>("import", CODE_EDITOR_MODULE); 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() public async ValueTask<string> GetCodeAsync()

View File

@ -204,21 +204,45 @@
} }
.code-editor { .code-editor {
display: grid;
grid-template-columns: minmax(2.8rem, auto) minmax(0, 1fr);
min-height: 32rem; min-height: 32rem;
height: 62vh; height: 62vh;
width: 100%; width: 100%;
overflow: auto; overflow: hidden;
padding: 0.8rem;
border: 1px solid var(--mw-code-editor-border, rgba(0,0,0,0.11764705882352941)); border: 1px solid var(--mw-code-editor-border, rgba(0,0,0,0.11764705882352941));
border-radius: 4px; border-radius: 4px;
background: var(--mw-code-editor-background, rgba(255,255,255,1)); background: var(--mw-code-editor-background, rgba(255,255,255,1));
color: var(--mw-code-editor-foreground, rgba(66,66,66,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-family: "JetBrains Mono", monospace;
font-size: 0.65rem; font-size: 0.65rem;
line-height: 1.45; line-height: 1.45;
white-space: pre;
tab-size: 4; 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; 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 * 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. * 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), { const codeJar = CodeJar(element, getHighlighter(language), {
tab: ' ', tab: ' ',
spellcheck: false spellcheck: false
}); });
const scrollHandler = () => syncLineNumbersScroll(element, lineNumbersElement);
codeJar.updateCode(code ?? ''); 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. * Returns the current plain text from a CodeJar instance.
*/ */
export function getCode(id) { 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. * state stays consistent with CodeJar's internal model.
*/ */
export function setCode(id, code) { 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. * Disposes one editor instance and removes it from the JS-side registry.
*/ */
export function destroy(id) { 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); 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) { function highlightLua(editor) {
editor.innerHTML = highlightLuaCode(editor.textContent ?? ''); editor.innerHTML = highlightLuaCode(editor.textContent ?? '');
} }