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
|
||||
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.
|
||||
1. **`index` (the one genuinely clean remaining leaf).** Zero shared-core
|
||||
coupling; establishes the `pub(crate) use` re-export for external
|
||||
callers (`editor.rs`, `completion_framework.rs`) and `super::` access
|
||||
to a stranded helper (`lua_to_json`). **Correction after tranche 1:**
|
||||
the recon under-counted the misplaced helpers. Beyond the JSON/ANSI
|
||||
clusters, the *style/color* converters (`lua_to_style`/`style_to_lua`/
|
||||
`color_to_lua`/…) sit in the `theme` section, and `caller_source` +
|
||||
the command/menu builders (`build_command_from_spec`,
|
||||
`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`,
|
||||
`completion` (edge-free once the JSON helpers are hoisted).
|
||||
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,
|
||||
which drive `pmacs.diag.*` through the Lua VM, are the behavioral oracle —
|
||||
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.
|
||||
// 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.
|
||||
// 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 index;
|
||||
pub use index::{SharedProjectIndexer, make_project_indexer};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared registry alias
|
||||
|
|
@ -10122,389 +10126,6 @@ pub fn make_workspace(lua: &Lua, lsp_manager: &SharedLspManager) -> mlua::Result
|
|||
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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue