refactor(lua): split lua_bindings.rs into a module dir; extract pmacs.diag (F-016, tranche 0)

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
This commit is contained in:
Levi Neuwirth 2026-07-06 10:46:47 -04:00
parent dc7598fb85
commit d002b7da71
3 changed files with 415 additions and 225 deletions

View File

@ -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.<domain>` 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.<domain>` sections, already fenced by `// ---` banners and each
wired through its own `install_<domain>_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/<domain>.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_<domain>_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/<domain>.rs` — one file per `pmacs.<domain>` surface,
exposing `pub(super) fn install_<domain>_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/<d>.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 <d>::<item>;` re-exports in `mod.rs` so
`crate::lua_bindings::<item>` 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.<domain>` 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).

229
src/lua_bindings/diag.rs Normal file
View File

@ -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<Table> {
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<String>| {
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<bool>)| {
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<bool>)| {
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<String> = 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::<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

@ -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.<domain>` 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<SharedLspManager> {
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<Table> {
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<String>| {
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<bool>)| {
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<bool>)| {
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<String> = 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::<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(())
}
// ---------------------------------------------------------------------------
// pmacs.completion / pmacs.hover / pmacs.signature: T M4.7 surfaces
// ---------------------------------------------------------------------------