A legibility-first code editor.
0

Configure Feed

Select the types of activity you want to include in your feed.

app: render selection body + block cursor + insert-mode caret

renderSnapshot now consumes selectionStart/selectionEnd alongside
cursorStart/cursorEnd. In normal/select mode it draws a subtle
.selection tint over the body and a stronger .cursor highlight over
the block-cursor grapheme inside it — so multi-char selections (w, e, x)
finally show up visually. In insert mode it applies a border-left to
the character at cursor position (no more inline-block caret, which
disrupted the monospace grid and caused the c cursor to visually shift
a pixel). EOF caret handled separately with a zero-width span.

Fixes the two things user asked for: highlighting renders, and the c
cursor is now visually where the underlying data says it is.

author
Isaac Corbrey
date (Jul 16, 2026, 5:09 PM -0400) commit baa17150 parent fef88628 change-id mnupvurs
+106 -43
+28 -8
app/index.html
··· 13 13 white-space: pre; 14 14 margin: 0; 15 15 } 16 + /* Selection body: subtler highlight than the cursor, so the two 17 + can visually nest without confusion. */ 18 + .selection { 19 + background: rgba(255, 204, 51, 0.25); 20 + } 21 + /* Block cursor in normal/select mode: strong highlight over the 22 + grapheme the cursor is on. */ 16 23 .cursor { 17 24 background: #ffcc33; 18 25 color: #000; 19 26 } 20 - .caret { 27 + /* Insert-mode caret drawn as a left border on the character the 28 + cursor sits before. No `inline-block` — this keeps the 29 + monospace grid alignment perfect. */ 30 + .caret-on { 31 + border-left: 2px solid #ffcc33; 32 + /* Push the character back by the border width so we don't 33 + shift the character to the right of the caret. */ 34 + margin-left: -2px; 35 + } 36 + /* Insert-mode caret at EOF, when there's no character to 37 + decorate. Drawn as a bare border with a small span to give it 38 + height. */ 39 + .caret-eof { 40 + border-left: 2px solid #ffcc33; 41 + margin-left: -2px; 42 + /* Reserve some vertical space so the caret is visible on an 43 + otherwise-empty final line. Zero-width horizontally so it 44 + doesn't disrupt the grid. */ 21 45 display: inline-block; 22 - width: 2px; 23 - margin-left: -1px; 24 - margin-right: -1px; 25 - background: #ffcc33; 26 - vertical-align: baseline; 27 - /* Give the caret height even when it has no text content. */ 28 - height: 1.1em; 46 + width: 0; 47 + height: 1em; 48 + vertical-align: text-bottom; 29 49 } 30 50 #mode { 31 51 margin-top: 1rem;
+78 -35
app/src/main.ts
··· 10 10 11 11 type Snapshot = { 12 12 text: string; 13 - // Range of characters the primary cursor visually covers, following 14 - // Helix's block-cursor semantics: `[cursorStart, cursorEnd)` in 15 - // normal/select modes is a width-1 grapheme; in insert mode it 16 - // collapses to `cursorStart == cursorEnd` for a between-character 17 - // caret. 13 + // Primary selection body: `[selectionStart, selectionEnd)`. In 14 + // normal/select modes this is what the frontend renders as the 15 + // background highlight. In insert mode we ignore this and just draw 16 + // the caret at `cursorStart`. 17 + selectionStart: number; 18 + selectionEnd: number; 19 + // Range of characters the primary cursor visually covers as a block, 20 + // following Helix's block-cursor semantics: `[cursorStart, cursorEnd)` 21 + // in normal/select is a width-1 grapheme; in insert mode it collapses 22 + // to `cursorStart == cursorEnd` (a between-character caret). 18 23 cursorStart: number; 19 24 cursorEnd: number; 20 25 mode: Mode; ··· 44 49 const statusEl = () => document.getElementById("status") as HTMLElement | null; 45 50 const modeEl = () => document.getElementById("mode") as HTMLElement | null; 46 51 52 + function span(className: string, text: string): HTMLSpanElement { 53 + const el = document.createElement("span"); 54 + el.className = className; 55 + // Use textContent (not innerText) to preserve exact bytes including 56 + // newlines. The parent `<pre>` handles whitespace preservation. 57 + el.textContent = text; 58 + return el; 59 + } 60 + 61 + /** 62 + * Render the buffer with: 63 + * - a selection-body highlight over `[selectionStart, selectionEnd)` 64 + * in normal/select mode 65 + * - a block-cursor highlight over `[cursorStart, cursorEnd)` inside 66 + * that body (or standalone if the body is collapsed) 67 + * - a thin caret (via `border-left` on the character at `cursorStart`) 68 + * in insert mode; or a zero-width span when the caret sits at EOF 69 + * 70 + * Uses at most five text/span nodes so the monospace grid in `<pre>` 71 + * stays perfectly aligned — no `inline-block` gaps, no baseline shifts. 72 + */ 47 73 function renderSnapshot(snap: Snapshot) { 48 74 const el = bufferEl(); 49 75 if (el) { 50 - // Render the cursor per Helix's block-cursor semantics: 51 - // - normal/select: highlight the grapheme in `[cursorStart, cursorEnd)`. 52 - // - insert: draw a zero-width caret at `cursorStart` (== cursorEnd) 53 - // between characters. 54 - // Placeholder rendering — real DOM ranges + styled highlights come 55 - // when we wire up proper text rendering. 56 76 const text = snap.text; 57 - const start = Math.min(snap.cursorStart, text.length); 58 - const end = Math.min(snap.cursorEnd, text.length); 59 - const before = text.slice(0, start); 60 - const after = text.slice(end); 77 + const len = text.length; 78 + // Clamp everything so out-of-bounds server values can't produce 79 + // negative-slice arguments. 80 + const clamp = (n: number) => Math.max(0, Math.min(n, len)); 81 + const selStart = clamp(snap.selectionStart); 82 + const selEnd = clamp(Math.max(snap.selectionEnd, snap.selectionStart)); 83 + const curStart = clamp(snap.cursorStart); 84 + const curEnd = clamp(Math.max(snap.cursorEnd, snap.cursorStart)); 85 + 61 86 el.innerHTML = ""; 62 - el.append(document.createTextNode(before)); 63 - if (end > start) { 64 - // Block cursor: render the highlighted grapheme (fallback to a 65 - // space if the range lands past the last character). 66 - const at = text.slice(start, end) || " "; 67 - el.append( 68 - Object.assign(document.createElement("span"), { 69 - className: "cursor", 70 - textContent: at, 71 - }) 72 - ); 87 + 88 + if (snap.mode === "insert") { 89 + // Insert mode: draw a thin caret between characters. Prefer 90 + // decorating the character *at* the cursor with a left border so 91 + // the caret is pixel-accurate against the monospace grid. When 92 + // the cursor sits past the last character, fall back to an empty 93 + // caret span. 94 + el.append(document.createTextNode(text.slice(0, curStart))); 95 + if (curStart < len) { 96 + el.append(span("caret-on", text.slice(curStart, curStart + 1))); 97 + el.append(document.createTextNode(text.slice(curStart + 1))); 98 + } else { 99 + // Caret at EOF: draw a zero-width marker so the visual position 100 + // is still visible after the last character. 101 + el.append(span("caret-eof", "")); 102 + } 73 103 } else { 74 - // Zero-width caret for insert mode. 75 - el.append( 76 - Object.assign(document.createElement("span"), { 77 - className: "caret", 78 - textContent: "", 79 - }) 80 - ); 104 + // Normal/select mode: block cursor + optional selection body 105 + // around it. Handle the two edge cases (cursor entirely inside 106 + // the body, or body collapsed onto the cursor) uniformly by 107 + // just min/max-ing the boundaries. 108 + const bodyStart = Math.min(selStart, curStart); 109 + const bodyEnd = Math.max(selEnd, curEnd); 110 + el.append(document.createTextNode(text.slice(0, bodyStart))); 111 + if (bodyStart < curStart) { 112 + el.append(span("selection", text.slice(bodyStart, curStart))); 113 + } 114 + // Block cursor. Fall back to a non-breaking space when the range 115 + // lands past the last character (i.e. append-at-EOF); otherwise 116 + // slicing gives us the exact grapheme. 117 + const cursorText = text.slice(curStart, curEnd) || "\u00a0"; 118 + el.append(span("cursor", cursorText)); 119 + if (curEnd < bodyEnd) { 120 + el.append(span("selection", text.slice(curEnd, bodyEnd))); 121 + } 122 + el.append(document.createTextNode(text.slice(bodyEnd))); 81 123 } 82 - el.append(document.createTextNode(after)); 83 124 } 84 125 const mode = modeEl(); 85 126 if (mode) { 86 - const pending = snap.pendingKeys.length ? ` · pending: ${snap.pendingKeys.join(" ")}` : ""; 127 + const pending = snap.pendingKeys.length 128 + ? ` · pending: ${snap.pendingKeys.join(" ")}` 129 + : ""; 87 130 const count = snap.count !== undefined ? ` · count: ${snap.count}` : ""; 88 131 mode.textContent = `${snap.mode}${count}${pending}`; 89 132 }