Skip to content

feat(playground): line-number gutter, Hack font, string literals - #23

Merged
ohnishant merged 4 commits into
feat/undo-stackfrom
feat/editor-polish
Aug 23, 2026
Merged

ohnishant merged 4 commits into
feat/undo-stackfrom
feat/editor-polish

Conversation

@ohnishant

Copy link
Copy Markdown
Owner

Stacked on #20; merge order is #20 then this. Three related editor upgrades:

Line-number gutter. Numbers are derived from the source text, never from the DOM, and the gutter mirrors only the editor's vertical scroll (wide lines scroll horizontally under fixed numbers). Clicking a number places the caret at that line's start. Getting here surfaced two layout bugs worth knowing about: main's auto-sized grid row let long source stretch the whole page instead of scrolling inside the pane, and body's min-height gave flex nothing definite to hand down. The wide layout now caps both, so the pane scrolls internally and the gutter stays pinned. The editor also switches from pre-wrap to pre, since a wrapped line would occupy several rows and desynchronize the numbers.

Hack font. The CSS has been asking for JetBrains Mono since it existed but loaded neither it nor Fira Code, so every visitor saw an arbitrary fallback. Hack woff2 (regular + bold) is vendored under web/fonts with its MIT license, same offline-first approach as the vendored emotes.

