Merge pull request #91 from levineuwirth/fix-gpu-gutter-correctness

fix(gpu): gutter click classification + fit guard; test v14 wire round-trip
This commit is contained in:
Levi Neuwirth 2026-07-07 11:06:40 -04:00 committed by GitHub
commit 6979ab3006
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 383 additions and 52 deletions

190
README.md
View File

@ -10,23 +10,113 @@ discards the single-threaded substrate; workers, message bus, and a
coroutine-based async surface are core primitives, not bolt-ons. coroutine-based async surface are core primitives, not bolt-ons.
The editor is partitioned into a long-lived **instance** (the daemon The editor is partitioned into a long-lived **instance** (the daemon
that owns buffers, processes, and language services) and a thin that owns buffers, processes, and language services) and thin
**frontend** that attaches over a typed protocol. Frontends can run **frontends** that attach over a typed protocol (currently v14). Two
locally over a Unix socket or remotely over SSH; reconnect-on-drop frontends ship today:
modeled on `mosh` keeps remote sessions alive across laptop suspends.
The first-class package is a **REPL package** written entirely against - a **TUI** (crossterm cell grid), attachable locally over a Unix
the public Lua API: PTY-spawned shells (bash, zsh, fish, lua), an socket or remotely over SSH, with reconnect-on-drop modeled on
ECMA-48 ANSI parser, multi-REPL coexistence, and scrollback management `mosh`; and
with line/byte retention. Successful completion of an audit verifying - **pmacs-gpu**, a GPU frontend (wgpu + winit + glyphon) that renders
the package uses zero direct Rust core access was the v0.1 ship gate. from a *semantic projection* of editor state --- style spans,
decorations, inlay adornments --- rather than a character grid, and
edits optimistically against a local CRDT replica for
latency-free typing.
Buffers are optionally CRDT-backed (`loro`, behind `--features crdt`),
so multiple frontends --- TUI and GPU, local and remote --- can edit
the same buffers concurrently with live cursor/selection presence.
## Status ## Status
**v1.0.0 --- stable.** The design described above is implemented and **v1.0.0 --- stable core, active development.** The v1.0 gate (the
working. Solo development carried the project to 1.0; public instance/frontend partition, the Lua surface, and a REPL package
contributions are open from this release. Use, evaluate, file issues, audited to use zero direct Rust core access) shipped some time ago;
and send pull requests. development since has landed the GPU frontend at near input/render
parity with the TUI, the semantic-frontend protocol (v6 → v14), the
LSP feature arc, in-buffer search, the context menu + OS clipboard,
the line-number gutter with diagnostic signs, and the package-manager
hardening pass. Current direction lives in `docs/roadmap-2026-07.md`.
Public contributions are open: use, evaluate, file issues, send pull
requests.
## Highlights
**Editing & UI.** CUA-style region editing plus Emacs kill/yank
bindings; linear undo/redo; incremental search, substring and regex
(`C-s` / `C-r` / `C-M-s`); line-number gutter with absolute, relative,
and hybrid modes; diagnostic gutter signs; right-click context menu;
OS clipboard integration (OSC 52 in the TUI, native in the GPU);
minibuffer with completion dropdown and per-bucket persisted history;
buffer-list mode (`C-x C-b`); self-navigable help system
(`describe-command`, `describe-key`); atomic saves (temp + rename +
parent fsync, mode-preserving).
**Language intelligence.** LSP client with async, never-blocking
requests: diagnostics (severity-colored underlines/squiggles, gutter
signs, statusline counts, `M-g n`/`M-g p` navigation), rename with
`prepareRename`, go-to-definition including cross-file navigation,
hover, signature help, references, document symbols, code actions,
buffer formatting, semantic tokens, and inlay hints (rendered inline
in the GPU frontend). Servers are preconfigured for rust-analyzer,
clangd (C/C++), basedpyright, gopls, typescript-language-server
(TS/TSX/JS/JSX), lua-language-server, bash-language-server, taplo
(TOML), and zls (Zig). Syntax highlighting is dual-authority:
bundled tree-sitter grammars (Rust, Lua, Markdown, C, C++) paint
lexical structure and LSP semantic tokens refine it --- languages
without a bundled grammar still get full semantic coloring. A
persistent project symbol index (`.pmacs/index.json`) rides the same
worker infrastructure.
**Collaboration & frontends.** With `--features crdt`, buffers are
CRDT-backed and any number of frontends attach to one daemon and edit
concurrently; peers see each other's cursors and selections as
translucent washes. The GPU frontend adds a live minimap (click to
jump, drag to scrub), wavy diagnostic squiggles, a status band with
live diagnostic counts, and optimistic local editing that rebases
in-flight edits through authoritative frames.
**Extensibility.** ~37 `pmacs.*` Lua namespaces cover buffers,
windows, commands, keymaps (global/mode/buffer scope), hooks, themes
(truecolor-capable syntax palette), tree-sitter, LSP stores, async
workers, and a PTY-aware process supervisor with an ECMA-48 ANSI
parser. A package manager installs from git (`github:owner/repo`,
version/branch/commit pins) with transitive dependency resolution and
a SHA-256 lockfile. Pmacs is also an **MCP client**: packages can
spawn MCP servers and consume their tools, resources, and prompts ---
AI integrations are packages over a transport, not a built-in
feature. The bundled REPL package (PTY shells, ANSI rendering,
multi-REPL, scrollback retention) is written entirely against the
public Lua API.
## Running
Single-process TUI:
```sh
pmacs [FILE] # TUI; -nw reserved for when a GUI default lands
```
Daemon + attached frontends (build with `--features crdt` for
multi-frontend editing and the GPU frontend):
```sh
pmacs --daemon --socket NAME # foreground daemon; bare NAME →
# <runtime>/pmacs/NAME.sock
pmacs --attach --socket NAME # TUI frontend; F12 detaches
pmacs --attach user@host # remote TUI over SSH
pmacs-gpu --attach /run/user/$UID/pmacs/NAME.sock # GPU frontend
```
`pmacs --attach` also understands `ssh:user@host/instance`,
`local:/path.sock`, and bare hostnames (treated as SSH). See
`pmacs --help` for the full matrix.
User configuration is plain Lua at
`$XDG_CONFIG_HOME/pmacs/init.lua` (default `~/.config/pmacs/init.lua`),
loaded after the builtin runtime so plain assignments override
defaults --- keybindings, `pmacs.lsp.config`, theme overrides, and
package installs all live there.
## Build ## Build
@ -34,7 +124,9 @@ Builds on the toolchain pinned in `rust-toolchain.toml` (Rust
`1.95.0`, edition 2024); rustup selects it automatically. `1.95.0`, edition 2024); rustup selects it automatically.
```sh ```sh
cargo build --release # produce target/release/pmacs (LuaJIT) cargo build --release # target/release/pmacs (LuaJIT flavor)
cargo build --release --features crdt # + CRDT buffers (daemon use)
cargo build --release -p pmacs-gpu # the GPU frontend binary
cargo run --release -- <file> # build and run on a file cargo run --release -- <file> # build and run on a file
cargo test --workspace # unit + integration tests (all crates) cargo test --workspace # unit + integration tests (all crates)
cargo fmt --check cargo fmt --check
@ -76,7 +168,11 @@ CI runs the matrix on every push.
Release-only perf gates (M5 keystroke-to-render, M6 ingest/RSS/cancel Release-only perf gates (M5 keystroke-to-render, M6 ingest/RSS/cancel
and scrollback navigation/search) are `#[ignore]`'d during normal and scrollback navigation/search) are `#[ignore]`'d during normal
test runs and exercised in CI under dedicated jobs. test runs and exercised in CI under dedicated jobs. The GPU frontend
has headless render tests (offscreen wgpu, pixels read back) that run
in CI under lavapipe and skip gracefully on machines without a Vulkan
adapter (`PMACS_REQUIRE_GPU=1` turns a missing adapter into a hard
failure).
## Runtime requirements ## Runtime requirements
@ -125,59 +221,53 @@ The Lua VM (LuaJIT or Lua 5.4) is statically vendored via `mlua`'s
`vendored` feature, so there is no external Lua dependency at `vendored` feature, so there is no external Lua dependency at
runtime. runtime.
## What v0.1 ships with The GPU frontend additionally needs a Vulkan-capable driver stack
(any real GPU driver, or lavapipe for software rendering); its font
- **Editor core.** Persistent rope with O(log N) edits and snapshots; (JetBrains Mono, OFL-licensed) is bundled into the binary.
buffers with chained intercept-views; undo/redo; atomic file I/O;
crossterm-driven TUI.
- **Lua surface.** Embedded LuaJIT (or Lua 5.4) with `pmacs.command`,
`pmacs.keymap` (global / mode / buffer scopes), `pmacs.hook`
(typed kinds: all-must-succeed, first-non-nil, last-write-wins),
`pmacs.buffer`, `pmacs.window`, `pmacs.editor`. Minibuffer is itself
a buffer. `describe-key` and `describe-command` for self-introspection.
- **Async runtime.** Worker pool + message bus + coroutine-based Lua
async surface (`pmacs.async`). Cancellation is provably correct
under load.
- **Language services.** Tree-sitter highlighting and LSP integration
ride the worker/message infrastructure. Project indexing as a third
service. Symbol search across 1M+ symbols completes under a second.
- **Frontend partition.** Daemon mode with local Unix-socket transport;
cell-delta diffing on the instance side; SSH transport variant for
remote attach; reconnect-on-drop preserves session state across
laptop suspend / network drop.
- **REPL package.** A 691-line Lua package that wires the M6 ANSI
parser to PTY-spawned shells with raw-mode line discipline. Three-
region buffer (history / prompt / input) with read-only enforcement;
RET / C-c / C-d bindings; multi-REPL coexistence; scrollback
retention with line- and byte-bounded truncation. Published
alongside an audit verifying zero direct Rust core access.
## Layout ## Layout
The workspace has three first-party crates:
``` ```
src/ Rust core src/ pmacs — the core + TUI + daemon
rope.rs persistent byte-sequence backing every buffer rope.rs persistent byte-sequence backing every buffer
buffer.rs buffer + view chain + undo/redo buffer.rs buffer + view chain + undo/redo
editor_core.rs cursor + commands + edit dispatch editor_core.rs cursor + commands + edit dispatch
crdt.rs loro-backed CRDT buffer state (feature `crdt`)
daemon.rs instance side of the frontend partition
attach.rs frontend side; transports + reconnect
semantic_render.rs semantic-frame producer (StyleSpans, Decorations, …)
lsp.rs language-server client
diag.rs, highlight.rs diagnostic + syntax/semantic-token rendering
syntax.rs tree-sitter integration
search.rs incremental search (substring + regex)
minibuffer.rs prompt, completion, persisted history
menu.rs context-menu model
file_io.rs atomic saves + external-modification detection
async_runtime.rs worker pool + message bus async_runtime.rs worker pool + message bus
process.rs PTY-aware process supervisor process.rs PTY-aware process supervisor
ansi.rs ECMA-48 parser ansi.rs ECMA-48 parser
daemon.rs instance side of the frontend partition project.rs, project_index.rs project detection + symbol index
attach.rs frontend side; protocol + reconnect packages/ resolver, fetcher, installer, lockfile, loader
lsp.rs language-server client mcp.rs MCP client (packages speak to MCP servers)
syntax.rs tree-sitter integration lua_bindings/ pmacs.* Lua surface installers
project_index.rs symbol / file indexing
text_view.rs cell-grid renderer text_view.rs cell-grid renderer
frontend.rs crossterm TUI frontend.rs crossterm TUI
lua_bindings.rs pmacs.* Lua surface installers main.rs entry point (TUI / daemon / attach modes)
main.rs entry point (TUI + daemon modes)
pmacs-protocol/ wire types + framing codec shared by all frontends
pmacs-gpu/ the GPU frontend (wgpu + winit + glyphon)
builtin/ Lua runtime shipped with the binary builtin/ Lua runtime shipped with the binary
commands/default.lua named commands for every editor primitive commands/default.lua named commands for every editor primitive
keymaps/default.lua default key bindings keymaps/default.lua default key bindings
hooks/default.lua built-in hook definitions hooks/default.lua built-in hook definitions
runtime/ packages (async, lsp, repl, syntax) menus/default.lua context-menu items
runtime/ async, lsp, syntax, mcp, fs runtimes
packages/repl/ the bundled REPL package
docs/ design notes, framing docs, and the roadmap
tests/ integration tests (acceptance gates per milestone) tests/ integration tests (acceptance gates per milestone)
``` ```

135
docs/roadmap-2026-07.md Normal file
View File

@ -0,0 +1,135 @@
# pmacs roadmap — July 2026
Date: 2026-07-07. Produced from a five-way codebase/docs/memory sweep
(core editing + persistence, LSP surface, GPU parity, extensibility +
terminal, deferred-work inventory across all framing docs).
**Decision (2026-07-07): push Arc 1 (LSP utility surface), with Arc 2
(editing table stakes) items interleaved between sub-arcs.**
---
## State assessment (as of main @ ccb0ff6, protocol v14)
**Strong.** CRDT multi-frontend core; both frontends at near input/render
parity (GPU exceeds TUI: minimap, wavy squiggles); LSP data layer — 13
language ids / ~8 servers, rename + cross-file definition + formatting
fully working, semantic tokens + inlay hints rendering; package manager
(git installs, lockfile, resolver, SHA-256 hardening); MCP client; async
worker + PTY subprocess substrate; bundled line-oriented REPL package;
headless GPU render CI; help system; buffer-list UI; atomic save;
persisted minibuffer history.
**Dark matter — built but unwired (highest leverage).**
- Complete completion framework (`src/completion_framework.rs`:
providers for lsp/snippets/project-symbols/dabbrev; store;
`CompletionView` popup in `src/completion.rs`) — **no keybinding, no
typed-char trigger, popup never instantiated, no GPU message.**
- `HoverView` (`src/hover.rs`), `SignatureView` (`src/signature.rs`),
document-highlight store — plumbing only. Hover / signature /
references / document-symbols surface as one-line modeline summaries
(`builtin/runtime/lsp.lua` marks each "future UX work").
- Code actions apply the **first** action blindly — no picker.
- Semantic-token pull is refresh/manual-only (no pull on attach/edit) —
semantic styling can silently never appear. Arguably a bug.
- Wire-declared but unproduced protocol families: `FoldState`,
`BlockAdornments`, `ResourceOffer` (no fold engine / blame / diff
source).
**Absent.** Query-replace, kill ring (single slot only), keyboard
macros, rectangles, registers, comment/uncomment, auto-indent,
snippets, auto-pairing; desktop-save/session restore, recentf,
saveplace, autosave, backups, crash recovery; theming beyond syntax
captures (all chrome hardcoded per-frontend; GPU font hardcoded);
terminal emulation (`src/ansi.rs` parses but discards alt-screen /
cursor addressing — no grid/scrollback); compile/grep/shell-command
modes; DAP debugging; GPU splits / multi-buffer / auto-reconnect.
---
## Arcs, ranked by value-per-effort
### Arc 1 — LSP utility surface: "light up the dark matter" ← ACTIVE
Data layer is done; only UI is missing.
- **1a. In-buffer completion popup** (first). Trigger on typing,
TAB/RET accept, both frontends. GPU needs a wire message — mirror the
minibuffer-dropdown pattern (protocol v12). Framework + popup view
already exist.
- **1b. Panels**: hover popup, code-action picker, references list,
document-symbol outline. Generalize the buffer-list UI pattern
(`*buffer-list*` buffer-local bindings) into a reusable list-buffer
idiom.
- **1c. Semantic-token auto-pull fix** (small): pull on attach + on
edit-flush, like inlay hints already do.
- **1d. Signature-help auto-trigger** on `(`.
### Arc 2 — Editing table stakes (interleave with Arc 1)
Each small, core-only, frontend-agnostic: query-replace (isearch
exists; `search.rs` has no replace API), real kill ring + `M-y`,
comment/uncomment, auto-indent on newline, auto-pairing.
### Arc 3 — Persistence/serialization
Desktop-save (buffer set + layout + cursors → restore), recentf,
saveplace, autosave + crash recovery, optional backups. Generalize the
`$XDG_STATE_HOME/pmacs/` pattern from minibuffer history. Framing
question: what is a "session" in a daemon world; do CRDT snapshots
ride along.
### Arc 4 — Themes + extensibility surface
Extend `pmacs.theme` from syntax captures to named UI faces (modeline,
minibuffer, gutter, selection, status band); wire GPU chrome to it —
Q#UX1 lesson applies: rendering is frontend-local but control is
daemon-owned, so a wire channel (`ThemeFacts`-style) is needed. Add
`pmacs.gpu.set_font` (designed, never built) and a Lua
statusline-segment API.
### Arc 5 — Terminal, staged
- **Stage 1**: compile-mode / grep-mode / shell-command on the existing
PTY + ANSI + REPL-package substrate (line-oriented output buffer,
error-regex jump-to-file, `M-x compile`). Cheap, transformative.
- **Stage 2 (vterm)**: extend `ansi.rs` into a 2D grid model
(alt-screen, cursor addressing, scrollback — parser already
recognizes and discards these), grid-backed buffer view, GPU
rendering question (grid cells vs text buffer).
### Arc 6 — Folding (keystone gutter rider)
Fold engine (tree-sitter fold ranges) unblocks gutter fold markers +
the unproduced `FoldState` wire family + a visible feature. Git gutter
markers similarly just need a diff source.
### Arc 7 — Debugging (DAP)
Greenfield but newly unblocked: gutter (breakpoint signs), process
substrate (DAP = JSON-RPC over stdio, same shape as the LSP client),
list-buffer panels from Arc 1b (stack/variables). Frame after Arc 1
ships — panels are reused here.
### Arc 8 — GPU structural parity
Splits / multi-buffer (largest unscoped design problem), auto-reconnect
after daemon restart, cursor blink, font/theme config (overlaps Arc 4).
---
## Housekeeping (do opportunistically)
- Merge PR #91 (gutter click classification + fit guard).
- Delete or mark-superseded stale docs: `pmacs-gpu-editing-perf-handoff.md`
(freeze fixed by PR #60 arc), `session-5-stale-styling-handover.md`
(fixed, task #25 closed), `python_experiment.md`, `#run.sh#`,
`semantic-frontend-protocol.md.local-bak`.
- `V0.2-PREREQUISITES.md` cited twice by CHANGELOG but missing on disk —
reconstruct or unlink.
- README still leads with "What v0.1 ships with" — refresh.
- F-016 lua_bindings split: ~58 tranches left; `editor.rs` (7k lines)
and `pmacs-gpu/main.rs` (7.6k lines) splits are named follow-up arcs.
- Deferred backlogs live in each framing doc's Deferred section; the
consolidated sweep is reflected above.

View File

@ -102,6 +102,11 @@ const GUTTER_MONO_ADVANCE_FALLBACK: f32 = 9.6;
/// `W` its width; it spans the full line height. /// `W` its width; it spans the full line height.
const GUTTER_SIGN_X: f32 = 4.0; const GUTTER_SIGN_X: f32 = 4.0;
const GUTTER_SIGN_W: f32 = 4.0; const GUTTER_SIGN_W: f32 = 4.0;
/// Minimum text-area width (px) the gutter must leave. If reserving the
/// gutter would crowd the text below this, the gutter is dropped for the
/// frame — the GPU mirror of the TUI's too-narrow-window disable, so a
/// narrow window or a very large file can never force `left >= right`.
const MIN_TEXT_WIDTH_PX: f32 = 48.0;
const MINIMAP_BG: [f32; 4] = [0.075, 0.075, 0.105, 0.92]; const MINIMAP_BG: [f32; 4] = [0.075, 0.075, 0.105, 0.92];
const MINIMAP_DEFAULT_LINE: [f32; 4] = [0.23, 0.23, 0.29, 0.82]; const MINIMAP_DEFAULT_LINE: [f32; 4] = [0.23, 0.23, 0.29, 0.82];
const MINIMAP_THUMB_FILL: [f32; 4] = [0.82, 0.82, 0.92, 0.18]; const MINIMAP_THUMB_FILL: [f32; 4] = [0.82, 0.82, 0.92, 0.18];
@ -2818,7 +2823,19 @@ impl State {
return 0.0; return 0.0;
} }
let lines = self.current_line_starts.len().max(1); let lines = self.current_line_starts.len().max(1);
decimal_digits(lines) as f32 * self.mono_advance() + GUTTER_GAP_PX let want = decimal_digits(lines) as f32 * self.mono_advance() + GUTTER_GAP_PX;
// Fit guard (mirrors the TUI's too-narrow disable): never reserve so
// much gutter that the text area collapses. `text_bounds_right` is
// the text clip edge (minimap-aware); if the gutter would leave less
// than `MIN_TEXT_WIDTH_PX` past `TEXT_LEFT`, drop it this frame
// rather than shift `text_left` to or past the clip and render into
// a degenerate `left >= right` rectangle.
let avail = self.text_bounds_right() as f32 - TEXT_LEFT;
if want + MIN_TEXT_WIDTH_PX > avail {
0.0
} else {
want
}
} }
/// The code's left origin in px: `TEXT_LEFT` plus the gutter. Every /// The code's left origin in px: `TEXT_LEFT` plus the gutter. Every
@ -2882,6 +2899,22 @@ impl State {
/// line) → projected byte → run map → slice byte → + `vstart`. /// line) → projected byte → run map → slice byte → + `vstart`.
/// `None` when no buffer is attached or the position is outside /// `None` when no buffer is attached or the position is outside
/// anything hit-testable. /// anything hit-testable.
/// Text-relative x for hit testing, classifying the gutter band first
/// (UX gutter, Q#UX6). A click left of the text origin (`raw_x < 0`,
/// i.e. inside the gutter) is not a text hit — it clamps to `0.0`, the
/// line start, rather than feeding glyphon a negative x (undefined).
/// Mirrors the TUI's saturate-to-column-0 affordance and is the stable
/// seam a future gutter marker would branch on instead of relying on
/// glyphon's negative-x edge behavior.
fn gutter_aware_rel_x(&self, x: f64) -> f32 {
let raw_x = x as f32 - self.text_left();
if self.line_numbers.is_on() && raw_x < 0.0 {
0.0
} else {
raw_x
}
}
fn hit_test_source_byte(&mut self, x: f64, y: f64) -> Option<u64> { fn hit_test_source_byte(&mut self, x: f64, y: f64) -> Option<u64> {
self.current_buffer_id?; self.current_buffer_id?;
if self.hit_map_dirty { if self.hit_map_dirty {
@ -2900,7 +2933,7 @@ impl State {
self.projected_line_starts = projected_line_starts; self.projected_line_starts = projected_line_starts;
self.hit_map_dirty = false; self.hit_map_dirty = false;
} }
let rel_x = x as f32 - self.text_left(); let rel_x = self.gutter_aware_rel_x(x);
let rel_y = y as f32 - TEXT_TOP; let rel_y = y as f32 - TEXT_TOP;
let cursor = self.buffer.hit(rel_x, rel_y)?; let cursor = self.buffer.hit(rel_x, rel_y)?;
let line_start = *self.projected_line_starts.get(cursor.line)?; let line_start = *self.projected_line_starts.get(cursor.line)?;
@ -7517,4 +7550,55 @@ mod tests {
"relative numbering must differ from absolute ({differing} bytes differ)" "relative numbering must differ from absolute ({differing} bytes differ)"
); );
} }
#[test]
fn gutter_aware_rel_x_clamps_the_gutter_band() {
// F1: a click in the gutter band (left of the text origin) clamps
// to the line start (rel_x 0), never a negative x into glyphon.
let text = "alpha\nbeta\ngamma\n";
let Some(mut s) = headless_or_skip(400, 300, text) else {
return;
};
s.line_numbers = LineNumberMode::Absolute;
let text_left = f64::from(s.text_left());
assert!(text_left > f64::from(TEXT_LEFT), "the gutter is present");
// Inside the gutter band and at the exact origin → clamped to 0.
assert!(s.gutter_aware_rel_x(text_left - 4.0).abs() < f32::EPSILON);
assert!(s.gutter_aware_rel_x(text_left).abs() < f32::EPSILON);
// Well into the text → a positive text-relative x.
assert!(s.gutter_aware_rel_x(text_left + 40.0) > 0.0);
// With the gutter off there's no band, so a left-of-origin x passes
// through negative (the pre-gutter behavior is unchanged).
s.line_numbers = LineNumberMode::Off;
assert!(s.gutter_aware_rel_x(f64::from(TEXT_LEFT) - 4.0) < 0.0);
}
#[test]
fn narrow_window_drops_the_gutter() {
// F2: a window too narrow to fit the gutter + a minimum text area
// drops the gutter for the frame (no `left >= right`), mirroring the
// TUI. A wide window keeps it.
let text = "l1\nl2\nl3\n";
let Some(mut narrow) = headless_or_skip(60, 200, text) else {
return;
};
narrow.line_numbers = LineNumberMode::Absolute;
assert!(
narrow.gutter_width_px() < f32::EPSILON,
"a 60px window can't fit gutter + min text → gutter dropped"
);
assert!(
(narrow.text_left() - TEXT_LEFT).abs() < f32::EPSILON,
"text origin unshifted when the gutter is dropped"
);
let mut wide = State::new_headless(800, 200, text).expect("adapter was just available");
wide.line_numbers = LineNumberMode::Absolute;
assert!(
wide.gutter_width_px() > 0.0,
"a wide window keeps the gutter"
);
}
} }

View File

@ -2769,6 +2769,28 @@ mod tests {
} }
} }
#[test]
fn line_numbers_all_modes_round_trip_through_postcard() {
// UX gutter v14: `LineNumbers` swapped `enabled: bool` for a
// `LineNumberMode` enum. Pin every variant's postcard shape so a
// future enum reorder / addition can't silently change the wire.
let bid = crate::buffer::BufferId::next();
for mode in [
LineNumberMode::Off,
LineNumberMode::Absolute,
LineNumberMode::Relative,
LineNumberMode::Hybrid,
] {
let msg = InstanceMessage::LineNumbers {
buffer_id: bid,
mode,
};
let bytes = postcard::to_allocvec(&msg).expect("encode");
let decoded: InstanceMessage = postcard::from_bytes(&bytes).expect("decode");
assert_eq!(msg, decoded);
}
}
#[test] #[test]
fn frontend_event_viewport_round_trips_through_postcard() { fn frontend_event_viewport_round_trips_through_postcard() {
let ev = FrontendEvent::Viewport { let ev = FrontendEvent::Viewport {