From c414954820ac1d81168d1b845b064e5f9f0f7454 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 21 May 2026 21:10:36 +0000 Subject: [PATCH] =?UTF-8?q?M4.6=20=E2=80=94=20attach=20DiagnosticView=20to?= =?UTF-8?q?=20TUI=20windows=20(closes=20task=20#23)=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- builtin/runtime/lsp.lua | 15 +++++++++ src/diag.rs | 13 ++++++++ src/lua_bindings.rs | 30 +++++++++++++++++ tests/m4_acceptance.rs | 74 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/builtin/runtime/lsp.lua b/builtin/runtime/lsp.lua index 8161e7f..3e72069 100644 --- a/builtin/runtime/lsp.lua +++ b/builtin/runtime/lsp.lua @@ -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 diff --git a/src/diag.rs b/src/diag.rs index e053fe0..29548db 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -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"); + } } diff --git a/src/lua_bindings.rs b/src/lua_bindings.rs index 3b6c148..334ef3f 100644 --- a/src/lua_bindings.rs +++ b/src/lua_bindings.rs @@ -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::() + .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(()) } diff --git a/tests/m4_acceptance.rs b/tests/m4_acceptance.rs index 08af3df..82c9e12 100644 --- a/tests/m4_acceptance.rs +++ b/tests/m4_acceptance.rs @@ -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 // ===========================================================================