String literals (#21). buildEditorFragment splits quoted spans before emote substitution and wraps them in .str (amber). Quote pairing mirrors the lexer's readString exactly: no escapes, newlines allowed, unterminated quotes stay lit to EOF because that is what the lexer will consume as ILLEGAL. Emote names still render as images inside strings, as before. No changes were needed in editorSource/forEachPiece/setCaretOffset - they already recurse into element nodes.

Verified in a real browser against a local server: Hack loads via document.fonts; gutter row 1 and text line 1 share the same y-coordinate; line count tracks typing, undo (11 -> 15 -> 11 rows across an undo/redo round trip), and example loading; click-to-line lands on exact source offsets; source extraction round-trips through spans and emote images unchanged; autocomplete acceptance still resets undo history as designed in #20.

Closes #21

Related: #20, #22

The CSS already asked for JetBrains Mono and Fira Code but loaded
neither, so every visitor saw a fallback. Ship Hack woff2 (regular +
bold, MIT license included) with @font-face and put it first in the
mono stacks for editor, output, and autocomplete.
buildEditorFragment now splits quoted spans before emote substitution
and wraps them in a .str span. The quote pairing mirrors the lexer's
readString: no escapes, newlines allowed, unterminated runs to EOF.
Emote names still render as images inside strings, as before.

editorSource/forEachPiece/setCaretOffset already recurse into element
nodes, so the span wrapper needs no caret-math changes.
The editor gets a real text-editor gutter: numbers derived from the
source (never the DOM), vertical scroll mirrored from the editor,
horizontal scrolls left alone, and clicking a number places the caret
at that line's first character. applySnapshot updates the gutter too,
so undo/redo keep the count honest.

Two layout fixes make internal scrolling possible at all: main's grid
row is capped with minmax(0,1fr) and the wide layout gives body a
definite height, so long source scrolls inside the pane instead of
stretching the page and dragging the gutter away. The editor switches
from pre-wrap to pre for the same reason - a wrapped line would span
several rows and desynchronize the numbers.
@ohnishant

Copy link
Copy Markdown
Owner Author

Self-review of the stacked diff, same drill as #20.

Verified against a local server in a real browser: gutter row one and text line one share an exact y-coordinate; line counts track typing, undo/redo round trips, and example loads; click-to-line lands on exact source offsets; source extraction round-trips through string spans and emote images unchanged.

Found four things. All are small, so they're fixed in this PR rather than deferred:

  1. Quotes inside comments hijack string coloring. With # say "hi" here above real code, the highlighter paired the comment's quote with the next quote below and painted everything between as a string - reproduced live. The lexer consumes comments before readString ever runs, so those quotes are invisible to it. buildEditorFragment now carves out #-to-end-of-line regions first. Since comments became a known region anyway, they render muted instead of looking like ordinary code; emote names inside them still render as images, same as inside strings.

  2. Clicking the gutter's empty space below the last number resolves to the gutter container itself, whose index comes back as -1, which sends the caret to offset 0. Such clicks are now ignored.

  3. Undoing from a scrolled position can leave the gutter scrolled past what the shrunken content supports if the browser doesn't fire a scroll event for its clamp. applySnapshot resyncs explicitly now.

  4. hack-bold.woff2 rode along in the font commit, but nothing in any stylesheet requests bold monospace, so it was 108KB of dead repo weight (@font-face sources only download when matched, so runtime was never affected). Removed; trivial to restore if something needs a bold face later.

Known limits, left alone on purpose: verification ran in one Chromium engine, no Firefox/Safari pass; the gutter width jumps one notch when the count crosses 9 -> 10 lines; there's no active-line marker yet; dragging from the gutter to select a range isn't supported.

- pair strings the way the lexer does: #-to-EOL comment regions are
  carved out first, so a quote in a comment can no longer open a
  phantom string across real code; comments render muted, emotes
  inside them still render as images
- ignore gutter clicks on empty space below the last number instead
  of teleporting the caret to line 1
- resync gutter scroll after undo/redo restores, for engines that
  skip the scroll event when clamping scrollTop
- drop hack-bold.woff2: no stylesheet requests bold monospace

Found via the self-review posted on #23.
@ohnishant
ohnishant merged commit db1c232 into feat/undo-stack Aug 23, 2026
ohnishant added a commit that referenced this pull request Aug 23, 2026
* feat(playground): manual undo/redo for the editor

normalize() rebuilds the DOM after every input event, so the browser's
native undo history never survives; ctrl+z did nothing. Keep our own
stack instead of pulling in an editor component:

- snapshot commits land 400ms after the last mutation and store the
  previous {source, caretOffset}, so one step rewinds a typed burst
- ctrl/cmd+z undoes, ctrl/cmd+shift+z and ctrl+y redo; the combos are
  always swallowed since native undo would corrupt the rebuilt DOM
- programmatic replaces (autocomplete accept, example load) reset the
  stack, as sketched in #18
- depth capped at 200 snapshots

Closes #18

* fix(playground): guard undo commit against a missing baseline

Self-review found committed is only initialized because loadExample
runs synchronously at startup; make commitUndoPoint adopt the current
state instead of throwing if that init ever moves.

* feat(playground): line-number gutter, Hack font, string literals (#23)

* feat(playground): vendor Hack as the editor font

The CSS already asked for JetBrains Mono and Fira Code but loaded
neither, so every visitor saw a fallback. Ship Hack woff2 (regular +
bold, MIT license included) with @font-face and put it first in the
mono stacks for editor, output, and autocomplete.

* feat(playground): highlight string literals in the editor (#21)

buildEditorFragment now splits quoted spans before emote substitution
and wraps them in a .str span. The quote pairing mirrors the lexer's
readString: no escapes, newlines allowed, unterminated runs to EOF.
Emote names still render as images inside strings, as before.

editorSource/forEachPiece/setCaretOffset already recurse into element
nodes, so the span wrapper needs no caret-math changes.

* feat(playground): line-number gutter with click-to-line

The editor gets a real text-editor gutter: numbers derived from the
source (never the DOM), vertical scroll mirrored from the editor,
horizontal scrolls left alone, and clicking a number places the caret
at that line's first character. applySnapshot updates the gutter too,
so undo/redo keep the count honest.

Two layout fixes make internal scrolling possible at all: main's grid
row is capped with minmax(0,1fr) and the wide layout gives body a
definite height, so long source scrolls inside the pane instead of
stretching the page and dragging the gutter away. The editor switches
from pre-wrap to pre for the same reason - a wrapped line would span
several rows and desynchronize the numbers.

* fix(playground): address self-review findings on the editor work

- pair strings the way the lexer does: #-to-EOL comment regions are
  carved out first, so a quote in a comment can no longer open a
  phantom string across real code; comments render muted, emotes
  inside them still render as images
- ignore gutter clicks on empty space below the last number instead
  of teleporting the caret to line 1
- resync gutter scroll after undo/redo restores, for engines that
  skip the scroll event when clamping scrollTop
- drop hack-bold.woff2: no stylesheet requests bold monospace

Found via the self-review posted on #23.
ohnishant added a commit that referenced this pull request Aug 23, 2026
…releases (#25)

* feat(playground): manual undo/redo for the editor

normalize() rebuilds the DOM after every input event, so the browser's
native undo history never survives; ctrl+z did nothing. Keep our own
stack instead of pulling in an editor component:

- snapshot commits land 400ms after the last mutation and store the
  previous {source, caretOffset}, so one step rewinds a typed burst
- ctrl/cmd+z undoes, ctrl/cmd+shift+z and ctrl+y redo; the combos are
  always swallowed since native undo would corrupt the rebuilt DOM
- programmatic replaces (autocomplete accept, example load) reset the
  stack, as sketched in #18
- depth capped at 200 snapshots

Closes #18

* fix(playground): guard undo commit against a missing baseline

Self-review found committed is only initialized because loadExample
runs synchronously at startup; make commitUndoPoint adopt the current
state instead of throwing if that init ever moves.

* feat(playground): vendor Hack as the editor font

The CSS already asked for JetBrains Mono and Fira Code but loaded
neither, so every visitor saw a fallback. Ship Hack woff2 (regular +
bold, MIT license included) with @font-face and put it first in the
mono stacks for editor, output, and autocomplete.

* feat(playground): highlight string literals in the editor (#21)

buildEditorFragment now splits quoted spans before emote substitution
and wraps them in a .str span. The quote pairing mirrors the lexer's
readString: no escapes, newlines allowed, unterminated runs to EOF.
Emote names still render as images inside strings, as before.

editorSource/forEachPiece/setCaretOffset already recurse into element
nodes, so the span wrapper needs no caret-math changes.

* feat(playground): line-number gutter with click-to-line

The editor gets a real text-editor gutter: numbers derived from the
source (never the DOM), vertical scroll mirrored from the editor,
horizontal scrolls left alone, and clicking a number places the caret
at that line's first character. applySnapshot updates the gutter too,
so undo/redo keep the count honest.

Two layout fixes make internal scrolling possible at all: main's grid
row is capped with minmax(0,1fr) and the wide layout gives body a
definite height, so long source scrolls inside the pane instead of
stretching the page and dragging the gutter away. The editor switches
from pre-wrap to pre for the same reason - a wrapped line would span
several rows and desynchronize the numbers.

* fix(playground): address self-review findings on the editor work

- pair strings the way the lexer does: #-to-EOL comment regions are
  carved out first, so a quote in a comment can no longer open a
  phantom string across real code; comments render muted, emotes
  inside them still render as images
- ignore gutter clicks on empty space below the last number instead
  of teleporting the caret to line 1
- resync gutter scroll after undo/redo restores, for engines that
  skip the scroll event when clamping scrollTop
- drop hack-bold.woff2: no stylesheet requests bold monospace

Found via the self-review posted on #23.

* fix(ci): only rebuild the pages site when its inputs change

* fix(ci): stop cutting releases on docs-only pushes to main

* fix(ci): skip tests on docs-only changes and add gofmt/vet job
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant