M4.6 — attach DiagnosticView to TUI windows (closes task #23) (#50)

The TUI's `DiagnosticView` has existed in `src/diag.rs` since v0.1 but
was never instantiated, so the local-grid renderer never painted
diagnostic underlines. This wires the view in the same way
`LspStyleView` and `SyntaxHighlightView` are wired — a Lua binding
that pushes the overlay onto the active window, driven from
`lsp.lua`'s `attach_buffer` flow with the standard per-buffer dedup
table.

* `DiagnosticView::kind()` returns `"diagnostic"` so
  `pmacs.window._overlay_kinds()` can verify attachment.
* `pmacs.diag._attach_view(buf, uri)` mirrors `pmacs.lsp._attach_style`
  exactly: requires active window's buffer matches `buf`, constructs
  `DiagnosticView::new(uri, store)`, pushes as overlay.
* `lsp.lua` calls `pmacs.diag._attach_view` from `attach_buffer` and
  tracks pushed buffers in `diag_viewed_buffers` to prevent
  double-attach on repeated `attach_buffer` calls.

Scope is intentionally narrow: view attachment only. Navigation
bindings, statusline summary, and gutter signs remain follow-ups
under task #23.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-21 21:10:36 +00:00 committed by GitHub
parent 8db0485839
commit c414954820
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 132 additions and 0 deletions

View File

@ -327,6 +327,12 @@ end
-- is keyed by the same `tostring(buf)` the attachments table uses.
local styled_buffers = {}
-- M4.6 (task #23): buffers that already had a `DiagnosticView`
-- overlay pushed. Same dedup discipline as `styled_buffers` —
-- `pmacs.diag._attach_view` stacks a fresh overlay on every call,
-- so the after-load path must gate itself.
local diag_viewed_buffers = {}
local function attach_buffer(buf)
if not buf then return nil end
local key = tostring(buf)
@ -361,6 +367,15 @@ local function attach_buffer(buf)
local ok, attached = pcall(pmacs.lsp._attach_style, buf)
if ok and attached then styled_buffers[key] = true end
end
-- M4.6 (task #23): attach the DiagnosticView so the TUI grid
-- renderer underlines bytes covered by published diagnostics.
-- Keyed by `uri` to match the diag store; the view re-reads the
-- store on every render, so no further wiring is needed when
-- diagnostics update.
if not diag_viewed_buffers[key] then
local ok, attached = pcall(pmacs.diag._attach_view, buf, uri)
if ok and attached then diag_viewed_buffers[key] = true end
end
return rec
end

View File

@ -409,6 +409,10 @@ impl DiagnosticView {
}
impl View for DiagnosticView {
fn kind(&self) -> &'static str {
"diagnostic"
}
fn render(&mut self, buf: &Buffer, viewport: Viewport, cells: &mut CellGrid<'_>) {
// Snapshot the diagnostics under the lock and drop it
// immediately so we don't hold the lock through rendering
@ -783,4 +787,13 @@ mod tests {
assert_eq!(s.gutter_glyph(), gl);
}
}
#[test]
fn view_advertises_diagnostic_kind() {
// `pmacs.window._overlay_kinds()` introspection (task #23 wire-up,
// mirroring "syntax-highlight" / LspStyleView) relies on this.
let store = make_shared_store();
let view = DiagnosticView::new("file:///a", store);
assert_eq!(view.kind(), "diagnostic");
}
}

View File

@ -8742,6 +8742,36 @@ pub fn install_diag(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
})?;
}
// Sibling of `pmacs.lsp._attach_style` and
// `pmacs.parse._attach_highlight`: pushes a `DiagnosticView`
// overlay on the active window keyed under `uri`, so the TUI
// grid renderer paints diagnostic underlines for buffers that
// have an LSP server publishing diagnostics. Lua callers dedup
// per buffer; double-attach stacks duplicate overlays.
{
let m = manager.clone();
diag_mod.set(
"_attach_view",
lua.create_function(move |lua, (id, uri): (BufferIdLua, String)| {
let store_handle = m.borrow().diag_store();
let core = lua
.app_data_ref::<SharedCore>()
.ok_or_else(|| mlua::Error::external("editor core not yet installed"))?;
let mut core_borrow = core.borrow_mut();
let win = core_borrow.active_window_mut();
if win.buffer_id != id.0 {
return Err(mlua::Error::external(format!(
"active window's buffer is not {:?}",
id.0
)));
}
let overlay = crate::diag::DiagnosticView::new(uri, store_handle);
win.push_overlay(Box::new(overlay));
Ok(true)
})?,
)?;
}
pmacs.set("diag", diag_mod)?;
Ok(())
}

View File

@ -1754,6 +1754,80 @@ fn m4_6_lua_surface_reads_diagnostics() {
);
}
/// Task #23: `pmacs.diag._attach_view` pushes a `DiagnosticView` onto
/// the active window's overlay stack so the TUI grid renderer paints
/// diagnostic underlines. Verifies the binding lands and that the
/// overlay advertises the stable `"diagnostic"` kind that callers
/// (`builtin/runtime/lsp.lua`'s dedup table, future tests) key on.
#[test]
fn m4_6_diag_attach_view_pushes_diagnostic_overlay() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let attached: bool = state
.lua_host
.lua()
.load(
r"
local buf = pmacs.window.buffer()
return pmacs.diag._attach_view(buf, 'file:///fake.rs')
",
)
.eval()
.expect("_attach_view");
assert!(attached, "_attach_view should return true on success");
let has_diag_overlay: bool = state
.lua_host
.lua()
.load(
r"
for _, k in ipairs(pmacs.window._overlay_kinds()) do
if k == 'diagnostic' then return true end
end
return false
",
)
.eval()
.expect("overlay_kinds query");
assert!(
has_diag_overlay,
"active window must carry a 'diagnostic' overlay after _attach_view"
);
// Mirroring `_attach_style` / `_attach_highlight`: the binding
// itself does not dedup; callers (lsp.lua's `diag_viewed_buffers`)
// are responsible. Calling twice stacks two overlays.
state
.lua_host
.lua()
.load(
r"
pmacs.diag._attach_view(pmacs.window.buffer(), 'file:///fake.rs')
",
)
.exec()
.expect("second attach");
let diag_count: i64 = state
.lua_host
.lua()
.load(
r"
local n = 0
for _, k in ipairs(pmacs.window._overlay_kinds()) do
if k == 'diagnostic' then n = n + 1 end
end
return n
",
)
.eval()
.expect("overlay count");
assert_eq!(
diag_count, 2,
"binding does not dedup; two calls = two overlays"
);
}
// ===========================================================================
// T M4.7 --- LSP-backed views: completion, hover, signature
// ===========================================================================