use std::path::PathBuf;
use thiserror::Error;
use crate::extract::{FileMapL1, FileMapL2, Symbol, SymbolKind};
use crate::path::RelPath;
use crate::store::{Store, StoreError};
#[derive(Debug, Error)]
pub enum QueryError {
#[error("store error: {0}")]
Store(#[from] StoreError),
#[error("file not indexed: {0}")]
NotIndexed(String),
#[error("blob missing for indexed file (likely .basemind/blobs/ was cleaned): {0}")]
BlobMissing(String),
}
#[derive(Debug, Clone)]
pub struct SymbolHit {
pub path: RelPath,
pub symbol: Symbol,
}
pub fn file_outline(store: &Store, rel: impl AsRef<[u8]>) -> Result<FileMapL1, QueryError> {
let rel_bytes = rel.as_ref();
let entry = store
.lookup(rel_bytes)
.ok_or_else(|| QueryError::NotIndexed(String::from_utf8_lossy(rel_bytes).into_owned()))?;
let l1 = store
.read_l1_by_hex(&entry.hash_hex)?
.ok_or_else(|| QueryError::BlobMissing(String::from_utf8_lossy(rel_bytes).into_owned()))?;
Ok(l1)
}
pub fn file_outline_l2(store: &Store, rel: impl AsRef<[u8]>, root: &std::path::Path) -> Result<FileMapL2, QueryError> {
let rel_bytes = rel.as_ref();
let rel_display = String::from_utf8_lossy(rel_bytes).into_owned();
let entry = store
.lookup(rel_bytes)
.ok_or_else(|| QueryError::NotIndexed(rel_display.clone()))?;
if let Some(l2) = store.read_l2_by_hex(&entry.hash_hex)? {
return Ok(l2);
}
let l1 = store
.read_l1_by_hex(&entry.hash_hex)?
.ok_or_else(|| QueryError::BlobMissing(rel_display.clone()))?;
let rel_path = RelPath::from(rel_bytes);
let abs = root.join(rel_path.to_path_buf());
let bytes = std::fs::read(&abs).map_err(|source| {
QueryError::Store(StoreError::Io {
path: abs.clone(),
source,
})
})?;
let lang = crate::lang::intern(&entry.language)
.ok_or_else(|| QueryError::NotIndexed(format!("unknown language {}", entry.language)))?;
let l2 = crate::extract::l2::extract_l2(lang, &bytes).map_err(|e| {
QueryError::Store(StoreError::Io {
path: abs,
source: std::io::Error::other(format!("{e}")),
})
})?;
store.write_filemap_hex(&entry.hash_hex, &l1, Some(&l2))?;
Ok(l2)
}
pub fn search_symbols(store: &Store, needle: &str, kind: Option<SymbolKind>) -> Result<Vec<SymbolHit>, QueryError> {
if needle.is_empty() {
return Ok(Vec::new());
}
let finder = memchr::memmem::Finder::new(needle.as_bytes());
let mut out = Vec::new();
for (rel, entry) in &store.index.files {
let l1 = match store.read_l1_by_hex(&entry.hash_hex)? {
Some(m) => m,
None => continue,
};
for sym in l1.symbols {
if finder.find(sym.name.as_bytes()).is_none() {
continue;
}
if let Some(k) = kind
&& sym.kind != k
{
continue;
}
out.push(SymbolHit {
path: rel.clone(),
symbol: sym,
});
}
}
Ok(out)
}
pub fn dependents_of(store: &Store, module: &str) -> Result<Vec<RelPath>, QueryError> {
let mut by_path: Vec<(PathBuf, Vec<crate::extract::Import>)> = Vec::with_capacity(store.index.files.len());
for (rel, entry) in &store.index.files {
let l1 = match store.read_l1_by_hex(&entry.hash_hex)? {
Some(m) => m,
None => continue,
};
by_path.push((rel.to_path_buf(), l1.imports));
}
let paths = crate::extract::l3::dependents_of(module, &by_path);
Ok(paths.into_iter().map(|p| RelPath::from(p.as_path())).collect())
}
pub fn resolved_references(store: &Store, def_path: &RelPath, def_start: u32) -> Vec<(RelPath, u32)> {
match store.index_db.as_ref() {
Some(index) => index.references_to(def_path, def_start),
None => resolved_references_from_blob(store, def_path, def_start),
}
}
fn resolved_references_from_blob(store: &Store, def_path: &RelPath, def_start: u32) -> Vec<(RelPath, u32)> {
let Some(entry) = store.lookup(def_path) else {
return Vec::new();
};
let refs = match store.read_resolved_by_hex(&entry.hash_hex) {
Ok(Some(refs)) => refs,
Ok(None) => return Vec::new(),
Err(error) => {
tracing::debug!(path = %def_path, %error, "resolved_references: resolution blob unreadable — no intra refs");
return Vec::new();
}
};
refs.intra
.iter()
.filter(|edge| edge.def_start == def_start)
.map(|edge| (def_path.clone(), edge.use_start))
.collect()
}
pub fn definition_of(store: &Store, use_path: &RelPath, use_start: u32) -> Option<(RelPath, u32)> {
match store.index_db.as_ref() {
Some(index) => index.definition_of(use_path, use_start),
None => definition_of_from_blob(store, use_path, use_start),
}
}
fn definition_of_from_blob(store: &Store, use_path: &RelPath, use_start: u32) -> Option<(RelPath, u32)> {
let entry = store.lookup(use_path)?;
let refs = store.read_resolved_by_hex(&entry.hash_hex).ok().flatten()?;
refs.intra
.iter()
.find(|edge| edge.use_start == use_start)
.map(|edge| (use_path.clone(), edge.def_start))
}
#[cfg(all(test, feature = "code-intel-js"))]
mod tests {
use super::*;
use crate::config::ConfigV1;
use crate::scanner::{ScanSource, scan};
use crate::store::{Store, VIEW_WORKING};
fn scan_foo_fixture(root: &std::path::Path) -> Store {
std::fs::write(
root.join("u.ts"),
b"export function foo() { return 1; }\nfoo();\nfoo();\n",
)
.expect("u.ts");
let mut store = Store::open(root, VIEW_WORKING).expect("open");
scan(
root,
&mut store,
&ConfigV1::with_defaults(),
ScanSource::WorkingTree,
crate::scanner::EmbedMode::Inline,
)
.expect("scan");
store
}
#[test]
fn resolved_references_blob_fallback_matches_index_for_intra_refs() {
let dir = tempfile::tempdir().expect("tempdir");
let store = scan_foo_fixture(dir.path());
let path = RelPath::from("u.ts".as_bytes());
let entry = store.lookup(&path).expect("indexed");
let refs = store
.read_resolved_by_hex(&entry.hash_hex)
.expect("read blob")
.expect("resolution facts present");
let def_start = refs
.intra
.first()
.map(|e| e.def_start)
.expect("at least one intra edge");
let via_blob = resolved_references_from_blob(&store, &path, def_start);
let via_index = store
.index_db
.as_ref()
.expect("index open")
.references_to(&path, def_start);
assert!(!via_blob.is_empty(), "blob fallback must find the intra uses");
assert_eq!(
via_blob.len(),
via_index.len(),
"blob fallback and Fjall index must agree for single-file (all-intra) refs"
);
assert!(
via_blob.iter().all(|(p, _)| *p == path),
"single-file fixture → all uses in u.ts"
);
}
#[test]
fn definition_of_blob_fallback_returns_intra_definition() {
let dir = tempfile::tempdir().expect("tempdir");
let store = scan_foo_fixture(dir.path());
let path = RelPath::from("u.ts".as_bytes());
let entry = store.lookup(&path).expect("indexed");
let refs = store
.read_resolved_by_hex(&entry.hash_hex)
.expect("read blob")
.expect("resolution facts present");
let edge = refs.intra.first().cloned().expect("at least one intra edge");
let def = definition_of_from_blob(&store, &path, edge.use_start);
assert_eq!(
def,
Some((path.clone(), edge.def_start)),
"blob fallback resolves the use back to its in-file definition"
);
}
}