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:
commit
6979ab3006
190
README.md
190
README.md
|
|
@ -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)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: ~5–8 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.
|
||||||
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue