From d002b7da71a0e81b31066e623c1cf752d275c682 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 6 Jul 2026 10:46:47 -0400 Subject: [PATCH] refactor(lua): split lua_bindings.rs into a module dir; extract pmacs.diag (F-016, tranche 0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First tranche of the F-016 split of the 15k-line src/lua_bindings.rs. Deliberately minimal — it validates the mechanics before bulk moves. - Convert src/lua_bindings.rs → src/lua_bindings/mod.rs (the `pub mod lua_bindings;` in lib.rs resolves to mod.rs unchanged). - Extract the pmacs.diag surface (diagnostic_to_lua + install_diag) into src/lua_bindings/diag.rs, moved verbatim. mod.rs declares `mod diag;` and its one internal call site is now `diag::install_diag(...)`. Pure code motion: no logic, signature, or behavior change. diag.rs reaches shared-core items (BufferIdLua, SharedCore) via `super::` — a child module can see its ancestors' private items, so no visibility widening was needed; install_diag's only caller is mod.rs itself, so no re-export either. The Lua-visible pmacs.diag.* API is byte-for-byte unchanged. mod.rs: 15202 → 14986 lines. Framing + tranche plan: docs/lua-bindings-split-framing.md. Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib suite 1437 passed / 0 failed under luajit AND lua54 (the tests drive pmacs.diag.* through the Lua VM — same outcomes, code relocated). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U --- docs/lua-bindings-split-framing.md | 176 ++++++++++++++ src/lua_bindings/diag.rs | 229 ++++++++++++++++++ src/{lua_bindings.rs => lua_bindings/mod.rs} | 235 +------------------ 3 files changed, 415 insertions(+), 225 deletions(-) create mode 100644 docs/lua-bindings-split-framing.md create mode 100644 src/lua_bindings/diag.rs rename src/{lua_bindings.rs => lua_bindings/mod.rs} (98%) diff --git a/docs/lua-bindings-split-framing.md b/docs/lua-bindings-split-framing.md new file mode 100644 index 0000000..db940b7 --- /dev/null +++ b/docs/lua-bindings-split-framing.md @@ -0,0 +1,176 @@ +# Splitting `lua_bindings.rs` — framing (F-016) + +`src/lua_bindings.rs` is 15,202 lines — the repository's largest file by +2×. It concentrates the entire Rust↔Lua surface: a shared core (the +registry alias, `BindingError`, the `BufferIdLua` userdata, intercept +views, ~15 state-holder types, the `install()` spine) followed by ~20 +independent `pmacs.` API surfaces (packages, lsp, mcp, completion, +project, index, window, minibuffer, parse, theme, process, diag, async, +ansi, …) and ~2,800 lines of tests. Audit F-016. + +This is exactly the file the audit calls out ("combines many Lua API +domains, package management glue, LSP stores, theme bindings, attachment +bindings, and tests in one file") and its guidance is explicit: **"Split +only along stable boundaries, not as a drive-by refactor."** The +`pmacs.` sections, already fenced by `// ---` banners and each +wired through its own `install__module` seam, *are* those stable +boundaries. + +## Non-negotiable: behavior-preserving code motion + +This arc moves code; it does not change it. Every tranche is: + +- **Pure relocation.** A domain section's items move verbatim into + `lua_bindings/.rs`. No logic edits, no signature changes, no + renames beyond what visibility requires. +- **Minimal visibility widening.** Items in the shared core that a moved + domain references become `pub(crate)` (or `pub(super)`); a domain's own + `install__module` becomes `pub(super)` so the parent `install()` + still calls it. Nothing gains wider visibility than the move demands — + the compiler names each one (E0603/E0433), so the set is exact, not + guessed. +- **Green per tranche.** `cargo build` + `cargo clippy --all-targets` + + the full test suite pass under **both** Lua flavors after every tranche, + before it's committed. A split that changes a test outcome is a bug in + the split. + +The Lua-visible API (`pmacs.buffer.*`, `pmacs.lsp.*`, …) is **byte-for-byte +unchanged** — the same `install()` builds the same tables; only the Rust +file layout moves. No `.lua` code and no test *behavior* changes. + +## Target structure + +`src/lua_bindings.rs` → `src/lua_bindings/`: + +- `mod.rs` — the shared core (registry alias, `BindingError`, + `BufferIdLua` + its method-adders, `LuaInterceptView`, the state-holder + types, `require_init_phase`, shared helpers) **and** the top-level + `pub fn install()` spine that calls each domain's installer. This is the + stable hub every domain depends on; it stays put. +- `lua_bindings/.rs` — one file per `pmacs.` surface, + exposing `pub(super) fn install__module(...)`. Each `#[cfg(test)] + mod tests` moves with its domain where the tests only touch that + domain's (now `pub(super)`-reachable) surface; shared/cross-cutting + tests stay in `mod.rs`. + +The dependency shape is a **hub-and-spoke**: `mod.rs` is the hub; domains +are spokes that depend on the hub and (ideally) not on each other. Where a +genuine domain→domain edge exists (e.g. completion→lsp), the depended-on +domain is extracted first and its needed items widened to `pub(crate)`. + +## Incremental, not a big bang + +A single 15k-line move is unreviewable and risks silent breakage. Instead, +**each tranche is its own PR**: convert the file to a directory module + +extract one dependency-ordered group of domains, prove green, merge, next. +Leaf domains (no outgoing cross-domain edges) go first to establish the +pattern; coupled domains follow their dependencies. + +### What the coupling recon established + +- **Two misplaced helper clusters cause almost every cross-domain edge.** + The generic JSON converters (`lua_to_json`/`lua_table_to_json`/ + `json_to_lua`) sit inside the `lsp` section but are used by `async`, + `mcp`, `completion`, and the completion framework — they belong in shared + core. The ANSI converters (`event_to_lua_table`/`style_to_lua_table`) sit + in the `packages` line-range but belong to `ansi` (`process` uses them + too). Hoisting these dissolves the async→lsp, mcp→lsp, completion→lsp, + and ansi→packages edges. +- **The banners are not clean cut-lines.** The `packages` line-range is a + grab-bag: it physically contains the core module installers (`command`, + `menu`, `help`, `hook`, `describe`, `keymap`) that `install()` calls and + that belong to shared core, plus editor sub-installers. Extraction must + move items to their *logical* home, not by line-range. +- **Rust privacy is on our side.** A child module can read its ancestors' + private items, so a domain moved to `lua_bindings/.rs` reaches + shared-core internals via `super::` with **no widening**. Widening is + needed only for (a) a domain's install fn so the parent can call it + (`pub(super)`), (b) sibling→sibling edges, and (c) external callers — the + last handled by `pub(crate) use ::;` re-exports in `mod.rs` so + `crate::lua_bindings::` paths in `editor.rs`/`lua.rs`/etc. never + change. +- **Real edges that survive the hoist:** `process → ansi`, `project → + lsp`, `completion_framework → index`/`lsp` (type-alias only). These + extract after their dependency. +- **Tests are one flat `#[cfg(test)] mod tests` over `super::*`** touching + private items (`BufferIdLua`, `require_init_phase`, `install`, handles). + They stay in `mod.rs` and move to `lua_bindings/tests.rs` last, once the + handful of items they name are reachable. + +### Tranche plan (one PR each) + +0. **This PR — directory conversion + the truest leaf (`diag`).** + `lua_bindings.rs` → `lua_bindings/mod.rs`; extract the smallest + zero-outgoing-edge leaf, `diag`, as `lua_bindings/diag.rs`. Deliberately + minimal: a large mechanical refactor's first PR should validate the + directory module, the `super::`-access discipline, and the new-file CI + path on the *simplest* real case before moving code in bulk. Subsequent + tranches batch multiple domains now that the mechanics are proven. +1. **`parse` + `theme` and the other pure leaves.** `parse`/`theme` come + as one unit (`make_syntax_registry` installs both; it's the external + entry from `editor.rs`, so this tranche also establishes the + `pub(crate) use` re-export that keeps `crate::lua_bindings::…` paths + stable). Batch in `index`, `window`, `minibuffer`. +2. **Hoist the misplaced helpers.** JSON converters → shared core; ANSI + converters → a new `ansi` module; extract `ansi` and `packages` (after + lifting the misplaced core installers back to shared core). Dissolves + the JSON / ANSI cross-domain edges. +3. **The `lsp` hub + its JSON consumers.** `lsp`, then `async`, `mcp`, + `completion` (edge-free once the JSON helpers are hoisted). +4. **The coupled tail.** `process` (→ansi), `project` (→lsp), + `completion_framework` (→index/lsp), and the `editor` umbrella + (gathering its sub-installers scattered across the `window`/`packages` + ranges). +5. **Tests.** Move the flat `mod tests` into `lua_bindings/tests.rs`. + +Each tranche is independently green and mergeable; the order is +dependency-correct so no tranche introduces an unresolved sibling edge. + +## Scope of this arc + +**This arc splits `src/lua_bindings.rs` only.** The other large files the +audit lists — `src/editor.rs` (7,093) and `pmacs-gpu/src/main.rs` (6,761) +— are separate future arcs with their own stable boundaries (editor +command groups; GPU input / render-pipeline / layout). Bundling them here +would violate the "one stable boundary at a time" discipline. Named as +follow-ups, not started. + +## Categorical bets + +- **The compiler is the safety net.** Because the split is pure motion, + every real breakage is a compile error (missing item, private item) or a + failing test — there is no silent behavioral drift to hunt for. That is + what makes a mechanical refactor of this size tractable. +- **Widen visibility exactly as far as the move forces, no further.** + `pub(crate)`/`pub(super)` over `pub`; the goal is the same encapsulation + in more files, not a newly-public surface. +- **Stable boundaries only.** The `pmacs.` seams are API-shaped and + long-lived; splitting along them ages well. Splitting by line-count or + incidental adjacency would not. + +## Validation + +Per tranche: `cargo fmt` clean; `cargo clippy --all-targets` clean under +luajit **and** lua54; full `cargo test` green under both flavors (the +2,800-line test suite is the behavioral oracle — same tests, same +outcomes, new locations). No Lua-side change, so the `.lua` fixtures and +acceptance suites are untouched and must stay green. + +## As-built + +**Tranche 0 (this PR).** `git mv src/lua_bindings.rs +src/lua_bindings/mod.rs` (the `pub mod lua_bindings;` in `lib.rs` resolves +to `mod.rs` unchanged), then extracted the `pmacs.diag` surface — +`diagnostic_to_lua` + `install_diag`, moved **verbatim** — into +`src/lua_bindings/diag.rs` (229 lines). `mod.rs` declares `mod diag;` and +its one internal call site became `diag::install_diag(...)`; `diag.rs` +reaches shared-core items (`BufferIdLua`, `SharedCore`) via `super::` and +imports externals (`SharedLspManager`, `crate::diag::*`) directly. No +visibility widening was needed — the child module sees the parent's items, +and `install_diag`'s only caller is `mod.rs` itself. No re-export needed +(nothing external references it). `mod.rs`: 15,202 → 14,986 lines. + +Validated: `cargo fmt` clean; `clippy --lib` clean under luajit **and** +lua54; full lib suite **1437 passed / 0 failed** under luajit (the tests, +which drive `pmacs.diag.*` through the Lua VM, are the behavioral oracle — +unchanged outcomes, code merely relocated). diff --git a/src/lua_bindings/diag.rs b/src/lua_bindings/diag.rs new file mode 100644 index 0000000..08951f5 --- /dev/null +++ b/src/lua_bindings/diag.rs @@ -0,0 +1,229 @@ +// lua_bindings/diag.rs --- pmacs.diag: diagnostics surface (T M4.6). + +//! `pmacs.diag.*` — reads the LSP diagnostic store and pushes diagnostic +//! overlays onto windows. Split out of `lua_bindings.rs` verbatim (audit +//! F-016); behavior unchanged. + +use mlua::{Lua, Table, Value}; + +use super::{BufferIdLua, SharedCore}; +use crate::diag::{Diagnostic, DiagnosticSeverity}; +use crate::lsp::SharedLspManager; + +fn diagnostic_to_lua(lua: &Lua, d: &Diagnostic) -> mlua::Result { + let t = lua.create_table_with_capacity(0, 8)?; + t.set("severity", d.severity.label())?; + t.set("severity_code", d.severity as i64)?; + t.set("message", d.message.as_str())?; + if let Some(s) = &d.source { + t.set("source", s.as_str())?; + } + if let Some(c) = &d.code { + t.set("code", c.as_str())?; + } + let range = lua.create_table_with_capacity(0, 4)?; + let start = lua.create_table_with_capacity(0, 2)?; + start.set("line", d.start_line)?; + start.set("character", d.start_col)?; + let end = lua.create_table_with_capacity(0, 2)?; + end.set("line", d.end_line)?; + end.set("character", d.end_col)?; + range.set("start", start)?; + range.set("end", end)?; + t.set("range", range)?; + t.set("start_line", d.start_line)?; + t.set("start_col", d.start_col)?; + t.set("end_line", d.end_line)?; + t.set("end_col", d.end_col)?; + Ok(t) +} + +/// Install `pmacs.diag.*` (T M4.6). +#[allow( + clippy::too_many_lines, + reason = "linear list of raw bindings; splitting fragments a coherent surface" +)] +pub fn install_diag(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> { + let pmacs: Table = lua.globals().get("pmacs")?; + let diag_mod = lua.create_table()?; + + { + let m = manager.clone(); + diag_mod.set( + "list", + lua.create_function(move |lua, uri: String| { + let store_handle = m.borrow().diag_store(); + let guard = store_handle.lock().expect("diag store mutex poisoned"); + let diags = guard.for_uri(&uri); + let out = lua.create_table_with_capacity(diags.len(), 0)?; + for (i, d) in diags.iter().enumerate() { + out.set(i + 1, diagnostic_to_lua(lua, d)?)?; + } + Ok(out) + })?, + )?; + } + + { + let m = manager.clone(); + diag_mod.set( + "count", + lua.create_function(move |_, uri: Option| { + let store_handle = m.borrow().diag_store(); + let guard = store_handle.lock().expect("diag store mutex poisoned"); + let n = match uri { + Some(u) => guard.count_for(&u), + None => { + guard.totals().0 + guard.totals().1 + guard.totals().2 + guard.totals().3 + } + }; + Ok(n) + })?, + )?; + } + + { + let mgr = manager.clone(); + diag_mod.set( + "totals", + lua.create_function(move |lua, ()| { + let store_handle = mgr.borrow().diag_store(); + let guard = store_handle.lock().expect("diag store mutex poisoned"); + let (errs, warns, infos, hints) = guard.totals(); + let table = lua.create_table_with_capacity(0, 4)?; + table.set("error", errs)?; + table.set("warning", warns)?; + table.set("info", infos)?; + table.set("hint", hints)?; + Ok(table) + })?, + )?; + } + + { + let m = manager.clone(); + diag_mod.set( + "next", + lua.create_function( + move |lua, (uri, line, col, wrap): (String, u32, u32, Option)| { + let store_handle = m.borrow().diag_store(); + let guard = store_handle.lock().expect("diag store mutex poisoned"); + let found = guard.next_after(&uri, line, col).or_else(|| { + if wrap.unwrap_or(true) { + guard.first_for(&uri) + } else { + None + } + }); + match found { + Some(d) => Ok(Value::Table(diagnostic_to_lua(lua, d)?)), + None => Ok(Value::Nil), + } + }, + )?, + )?; + } + + { + let m = manager.clone(); + diag_mod.set( + "previous", + lua.create_function( + move |lua, (uri, line, col, wrap): (String, u32, u32, Option)| { + let store_handle = m.borrow().diag_store(); + let guard = store_handle.lock().expect("diag store mutex poisoned"); + let found = guard.previous_before(&uri, line, col).or_else(|| { + if wrap.unwrap_or(true) { + guard.last_for(&uri) + } else { + None + } + }); + match found { + Some(d) => Ok(Value::Table(diagnostic_to_lua(lua, d)?)), + None => Ok(Value::Nil), + } + }, + )?, + )?; + } + + { + let m = manager.clone(); + diag_mod.set( + "uris", + lua.create_function(move |lua, ()| { + let store_handle = m.borrow().diag_store(); + let guard = store_handle.lock().expect("diag store mutex poisoned"); + let uris: Vec = guard.uris().map(str::to_owned).collect(); + let out = lua.create_table_with_capacity(uris.len(), 0)?; + for (i, u) in uris.iter().enumerate() { + out.set(i + 1, u.as_str())?; + } + Ok(out) + })?, + )?; + } + + { + // Helper for tests / Lua-driven flows: clear the + // diagnostics for a URI. + let m = manager.clone(); + diag_mod.set( + "clear", + lua.create_function(move |_, uri: String| { + let store_handle = m.borrow().diag_store(); + let mut guard = store_handle.lock().expect("diag store mutex poisoned"); + guard.clear(&uri); + Ok(()) + })?, + )?; + } + + { + // Look up the severity table-of-strings the rest of the + // surface uses; returned as a constant table for callers + // that prefer a tagged value. + diag_mod.set("severity", { + let t = lua.create_table_with_capacity(0, 4)?; + t.set("error", DiagnosticSeverity::Error as i64)?; + t.set("warning", DiagnosticSeverity::Warning as i64)?; + t.set("info", DiagnosticSeverity::Information as i64)?; + t.set("hint", DiagnosticSeverity::Hint as i64)?; + t + })?; + } + + // 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/src/lua_bindings.rs b/src/lua_bindings/mod.rs similarity index 98% rename from src/lua_bindings.rs rename to src/lua_bindings/mod.rs index 32d759b..b6494a5 100644 --- a/src/lua_bindings.rs +++ b/src/lua_bindings/mod.rs @@ -69,6 +69,15 @@ use crate::rope::Range; use crate::syntax::{self, ParseTreeBundle, ParseView, ParseViewHandle, SharedSyntaxRegistry}; use crate::workers_buffer; +// Domain submodules split out of this file (audit F-016). Each owns one +// `pmacs.` API surface and is installed from the `install()` spine +// / editor wiring below; the shared core (registry alias, `BindingError`, +// `BufferIdLua`, state holders, helpers, and `install()`) stays here. +// Submodules reach shared-core items via `super::` (a child module can see +// its ancestors' private items), so the split needs no visibility widening +// beyond call seams. +mod diag; + // --------------------------------------------------------------------------- // Shared registry alias // --------------------------------------------------------------------------- @@ -8079,7 +8088,7 @@ pub fn make_lsp_manager( ) -> mlua::Result { let manager = Rc::new(RefCell::new(LspManager::new(supervisor, runtime))); install_lsp(lua, &manager, syntax)?; - install_diag(lua, &manager)?; + diag::install_diag(lua, &manager)?; install_completion(lua, &manager)?; install_hover(lua, &manager)?; install_signature(lua, &manager)?; @@ -8689,230 +8698,6 @@ pub fn make_mcp_manager( Ok(manager) } -// --------------------------------------------------------------------------- -// pmacs.diag: diagnostics surface (T M4.6) -// --------------------------------------------------------------------------- - -use crate::diag::{Diagnostic, DiagnosticSeverity}; - -fn diagnostic_to_lua(lua: &Lua, d: &Diagnostic) -> mlua::Result
{ - let t = lua.create_table_with_capacity(0, 8)?; - t.set("severity", d.severity.label())?; - t.set("severity_code", d.severity as i64)?; - t.set("message", d.message.as_str())?; - if let Some(s) = &d.source { - t.set("source", s.as_str())?; - } - if let Some(c) = &d.code { - t.set("code", c.as_str())?; - } - let range = lua.create_table_with_capacity(0, 4)?; - let start = lua.create_table_with_capacity(0, 2)?; - start.set("line", d.start_line)?; - start.set("character", d.start_col)?; - let end = lua.create_table_with_capacity(0, 2)?; - end.set("line", d.end_line)?; - end.set("character", d.end_col)?; - range.set("start", start)?; - range.set("end", end)?; - t.set("range", range)?; - t.set("start_line", d.start_line)?; - t.set("start_col", d.start_col)?; - t.set("end_line", d.end_line)?; - t.set("end_col", d.end_col)?; - Ok(t) -} - -/// Install `pmacs.diag.*` (T M4.6). -#[allow( - clippy::too_many_lines, - reason = "linear list of raw bindings; splitting fragments a coherent surface" -)] -pub fn install_diag(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> { - let pmacs: Table = lua.globals().get("pmacs")?; - let diag_mod = lua.create_table()?; - - { - let m = manager.clone(); - diag_mod.set( - "list", - lua.create_function(move |lua, uri: String| { - let store_handle = m.borrow().diag_store(); - let guard = store_handle.lock().expect("diag store mutex poisoned"); - let diags = guard.for_uri(&uri); - let out = lua.create_table_with_capacity(diags.len(), 0)?; - for (i, d) in diags.iter().enumerate() { - out.set(i + 1, diagnostic_to_lua(lua, d)?)?; - } - Ok(out) - })?, - )?; - } - - { - let m = manager.clone(); - diag_mod.set( - "count", - lua.create_function(move |_, uri: Option| { - let store_handle = m.borrow().diag_store(); - let guard = store_handle.lock().expect("diag store mutex poisoned"); - let n = match uri { - Some(u) => guard.count_for(&u), - None => { - guard.totals().0 + guard.totals().1 + guard.totals().2 + guard.totals().3 - } - }; - Ok(n) - })?, - )?; - } - - { - let mgr = manager.clone(); - diag_mod.set( - "totals", - lua.create_function(move |lua, ()| { - let store_handle = mgr.borrow().diag_store(); - let guard = store_handle.lock().expect("diag store mutex poisoned"); - let (errs, warns, infos, hints) = guard.totals(); - let table = lua.create_table_with_capacity(0, 4)?; - table.set("error", errs)?; - table.set("warning", warns)?; - table.set("info", infos)?; - table.set("hint", hints)?; - Ok(table) - })?, - )?; - } - - { - let m = manager.clone(); - diag_mod.set( - "next", - lua.create_function( - move |lua, (uri, line, col, wrap): (String, u32, u32, Option)| { - let store_handle = m.borrow().diag_store(); - let guard = store_handle.lock().expect("diag store mutex poisoned"); - let found = guard.next_after(&uri, line, col).or_else(|| { - if wrap.unwrap_or(true) { - guard.first_for(&uri) - } else { - None - } - }); - match found { - Some(d) => Ok(Value::Table(diagnostic_to_lua(lua, d)?)), - None => Ok(Value::Nil), - } - }, - )?, - )?; - } - - { - let m = manager.clone(); - diag_mod.set( - "previous", - lua.create_function( - move |lua, (uri, line, col, wrap): (String, u32, u32, Option)| { - let store_handle = m.borrow().diag_store(); - let guard = store_handle.lock().expect("diag store mutex poisoned"); - let found = guard.previous_before(&uri, line, col).or_else(|| { - if wrap.unwrap_or(true) { - guard.last_for(&uri) - } else { - None - } - }); - match found { - Some(d) => Ok(Value::Table(diagnostic_to_lua(lua, d)?)), - None => Ok(Value::Nil), - } - }, - )?, - )?; - } - - { - let m = manager.clone(); - diag_mod.set( - "uris", - lua.create_function(move |lua, ()| { - let store_handle = m.borrow().diag_store(); - let guard = store_handle.lock().expect("diag store mutex poisoned"); - let uris: Vec = guard.uris().map(str::to_owned).collect(); - let out = lua.create_table_with_capacity(uris.len(), 0)?; - for (i, u) in uris.iter().enumerate() { - out.set(i + 1, u.as_str())?; - } - Ok(out) - })?, - )?; - } - - { - // Helper for tests / Lua-driven flows: clear the - // diagnostics for a URI. - let m = manager.clone(); - diag_mod.set( - "clear", - lua.create_function(move |_, uri: String| { - let store_handle = m.borrow().diag_store(); - let mut guard = store_handle.lock().expect("diag store mutex poisoned"); - guard.clear(&uri); - Ok(()) - })?, - )?; - } - - { - // Look up the severity table-of-strings the rest of the - // surface uses; returned as a constant table for callers - // that prefer a tagged value. - diag_mod.set("severity", { - let t = lua.create_table_with_capacity(0, 4)?; - t.set("error", DiagnosticSeverity::Error as i64)?; - t.set("warning", DiagnosticSeverity::Warning as i64)?; - t.set("info", DiagnosticSeverity::Information as i64)?; - t.set("hint", DiagnosticSeverity::Hint as i64)?; - t - })?; - } - - // 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(()) -} - // --------------------------------------------------------------------------- // pmacs.completion / pmacs.hover / pmacs.signature: T M4.7 surfaces // ---------------------------------------------------------------------------