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:
parent
dc7598fb85
commit
d002b7da71
|
|
@ -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).
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
Loading…
Reference in New Issue