refactor(lua): extract pmacs.index into its own module (F-016, tranche 1)
Second tranche of the F-016 split. Extract the pmacs.index surface (the project symbol-index bindings) from src/lua_bindings/mod.rs into src/lua_bindings/index.rs, moved verbatim. index is the one genuinely clean remaining leaf: its private helpers (symbol_kind_from_lua, lua_symbol_from_table, search_hit_to_lua) are used only within its own range, and it has zero shared-core coupling — it depends only on crate::project_index, mlua, and std, reaching one stranded helper (lua_to_json, still in the lsp section) via `super::`. mod.rs declares `mod index;` and re-exports `SharedProjectIndexer` + `make_project_indexer` via `pub use`, so the crate::lua_bindings::… paths in editor.rs and completion_framework.rs (and an in-file completion- framework use) stay valid — no external file changes. Pure code motion, no behavior change. mod.rs: 14986 → 14603 lines. While vetting the next leaves I found the recon under-counted the misplaced shared helpers: parse/theme, window, and minibuffer trail off into shared style/color, caller_source, and command/menu helpers, so a dedicated helper-hoist tranche must precede them (framing tranche plan updated). This tranche stops at index rather than force a contaminated extraction. Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib suite 1437 passed / 0 failed under both luajit and lua54. 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
f0e121ab05
commit
df35e03ecf
|
|
@ -106,15 +106,27 @@ pattern; coupled domains follow their dependencies.
|
||||||
directory module, the `super::`-access discipline, and the new-file CI
|
directory module, the `super::`-access discipline, and the new-file CI
|
||||||
path on the *simplest* real case before moving code in bulk. Subsequent
|
path on the *simplest* real case before moving code in bulk. Subsequent
|
||||||
tranches batch multiple domains now that the mechanics are proven.
|
tranches batch multiple domains now that the mechanics are proven.
|
||||||
1. **`parse` + `theme` and the other pure leaves.** `parse`/`theme` come
|
1. **`index` (the one genuinely clean remaining leaf).** Zero shared-core
|
||||||
as one unit (`make_syntax_registry` installs both; it's the external
|
coupling; establishes the `pub(crate) use` re-export for external
|
||||||
entry from `editor.rs`, so this tranche also establishes the
|
callers (`editor.rs`, `completion_framework.rs`) and `super::` access
|
||||||
`pub(crate) use` re-export that keeps `crate::lua_bindings::…` paths
|
to a stranded helper (`lua_to_json`). **Correction after tranche 1:**
|
||||||
stable). Batch in `index`, `window`, `minibuffer`.
|
the recon under-counted the misplaced helpers. Beyond the JSON/ANSI
|
||||||
2. **Hoist the misplaced helpers.** JSON converters → shared core; ANSI
|
clusters, the *style/color* converters (`lua_to_style`/`style_to_lua`/
|
||||||
converters → a new `ansi` module; extract `ansi` and `packages` (after
|
`color_to_lua`/…) sit in the `theme` section, and `caller_source` +
|
||||||
lifting the misplaced core installers back to shared core). Dissolves
|
the command/menu builders (`build_command_from_spec`,
|
||||||
the JSON / ANSI cross-domain edges.
|
`build_menu_item_from_spec`, `BindArgs`, `register`, …) sit at the tail
|
||||||
|
of the `minibuffer` range — all shared, used across sections. So
|
||||||
|
`parse`/`theme`, `window`, and `minibuffer` are **not** clean
|
||||||
|
line-range extractions until the hoist below runs.
|
||||||
|
2. **Hoist the misplaced helpers (must precede the contaminated leaves).**
|
||||||
|
Move every shared helper stranded in a domain section to its proper
|
||||||
|
home: JSON converters → shared core; ANSI converters → the `ansi`
|
||||||
|
module; style/color converters + `caller_source` + command/menu
|
||||||
|
builders → shared core. This is a within-`mod.rs` repositioning (no
|
||||||
|
behavior change) that makes the domain sections clean line-range units.
|
||||||
|
Then extract `parse`/`theme`, `window`, `minibuffer`, `ansi`,
|
||||||
|
`packages` (the last after lifting the misplaced core installers out of
|
||||||
|
its range too).
|
||||||
3. **The `lsp` hub + its JSON consumers.** `lsp`, then `async`, `mcp`,
|
3. **The `lsp` hub + its JSON consumers.** `lsp`, then `async`, `mcp`,
|
||||||
`completion` (edge-free once the JSON helpers are hoisted).
|
`completion` (edge-free once the JSON helpers are hoisted).
|
||||||
4. **The coupled tail.** `process` (→ansi), `project` (→lsp),
|
4. **The coupled tail.** `process` (→ansi), `project` (→lsp),
|
||||||
|
|
@ -174,3 +186,25 @@ Validated: `cargo fmt` clean; `clippy --lib` clean under luajit **and**
|
||||||
lua54; full lib suite **1437 passed / 0 failed** under luajit (the tests,
|
lua54; full lib suite **1437 passed / 0 failed** under luajit (the tests,
|
||||||
which drive `pmacs.diag.*` through the Lua VM, are the behavioral oracle —
|
which drive `pmacs.diag.*` through the Lua VM, are the behavioral oracle —
|
||||||
unchanged outcomes, code merely relocated).
|
unchanged outcomes, code merely relocated).
|
||||||
|
|
||||||
|
**Tranche 1 (this PR).** Extracted `pmacs.index` (the project symbol-index
|
||||||
|
surface) into `src/lua_bindings/index.rs` (390 lines) — the one remaining
|
||||||
|
*clean* leaf (its 3 private helpers are used only within its own range).
|
||||||
|
`mod.rs` declares `mod index;` and `pub use index::{SharedProjectIndexer,
|
||||||
|
make_project_indexer};`, which keeps the `crate::lua_bindings::…` paths in
|
||||||
|
`editor.rs` + `completion_framework.rs` (and an in-file completion-framework
|
||||||
|
use) valid. `index.rs` has **zero shared-core coupling** — it depends only
|
||||||
|
on `crate::project_index`, mlua, std, and reaches one stranded helper
|
||||||
|
(`lua_to_json`, still in the `lsp` section) via `super::`. Verbatim move,
|
||||||
|
no logic change. `mod.rs`: 14,986 → 14,603 lines.
|
||||||
|
|
||||||
|
While vetting the next leaves, discovered the recon under-counted the
|
||||||
|
misplaced helpers (see the corrected tranche plan above): `parse`/`theme`,
|
||||||
|
`window`, and `minibuffer` trail off into *shared* style/color, `caller_
|
||||||
|
source`, and command/menu helpers, so they need the helper-hoist tranche
|
||||||
|
(now #2) before they can be extracted cleanly. This tranche stops at
|
||||||
|
`index` rather than force those.
|
||||||
|
|
||||||
|
Validated: `cargo fmt` clean; `clippy --lib` clean under **both** flavors;
|
||||||
|
full lib suite **1437 passed / 0 failed** under **both** luajit and
|
||||||
|
lua54.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,395 @@
|
||||||
|
// lua_bindings/index.rs --- pmacs.index: project-scoped symbol index.
|
||||||
|
|
||||||
|
//! `pmacs.index.*` — the project symbol index surface (T M4.10). Split out
|
||||||
|
//! of `lua_bindings.rs` verbatim (audit F-016); behavior unchanged. Pure
|
||||||
|
//! leaf: depends only on `crate::project_index`, mlua, and std — no
|
||||||
|
//! shared-core coupling.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use mlua::{Lua, Table, Value};
|
||||||
|
|
||||||
|
// `lua_to_json` is a generic JSON converter that still physically lives in
|
||||||
|
// the `lsp` section of `mod.rs`; reachable here as a parent-private item.
|
||||||
|
// (A later tranche hoists it into shared core proper.)
|
||||||
|
use super::lua_to_json;
|
||||||
|
|
||||||
|
use crate::project_index::{
|
||||||
|
FileEntry, ProjectIndexer, SearchHit, Symbol, SymbolKind, SymbolSource, extract_heuristic,
|
||||||
|
fnv1a_64, ingest_lsp_symbols,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Cheaply-cloneable shared project index registry.
|
||||||
|
pub type SharedProjectIndexer = Rc<RefCell<ProjectIndexer>>;
|
||||||
|
|
||||||
|
fn symbol_kind_from_lua(tag: &str) -> SymbolKind {
|
||||||
|
match tag {
|
||||||
|
"function" => SymbolKind::Function,
|
||||||
|
"method" => SymbolKind::Method,
|
||||||
|
"struct" => SymbolKind::Struct,
|
||||||
|
"class" => SymbolKind::Class,
|
||||||
|
"trait" | "interface" => SymbolKind::Trait,
|
||||||
|
"enum" => SymbolKind::Enum,
|
||||||
|
"variable" => SymbolKind::Variable,
|
||||||
|
"constant" => SymbolKind::Constant,
|
||||||
|
"field" | "property" => SymbolKind::Field,
|
||||||
|
"module" | "namespace" => SymbolKind::Module,
|
||||||
|
"macro" => SymbolKind::Macro,
|
||||||
|
"type_alias" | "type" => SymbolKind::TypeAlias,
|
||||||
|
other => SymbolKind::Other(other.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_symbol_from_table(t: &Table) -> mlua::Result<Symbol> {
|
||||||
|
let name: String = t.get("name")?;
|
||||||
|
let kind_tag: Option<String> = t.get("kind").ok().flatten();
|
||||||
|
let kind = kind_tag
|
||||||
|
.as_deref()
|
||||||
|
.map_or(SymbolKind::Other("unknown".into()), symbol_kind_from_lua);
|
||||||
|
let line: u32 = t.get("line").unwrap_or(0);
|
||||||
|
let col: u32 = t.get("col").unwrap_or(0);
|
||||||
|
let source_tag: Option<String> = t.get("source").ok().flatten();
|
||||||
|
let source = source_tag
|
||||||
|
.as_deref()
|
||||||
|
.and_then(SymbolSource::from_tag)
|
||||||
|
.unwrap_or(SymbolSource::Lua);
|
||||||
|
let container: Option<String> = t.get("container").ok().flatten();
|
||||||
|
Ok(Symbol {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
line,
|
||||||
|
col,
|
||||||
|
source,
|
||||||
|
container,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_hit_to_lua(lua: &Lua, hit: &SearchHit) -> mlua::Result<Table> {
|
||||||
|
let t = lua.create_table_with_capacity(0, 9)?;
|
||||||
|
t.set("name", hit.name.as_str())?;
|
||||||
|
t.set("kind", hit.kind.tag())?;
|
||||||
|
t.set("source", hit.source.tag())?;
|
||||||
|
t.set("path", hit.path.display().to_string())?;
|
||||||
|
t.set("relative_path", hit.relative_path.display().to_string())?;
|
||||||
|
t.set("line", hit.line)?;
|
||||||
|
t.set("col", hit.col)?;
|
||||||
|
t.set("score", hit.score)?;
|
||||||
|
if let Some(c) = &hit.container {
|
||||||
|
t.set("container", c.as_str())?;
|
||||||
|
}
|
||||||
|
if let Some(l) = &hit.language {
|
||||||
|
t.set("language", l.as_str())?;
|
||||||
|
}
|
||||||
|
Ok(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install `pmacs.index.*` (T M4.10). Preserves any existing
|
||||||
|
/// `pmacs.index` keys (e.g. user-supplied indexer extensions
|
||||||
|
/// installed by builtin Lua chunks).
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "linear list of index bindings; splitting adds ceremony without clarity"
|
||||||
|
)]
|
||||||
|
pub fn install_project_index(lua: &Lua, indexer: &SharedProjectIndexer) -> mlua::Result<()> {
|
||||||
|
let pmacs: Table = lua.globals().get("pmacs")?;
|
||||||
|
let m: Table = match pmacs.get::<Option<Table>>("index")? {
|
||||||
|
Some(t) => t,
|
||||||
|
None => lua.create_table()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
// open(root) -> root_string. Ensures an index exists for
|
||||||
|
// `root`; idempotent. Returns the canonicalised root the
|
||||||
|
// caller should pass back to subsequent calls.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"open",
|
||||||
|
lua.create_function(move |_, root: String| {
|
||||||
|
let mut ix_ref = ix.borrow_mut();
|
||||||
|
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
||||||
|
Ok(idx.root.display().to_string())
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// close(root): drop the in-memory index. Does not touch disk.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"close",
|
||||||
|
lua.create_function(move |_, root: String| {
|
||||||
|
Ok(ix.borrow_mut().forget(std::path::Path::new(&root)))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// upsert_file(root, path, language, source) -> { added }.
|
||||||
|
// Runs the heuristic extractor on `source`, hashes it, and
|
||||||
|
// replaces the entry for `path`.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"upsert_file",
|
||||||
|
lua.create_function(
|
||||||
|
move |lua,
|
||||||
|
(root, path, language, source): (
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
String,
|
||||||
|
)| {
|
||||||
|
let mut ix_ref = ix.borrow_mut();
|
||||||
|
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
||||||
|
let lang = language.as_deref().unwrap_or("");
|
||||||
|
let symbols = if lang.is_empty() {
|
||||||
|
crate::project_index::extract_raw(&source)
|
||||||
|
} else {
|
||||||
|
extract_heuristic(lang, &source)
|
||||||
|
};
|
||||||
|
let added = symbols.len();
|
||||||
|
let entry = FileEntry {
|
||||||
|
path: std::path::PathBuf::from(&path),
|
||||||
|
mtime_secs: 0,
|
||||||
|
content_hash: fnv1a_64(source.as_bytes()),
|
||||||
|
language: language.clone(),
|
||||||
|
symbols,
|
||||||
|
};
|
||||||
|
idx.upsert_file(entry);
|
||||||
|
let t = lua.create_table_with_capacity(0, 1)?;
|
||||||
|
t.set("added", added)?;
|
||||||
|
Ok(t)
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// upsert_symbols(root, path, language, symbol_array): push
|
||||||
|
// pre-extracted symbols (e.g. from a Lua-side indexer) into
|
||||||
|
// the index. Each entry is a table with name/kind/line/col/
|
||||||
|
// source/container fields.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"upsert_symbols",
|
||||||
|
lua.create_function(
|
||||||
|
move |_,
|
||||||
|
(root, path, language, symbols): (
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Vec<Table>,
|
||||||
|
)| {
|
||||||
|
let mut parsed = Vec::with_capacity(symbols.len());
|
||||||
|
for t in &symbols {
|
||||||
|
parsed.push(lua_symbol_from_table(t)?);
|
||||||
|
}
|
||||||
|
let added = parsed.len();
|
||||||
|
let mut ix_ref = ix.borrow_mut();
|
||||||
|
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
||||||
|
let entry = FileEntry {
|
||||||
|
path: std::path::PathBuf::from(&path),
|
||||||
|
mtime_secs: 0,
|
||||||
|
content_hash: 0,
|
||||||
|
language,
|
||||||
|
symbols: parsed,
|
||||||
|
};
|
||||||
|
idx.upsert_file(entry);
|
||||||
|
Ok(added)
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// ingest_lsp(root, lsp_response): merge symbols from a
|
||||||
|
// workspace/symbol or documentSymbol response. Groups
|
||||||
|
// results by path and replaces each path's entry.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"ingest_lsp",
|
||||||
|
lua.create_function(move |_, (root, value): (String, Value)| {
|
||||||
|
let json = lua_to_json(value)?;
|
||||||
|
let inbound = ingest_lsp_symbols(&json);
|
||||||
|
let mut ix_ref = ix.borrow_mut();
|
||||||
|
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
||||||
|
let mut by_path: std::collections::HashMap<
|
||||||
|
std::path::PathBuf,
|
||||||
|
(Option<String>, Vec<Symbol>),
|
||||||
|
> = std::collections::HashMap::new();
|
||||||
|
for entry in inbound {
|
||||||
|
let bucket = by_path
|
||||||
|
.entry(entry.path)
|
||||||
|
.or_insert_with(|| (entry.language.clone(), Vec::new()));
|
||||||
|
bucket.1.push(entry.symbol);
|
||||||
|
}
|
||||||
|
let merged = by_path.len();
|
||||||
|
for (path, (lang, symbols)) in by_path {
|
||||||
|
idx.upsert_file(FileEntry {
|
||||||
|
path,
|
||||||
|
mtime_secs: 0,
|
||||||
|
content_hash: 0,
|
||||||
|
language: lang,
|
||||||
|
symbols,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(merged)
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// invalidate(root, path): drop one file's entry.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"invalidate",
|
||||||
|
lua.create_function(move |_, (root, path): (String, String)| {
|
||||||
|
let mut ix_ref = ix.borrow_mut();
|
||||||
|
Ok(ix_ref
|
||||||
|
.get_mut(std::path::Path::new(&root))
|
||||||
|
.is_some_and(|idx| idx.forget_file(std::path::Path::new(&path))))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// is_fresh(root, path, mtime_secs, content_hash) -> bool
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"is_fresh",
|
||||||
|
lua.create_function(
|
||||||
|
move |_, (root, path, mtime_secs, content_hash): (String, String, u64, u64)| {
|
||||||
|
let ix_ref = ix.borrow();
|
||||||
|
Ok(ix_ref.get(std::path::Path::new(&root)).is_some_and(|idx| {
|
||||||
|
idx.is_fresh(std::path::Path::new(&path), mtime_secs, content_hash)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// search(root, query [, limit]) -> array of hit tables.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"search",
|
||||||
|
lua.create_function(
|
||||||
|
move |lua, (root, query, limit): (String, String, Option<usize>)| {
|
||||||
|
let ix_ref = ix.borrow();
|
||||||
|
let Some(idx) = ix_ref.get(std::path::Path::new(&root)) else {
|
||||||
|
return lua.create_table();
|
||||||
|
};
|
||||||
|
let hits = idx.search(&query, limit.unwrap_or(50));
|
||||||
|
let out = lua.create_table_with_capacity(hits.len(), 0)?;
|
||||||
|
for (i, h) in hits.iter().enumerate() {
|
||||||
|
out.set(i + 1, search_hit_to_lua(lua, h)?)?;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// save(root [, path]): persist the index. Path defaults to
|
||||||
|
// <root>/.pmacs/index.json.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"save",
|
||||||
|
lua.create_function(move |_, (root, path): (String, Option<String>)| {
|
||||||
|
let ix_ref = ix.borrow();
|
||||||
|
let idx = ix_ref
|
||||||
|
.get(std::path::Path::new(&root))
|
||||||
|
.ok_or_else(|| mlua::Error::external(format!("unknown index root: {root}")))?;
|
||||||
|
let dest = path.map_or_else(|| idx.default_cache_path(), std::path::PathBuf::from);
|
||||||
|
idx.save(&dest).map_err(mlua::Error::external)?;
|
||||||
|
Ok(dest.display().to_string())
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// load(root [, path]): replace the in-memory index for
|
||||||
|
// `root` with the on-disk cache. A missing cache file
|
||||||
|
// results in an empty index (cold-start).
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"load",
|
||||||
|
lua.create_function(move |_, (root, path): (String, Option<String>)| {
|
||||||
|
let root_path = std::path::PathBuf::from(&root);
|
||||||
|
let cache_path = path.map_or_else(
|
||||||
|
|| crate::project_index::ProjectIndex::cache_path_for(&root_path),
|
||||||
|
std::path::PathBuf::from,
|
||||||
|
);
|
||||||
|
let idx = crate::project_index::ProjectIndex::load(root_path.clone(), &cache_path)
|
||||||
|
.map_err(mlua::Error::external)?;
|
||||||
|
let symbol_count = idx.symbol_count();
|
||||||
|
let file_count = idx.file_count();
|
||||||
|
let mut ix_ref = ix.borrow_mut();
|
||||||
|
let key = idx.root.clone();
|
||||||
|
ix_ref.forget(&key);
|
||||||
|
let slot = ix_ref.ensure(key);
|
||||||
|
*slot = idx;
|
||||||
|
Ok((file_count, symbol_count))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// stats(root) -> { files, symbols, generation } or nil.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"stats",
|
||||||
|
lua.create_function(move |lua, root: String| {
|
||||||
|
let ix_ref = ix.borrow();
|
||||||
|
let Some(idx) = ix_ref.get(std::path::Path::new(&root)) else {
|
||||||
|
return Ok(Value::Nil);
|
||||||
|
};
|
||||||
|
let t = lua.create_table_with_capacity(0, 4)?;
|
||||||
|
t.set("files", idx.file_count())?;
|
||||||
|
t.set("symbols", idx.symbol_count())?;
|
||||||
|
t.set("generation", idx.generation)?;
|
||||||
|
t.set("root", idx.root.display().to_string())?;
|
||||||
|
Ok(Value::Table(t))
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// roots() -> array of registered index roots.
|
||||||
|
let ix = indexer.clone();
|
||||||
|
m.set(
|
||||||
|
"roots",
|
||||||
|
lua.create_function(move |lua, ()| {
|
||||||
|
let ix_ref = ix.borrow();
|
||||||
|
let mut roots: Vec<String> =
|
||||||
|
ix_ref.roots().map(|p| p.display().to_string()).collect();
|
||||||
|
roots.sort();
|
||||||
|
let out = lua.create_table_with_capacity(roots.len(), 0)?;
|
||||||
|
for (i, r) in roots.iter().enumerate() {
|
||||||
|
out.set(i + 1, r.as_str())?;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// hash(text) -> u64. Exposes FNV-1a so Lua callers can
|
||||||
|
// produce stable cache keys without a separate hash crate.
|
||||||
|
m.set(
|
||||||
|
"hash",
|
||||||
|
lua.create_function(|_, text: String| Ok(fnv1a_64(text.as_bytes())))?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pmacs.set("index", m)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a fresh [`ProjectIndexer`] and install `pmacs.index.*` over it.
|
||||||
|
pub fn make_project_indexer(lua: &Lua) -> mlua::Result<SharedProjectIndexer> {
|
||||||
|
let ix: SharedProjectIndexer = Rc::new(RefCell::new(ProjectIndexer::new()));
|
||||||
|
install_project_index(lua, &ix)?;
|
||||||
|
Ok(ix)
|
||||||
|
}
|
||||||
|
|
@ -75,8 +75,12 @@ use crate::workers_buffer;
|
||||||
// `BufferIdLua`, state holders, helpers, and `install()`) stays here.
|
// `BufferIdLua`, state holders, helpers, and `install()`) stays here.
|
||||||
// Submodules reach shared-core items via `super::` (a child module can see
|
// Submodules reach shared-core items via `super::` (a child module can see
|
||||||
// its ancestors' private items), so the split needs no visibility widening
|
// its ancestors' private items), so the split needs no visibility widening
|
||||||
// beyond call seams.
|
// beyond call seams. Public entry points a domain owns are re-exported here
|
||||||
|
// so external `crate::lua_bindings::<item>` paths (and in-file uses) stay
|
||||||
|
// stable.
|
||||||
mod diag;
|
mod diag;
|
||||||
|
mod index;
|
||||||
|
pub use index::{SharedProjectIndexer, make_project_indexer};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Shared registry alias
|
// Shared registry alias
|
||||||
|
|
@ -10122,389 +10126,6 @@ pub fn make_workspace(lua: &Lua, lsp_manager: &SharedLspManager) -> mlua::Result
|
||||||
Ok(ws)
|
Ok(ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// pmacs.index: project-scoped symbol index (T M4.10)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
use crate::project_index::{
|
|
||||||
FileEntry, ProjectIndexer, SearchHit, Symbol, SymbolKind, SymbolSource, extract_heuristic,
|
|
||||||
fnv1a_64, ingest_lsp_symbols,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Cheaply-cloneable shared project index registry.
|
|
||||||
pub type SharedProjectIndexer = Rc<RefCell<ProjectIndexer>>;
|
|
||||||
|
|
||||||
fn symbol_kind_from_lua(tag: &str) -> SymbolKind {
|
|
||||||
match tag {
|
|
||||||
"function" => SymbolKind::Function,
|
|
||||||
"method" => SymbolKind::Method,
|
|
||||||
"struct" => SymbolKind::Struct,
|
|
||||||
"class" => SymbolKind::Class,
|
|
||||||
"trait" | "interface" => SymbolKind::Trait,
|
|
||||||
"enum" => SymbolKind::Enum,
|
|
||||||
"variable" => SymbolKind::Variable,
|
|
||||||
"constant" => SymbolKind::Constant,
|
|
||||||
"field" | "property" => SymbolKind::Field,
|
|
||||||
"module" | "namespace" => SymbolKind::Module,
|
|
||||||
"macro" => SymbolKind::Macro,
|
|
||||||
"type_alias" | "type" => SymbolKind::TypeAlias,
|
|
||||||
other => SymbolKind::Other(other.to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lua_symbol_from_table(t: &Table) -> mlua::Result<Symbol> {
|
|
||||||
let name: String = t.get("name")?;
|
|
||||||
let kind_tag: Option<String> = t.get("kind").ok().flatten();
|
|
||||||
let kind = kind_tag
|
|
||||||
.as_deref()
|
|
||||||
.map_or(SymbolKind::Other("unknown".into()), symbol_kind_from_lua);
|
|
||||||
let line: u32 = t.get("line").unwrap_or(0);
|
|
||||||
let col: u32 = t.get("col").unwrap_or(0);
|
|
||||||
let source_tag: Option<String> = t.get("source").ok().flatten();
|
|
||||||
let source = source_tag
|
|
||||||
.as_deref()
|
|
||||||
.and_then(SymbolSource::from_tag)
|
|
||||||
.unwrap_or(SymbolSource::Lua);
|
|
||||||
let container: Option<String> = t.get("container").ok().flatten();
|
|
||||||
Ok(Symbol {
|
|
||||||
name,
|
|
||||||
kind,
|
|
||||||
line,
|
|
||||||
col,
|
|
||||||
source,
|
|
||||||
container,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_hit_to_lua(lua: &Lua, hit: &SearchHit) -> mlua::Result<Table> {
|
|
||||||
let t = lua.create_table_with_capacity(0, 9)?;
|
|
||||||
t.set("name", hit.name.as_str())?;
|
|
||||||
t.set("kind", hit.kind.tag())?;
|
|
||||||
t.set("source", hit.source.tag())?;
|
|
||||||
t.set("path", hit.path.display().to_string())?;
|
|
||||||
t.set("relative_path", hit.relative_path.display().to_string())?;
|
|
||||||
t.set("line", hit.line)?;
|
|
||||||
t.set("col", hit.col)?;
|
|
||||||
t.set("score", hit.score)?;
|
|
||||||
if let Some(c) = &hit.container {
|
|
||||||
t.set("container", c.as_str())?;
|
|
||||||
}
|
|
||||||
if let Some(l) = &hit.language {
|
|
||||||
t.set("language", l.as_str())?;
|
|
||||||
}
|
|
||||||
Ok(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install `pmacs.index.*` (T M4.10). Preserves any existing
|
|
||||||
/// `pmacs.index` keys (e.g. user-supplied indexer extensions
|
|
||||||
/// installed by builtin Lua chunks).
|
|
||||||
#[allow(
|
|
||||||
clippy::too_many_lines,
|
|
||||||
reason = "linear list of index bindings; splitting adds ceremony without clarity"
|
|
||||||
)]
|
|
||||||
pub fn install_project_index(lua: &Lua, indexer: &SharedProjectIndexer) -> mlua::Result<()> {
|
|
||||||
let pmacs: Table = lua.globals().get("pmacs")?;
|
|
||||||
let m: Table = match pmacs.get::<Option<Table>>("index")? {
|
|
||||||
Some(t) => t,
|
|
||||||
None => lua.create_table()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
// open(root) -> root_string. Ensures an index exists for
|
|
||||||
// `root`; idempotent. Returns the canonicalised root the
|
|
||||||
// caller should pass back to subsequent calls.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"open",
|
|
||||||
lua.create_function(move |_, root: String| {
|
|
||||||
let mut ix_ref = ix.borrow_mut();
|
|
||||||
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
|
||||||
Ok(idx.root.display().to_string())
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// close(root): drop the in-memory index. Does not touch disk.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"close",
|
|
||||||
lua.create_function(move |_, root: String| {
|
|
||||||
Ok(ix.borrow_mut().forget(std::path::Path::new(&root)))
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// upsert_file(root, path, language, source) -> { added }.
|
|
||||||
// Runs the heuristic extractor on `source`, hashes it, and
|
|
||||||
// replaces the entry for `path`.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"upsert_file",
|
|
||||||
lua.create_function(
|
|
||||||
move |lua,
|
|
||||||
(root, path, language, source): (
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
Option<String>,
|
|
||||||
String,
|
|
||||||
)| {
|
|
||||||
let mut ix_ref = ix.borrow_mut();
|
|
||||||
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
|
||||||
let lang = language.as_deref().unwrap_or("");
|
|
||||||
let symbols = if lang.is_empty() {
|
|
||||||
crate::project_index::extract_raw(&source)
|
|
||||||
} else {
|
|
||||||
extract_heuristic(lang, &source)
|
|
||||||
};
|
|
||||||
let added = symbols.len();
|
|
||||||
let entry = FileEntry {
|
|
||||||
path: std::path::PathBuf::from(&path),
|
|
||||||
mtime_secs: 0,
|
|
||||||
content_hash: fnv1a_64(source.as_bytes()),
|
|
||||||
language: language.clone(),
|
|
||||||
symbols,
|
|
||||||
};
|
|
||||||
idx.upsert_file(entry);
|
|
||||||
let t = lua.create_table_with_capacity(0, 1)?;
|
|
||||||
t.set("added", added)?;
|
|
||||||
Ok(t)
|
|
||||||
},
|
|
||||||
)?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// upsert_symbols(root, path, language, symbol_array): push
|
|
||||||
// pre-extracted symbols (e.g. from a Lua-side indexer) into
|
|
||||||
// the index. Each entry is a table with name/kind/line/col/
|
|
||||||
// source/container fields.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"upsert_symbols",
|
|
||||||
lua.create_function(
|
|
||||||
move |_,
|
|
||||||
(root, path, language, symbols): (
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
Option<String>,
|
|
||||||
Vec<Table>,
|
|
||||||
)| {
|
|
||||||
let mut parsed = Vec::with_capacity(symbols.len());
|
|
||||||
for t in &symbols {
|
|
||||||
parsed.push(lua_symbol_from_table(t)?);
|
|
||||||
}
|
|
||||||
let added = parsed.len();
|
|
||||||
let mut ix_ref = ix.borrow_mut();
|
|
||||||
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
|
||||||
let entry = FileEntry {
|
|
||||||
path: std::path::PathBuf::from(&path),
|
|
||||||
mtime_secs: 0,
|
|
||||||
content_hash: 0,
|
|
||||||
language,
|
|
||||||
symbols: parsed,
|
|
||||||
};
|
|
||||||
idx.upsert_file(entry);
|
|
||||||
Ok(added)
|
|
||||||
},
|
|
||||||
)?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// ingest_lsp(root, lsp_response): merge symbols from a
|
|
||||||
// workspace/symbol or documentSymbol response. Groups
|
|
||||||
// results by path and replaces each path's entry.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"ingest_lsp",
|
|
||||||
lua.create_function(move |_, (root, value): (String, Value)| {
|
|
||||||
let json = lua_to_json(value)?;
|
|
||||||
let inbound = ingest_lsp_symbols(&json);
|
|
||||||
let mut ix_ref = ix.borrow_mut();
|
|
||||||
let idx = ix_ref.ensure(std::path::PathBuf::from(&root));
|
|
||||||
let mut by_path: std::collections::HashMap<
|
|
||||||
std::path::PathBuf,
|
|
||||||
(Option<String>, Vec<Symbol>),
|
|
||||||
> = std::collections::HashMap::new();
|
|
||||||
for entry in inbound {
|
|
||||||
let bucket = by_path
|
|
||||||
.entry(entry.path)
|
|
||||||
.or_insert_with(|| (entry.language.clone(), Vec::new()));
|
|
||||||
bucket.1.push(entry.symbol);
|
|
||||||
}
|
|
||||||
let merged = by_path.len();
|
|
||||||
for (path, (lang, symbols)) in by_path {
|
|
||||||
idx.upsert_file(FileEntry {
|
|
||||||
path,
|
|
||||||
mtime_secs: 0,
|
|
||||||
content_hash: 0,
|
|
||||||
language: lang,
|
|
||||||
symbols,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(merged)
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// invalidate(root, path): drop one file's entry.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"invalidate",
|
|
||||||
lua.create_function(move |_, (root, path): (String, String)| {
|
|
||||||
let mut ix_ref = ix.borrow_mut();
|
|
||||||
Ok(ix_ref
|
|
||||||
.get_mut(std::path::Path::new(&root))
|
|
||||||
.is_some_and(|idx| idx.forget_file(std::path::Path::new(&path))))
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// is_fresh(root, path, mtime_secs, content_hash) -> bool
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"is_fresh",
|
|
||||||
lua.create_function(
|
|
||||||
move |_, (root, path, mtime_secs, content_hash): (String, String, u64, u64)| {
|
|
||||||
let ix_ref = ix.borrow();
|
|
||||||
Ok(ix_ref.get(std::path::Path::new(&root)).is_some_and(|idx| {
|
|
||||||
idx.is_fresh(std::path::Path::new(&path), mtime_secs, content_hash)
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
)?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// search(root, query [, limit]) -> array of hit tables.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"search",
|
|
||||||
lua.create_function(
|
|
||||||
move |lua, (root, query, limit): (String, String, Option<usize>)| {
|
|
||||||
let ix_ref = ix.borrow();
|
|
||||||
let Some(idx) = ix_ref.get(std::path::Path::new(&root)) else {
|
|
||||||
return lua.create_table();
|
|
||||||
};
|
|
||||||
let hits = idx.search(&query, limit.unwrap_or(50));
|
|
||||||
let out = lua.create_table_with_capacity(hits.len(), 0)?;
|
|
||||||
for (i, h) in hits.iter().enumerate() {
|
|
||||||
out.set(i + 1, search_hit_to_lua(lua, h)?)?;
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
},
|
|
||||||
)?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// save(root [, path]): persist the index. Path defaults to
|
|
||||||
// <root>/.pmacs/index.json.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"save",
|
|
||||||
lua.create_function(move |_, (root, path): (String, Option<String>)| {
|
|
||||||
let ix_ref = ix.borrow();
|
|
||||||
let idx = ix_ref
|
|
||||||
.get(std::path::Path::new(&root))
|
|
||||||
.ok_or_else(|| mlua::Error::external(format!("unknown index root: {root}")))?;
|
|
||||||
let dest = path.map_or_else(|| idx.default_cache_path(), std::path::PathBuf::from);
|
|
||||||
idx.save(&dest).map_err(mlua::Error::external)?;
|
|
||||||
Ok(dest.display().to_string())
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// load(root [, path]): replace the in-memory index for
|
|
||||||
// `root` with the on-disk cache. A missing cache file
|
|
||||||
// results in an empty index (cold-start).
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"load",
|
|
||||||
lua.create_function(move |_, (root, path): (String, Option<String>)| {
|
|
||||||
let root_path = std::path::PathBuf::from(&root);
|
|
||||||
let cache_path = path.map_or_else(
|
|
||||||
|| crate::project_index::ProjectIndex::cache_path_for(&root_path),
|
|
||||||
std::path::PathBuf::from,
|
|
||||||
);
|
|
||||||
let idx = crate::project_index::ProjectIndex::load(root_path.clone(), &cache_path)
|
|
||||||
.map_err(mlua::Error::external)?;
|
|
||||||
let symbol_count = idx.symbol_count();
|
|
||||||
let file_count = idx.file_count();
|
|
||||||
let mut ix_ref = ix.borrow_mut();
|
|
||||||
let key = idx.root.clone();
|
|
||||||
ix_ref.forget(&key);
|
|
||||||
let slot = ix_ref.ensure(key);
|
|
||||||
*slot = idx;
|
|
||||||
Ok((file_count, symbol_count))
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// stats(root) -> { files, symbols, generation } or nil.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"stats",
|
|
||||||
lua.create_function(move |lua, root: String| {
|
|
||||||
let ix_ref = ix.borrow();
|
|
||||||
let Some(idx) = ix_ref.get(std::path::Path::new(&root)) else {
|
|
||||||
return Ok(Value::Nil);
|
|
||||||
};
|
|
||||||
let t = lua.create_table_with_capacity(0, 4)?;
|
|
||||||
t.set("files", idx.file_count())?;
|
|
||||||
t.set("symbols", idx.symbol_count())?;
|
|
||||||
t.set("generation", idx.generation)?;
|
|
||||||
t.set("root", idx.root.display().to_string())?;
|
|
||||||
Ok(Value::Table(t))
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// roots() -> array of registered index roots.
|
|
||||||
let ix = indexer.clone();
|
|
||||||
m.set(
|
|
||||||
"roots",
|
|
||||||
lua.create_function(move |lua, ()| {
|
|
||||||
let ix_ref = ix.borrow();
|
|
||||||
let mut roots: Vec<String> =
|
|
||||||
ix_ref.roots().map(|p| p.display().to_string()).collect();
|
|
||||||
roots.sort();
|
|
||||||
let out = lua.create_table_with_capacity(roots.len(), 0)?;
|
|
||||||
for (i, r) in roots.iter().enumerate() {
|
|
||||||
out.set(i + 1, r.as_str())?;
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// hash(text) -> u64. Exposes FNV-1a so Lua callers can
|
|
||||||
// produce stable cache keys without a separate hash crate.
|
|
||||||
m.set(
|
|
||||||
"hash",
|
|
||||||
lua.create_function(|_, text: String| Ok(fnv1a_64(text.as_bytes())))?,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
pmacs.set("index", m)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a fresh [`ProjectIndexer`] and install `pmacs.index.*` over it.
|
|
||||||
pub fn make_project_indexer(lua: &Lua) -> mlua::Result<SharedProjectIndexer> {
|
|
||||||
let ix: SharedProjectIndexer = Rc::new(RefCell::new(ProjectIndexer::new()));
|
|
||||||
install_project_index(lua, &ix)?;
|
|
||||||
Ok(ix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// pmacs.completion: unified completion framework (T M4.11)
|
// pmacs.completion: unified completion framework (T M4.11)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue