8.9 KiB
LSP panels — framing (Arc 1b)
pmacs's LSP data layer answers references, document symbols, code
actions, and hover in full — and then renders one line in the
modeline. lsp.find-references reports "12 references" and throws
eleven away; lsp.code-actions applies acts[1] blind;
lsp.document-symbols prints a count; multi-line hover shows its
first line. Every one of these is flagged "future UX work" in
builtin/runtime/lsp.lua. This arc builds that UX — as buffers,
not new UI surfaces, so both frontends get it for free.
Roadmap context: docs/roadmap-2026-07.md Arc 1b (also the panel
substrate DAP will reuse). Direct trigger: the PR #93 validation note
("we probably want a more fine grained UI for this sort of stuff and
error checking").
What already exists (verified)
- The
*buffer-list*idiom (builtin/commands/default.lua:320-545) is a working panel: a named persistent buffer, wholesale re-rendered (buf:delete+buf:insert), buffer-local keymap (scope = "buffer"binds, installed once, never torn down), aline_to_bufferrow→item map read viapmacs.editor.cursor_line(),prev_buffer_idcapture +qrestore, and a refresh that manually re-seats the cursor (becauseswitch_active_bufferzeroes cursor/overlays —editor_core.rs:2052-2071). Gaps: no read-only enforcement, logic not reusable. - Read surfaces are uniform
pmacs.<domain>.{getter, clear}(sid, uri)over Rust stores: references =pmacs.references.locations(rows{uri, line, col}, 0-based, the byte==UTF-16 v0.1 caveat); outline =pmacs.document_symbol.symbols(flat rows withdepth+ optionalcontainer— indent, don't recurse); actions =pmacs.code_action.actions(the full ordered array; each item carries its ownedit/command, so applying by index is pure Lua); hover =pmacs.hover.current(contentsis the full multi-line string). - The cross-file visit template is
go_to_definition's SP-4 path (lsp.lua:1187-1213):push_jump→path_for_uri→find_or_open→move_active_cursor_to. - The picker substrate is
pmacs.minibuffer.read { source = function() return {...} end, on_accept }— thewindow.set-line-numbersshape (default.lua:231-246), already dual-frontend since protocol v12. - Read-only is intercept-only (no buffer flag exists): the REPL's
add_intercept+error()+ self-write bypass (repl/init.lua:686-726) is the reference implementation.
Decisions
Q#P1 — Panels are buffers; one shared listview runtime module
builtin/runtime/listview.lua:
listview.open {
name = "*references*", -- persistent buffer, found-or-created
header = "12 references to `foo` RET visit n/p move g refresh q quit",
rows = { { text = "...", item = <any> }, ... },
on_visit = function(item) ... end, -- RET/SPC
on_refresh = function() return rows end, -- g (optional)
}
It owns everything the buffer-list hand-rolls: ensure-buffer,
wholesale render, line→item map, buffer-local keymap (RET/SPC
visit, n/p + arrows move, g refresh, q quit), prev-buffer capture +
restore, cursor re-seat after render, and the read-only intercept
(Q#P3). Bindings install once per panel buffer and persist — the
buffer-list precedent; per-open teardown buys nothing for persistent
buffers. *buffer-list* itself is NOT migrated in this arc (working
code, separate risk); named as a follow-up.
Q#P2 — Presentation: switch-in-place, q restores
Panels open in the current window via pmacs.window.switch_buffer;
q restores the saved previous buffer (scratch fallback if killed).
This is the only presentation that behaves identically in both
frontends today — the GPU renders exactly one buffer and cannot show
splits. TUI split placement (display-buffer-style) is a named
deferral, not a v1 behavior fork.
Q#P3 — Read-only via intercept; its honest limits
Each panel buffer gets an add_intercept that error("read-only panel")s every op; listview's own renders write with
{ bypass_intercept = true }. Two recorded limits: (a) the intercept
guards the daemon's command/edit path — a CRDT-import write (a
semantic frontend's optimistic op) does not run the intercept chain;
Q#P6's round-trip seam is what actually keeps GPU typing out of
panels. (b) A real read_only buffer flag (enforced in both edit
paths, shipped to frontends) is deferred — it's the proper fix for
this AND the REPL's identical exposure, and deserves its own small
arc.
Q#P6 — Panel input routing: round-trip buffers (the load-bearing catch)
RET on a panel row must visit, but in the GPU, Enter is
optimistic-eligible — it would locally insert \n into the panel
mirror and ship a CrdtOp, never reaching the buffer-local RET
binding. Same for plain chars (vandalizing the panel around the
intercept, per Q#P3a).
Fix, wire-free: a core-side set of round-trip buffers.
pmacs.buffer.set_round_trip_input(buf, true) marks a buffer;
EditorState::dispatch_idle() returns false while the active
buffer is marked. DispatchIdle { idle: false } already gates every
semantic frontend's optimistic path (M11.6), so with a panel focused,
every GPU key round-trips: RET dispatches into the buffer-local
binding (visit works), typing dispatches to self-insert and the
intercept rejects it cleanly. No protocol change, no GPU change.
listview marks every panel it creates. (The REPL can adopt the same
flag later for its GPU story — named follow-up.)
Q#P4 — References list (*references*, the flagship)
lsp.find-references (M-?) keeps its request/store flow, then
opens a panel: one row per location, path:line+1:col+1 with the
path shortened relative to the project root when possible. RET =
the SP-4 visit template (jump ring included), so M-, returns.
Modeline summary stays (it's now the panel's byline). Row snippets
(the referenced line's text) require reading target files that may
not be open — deferred, named.
Q#P5 — Outline (*outline*) + code-action picker + hover doc
- Outline:
lsp.document-symbols(C-c o) → panel rows indented2 × depthwith a kind tag; RET restores the source buffer and moves to the symbol (same-file visit:switch_buffer(prev)+move_active_cursor_to). - Code actions: no panel needed —
lsp.code-actions(C-c a) openspmacs.minibuffer.readwith"N: title"candidates (index prefix disambiguates duplicate titles);on_acceptapplies the chosen item'sedit/commandthrough the existing apply branch. One action still auto-applies without prompting? No — always prompt when more than one; a single action applies directly (today's behavior, now correct instead of lucky). - Hover doc:
lsp.hover(C-c h) unchanged (first line in the echo area). Newlsp.hover-doc(C-c H) renders the fullcontentsinto a*lsp-help*panel (rows non-visitable; q quits).
Q#P7 — Coordinates
Panels inherit the v0.1 byte==UTF-16 assumption exactly as
go_to_definition does today (lsp.lua:658-662); the v0.2
position-encoding hardening remains one deferral, not four new ones.
Phasing (each phase green + user-validated)
listviewmodule + Q#P6 round-trip seam + references list. The seam is the only Rust change (core set + one binding +dispatch_idleclause + tests). Validate in TUI and GPU — phase 1's GPU validation is what scores bet #3.- Outline + code-action picker + hover doc. Pure Lua on the phase-1 substrate.
- As-built notes + deferral ledger update.
Arc 2 interleave point (query-replace) after phase 2.
Categorical bets (score at close)
- The listview module covers all three panels without per-panel Rust. The whole arc lands with one small core seam (Q#P6) and zero protocol change.
- Cursor-zeroing bites once.
switch_active_bufferclears cursor/overlays; some flow (refresh, visit-then-return) will land the cursor somewhere surprising before the re-seat discipline is applied everywhere. - GPU panels render via the existing BufferSnapshot/F29 path with
no GPU changes. The mechanism exists (mid-session CRDT upgrade +
snapshot push); a panel buffer exercising it from a
switch_bufferis the untested claim in this arc. - Round-trip routing has one gap somewhere — a key that neither round-trips nor falls through correctly while a panel is focused (the Q#C6-class finding of this arc).
Deferred (named, not silently dropped)
- A real
read_onlybuffer flag enforced on both edit paths and shipped to frontends (fixes panels + REPL properly; Q#P3). - Migrating
*buffer-list*ontolistview. - TUI split placement for panels (
display-buffer-style). - Reference row snippets (needs off-buffer file reads).
- Fuzzy filtering inside panels (type-to-narrow).
- Peek/preview on n/p (echo the target line without visiting).
- Call-hierarchy / workspace-symbols panels (data partially present; same idiom when wanted).
- REPL adopting the Q#P6 round-trip flag for its GPU story.