use std::path::{Path, PathBuf};
use crate::graph::unified::NodeKind;
use crate::query::QueryExecutor;
use crate::session::SessionManager;
use super::error::{WorkspaceError, WorkspaceResult};
use super::registry::{WorkspaceRegistry, WorkspaceRepository};
pub struct WorkspaceIndex {
registry: WorkspaceRegistry,
session: SessionManager,
workspace_root: PathBuf,
}
impl WorkspaceIndex {
pub fn open(workspace_root: impl Into<PathBuf>, registry_path: &Path) -> WorkspaceResult<Self> {
let workspace_root = workspace_root.into();
let registry = WorkspaceRegistry::load(registry_path)?;
let session = SessionManager::new().map_err(|e| WorkspaceError::QueryParsing {
message: format!("Failed to create SessionManager: {e}"),
})?;
Ok(Self {
registry,
session,
workspace_root,
})
}
pub fn new(
workspace_root: impl Into<PathBuf>,
registry: WorkspaceRegistry,
session: SessionManager,
) -> Self {
Self {
registry,
session,
workspace_root: workspace_root.into(),
}
}
pub fn query(&mut self, query: &str) -> WorkspaceResult<Vec<NodeWithRepo>> {
let executor = QueryExecutor::new();
let parsed = executor
.parse_query_ast(query)
.map_err(|e| WorkspaceError::QueryParsing {
message: format!("Failed to parse query: {e}"),
})?;
let repos: Vec<&WorkspaceRepository> = self
.registry
.repositories
.iter()
.filter(|repo| parsed.repo_filter.matches(&repo.name))
.collect();
if repos.is_empty() {
return Ok(Vec::new());
}
let query_str = parsed.normalized.as_ref();
let mut all_results = Vec::new();
for repo in repos {
let repo_path = if repo.root.is_absolute() {
repo.root.clone()
} else {
self.workspace_root.join(&repo.root)
};
match self.session.query(&repo_path, query_str) {
Ok(results) => {
for m in results.iter() {
let match_info = MatchInfo {
name: m.name().map(|s| s.to_string()).unwrap_or_default(),
qualified_name: m.qualified_name().map(|s| s.to_string()),
kind: m.kind(),
language: m.language().map(|lang| lang.to_string()),
file_path: m.relative_path().unwrap_or_default(),
start_line: m.start_line(),
start_column: m.start_column(),
end_line: m.end_line(),
end_column: m.end_column(),
is_static: m.is_static(),
signature: m.signature().map(|s| s.to_string()),
doc: m.doc().map(|s| s.to_string()),
};
all_results.push(NodeWithRepo {
match_info,
repo_name: repo.name.clone(),
repo_id: repo.id.clone(),
repo_path: repo_path.clone(),
});
}
}
Err(err) => {
eprintln!(
"Warning: Failed to query repository '{}': {}",
repo.name, err
);
}
}
}
Ok(all_results)
}
#[must_use]
pub fn stats(&self) -> WorkspaceStats {
let total_repos = self.registry.repositories.len();
let indexed_repos = self
.registry
.repositories
.iter()
.filter(|r| r.last_indexed_at.is_some())
.count();
let total_symbols = self
.registry
.repositories
.iter()
.filter_map(|r| r.symbol_count)
.sum();
WorkspaceStats {
total_repos,
indexed_repos,
total_symbols,
}
}
#[must_use]
pub fn detailed_stats(&self) -> super::stats::DetailedWorkspaceStats {
super::stats::DetailedWorkspaceStats::from_registry(&self.registry)
}
#[must_use]
pub fn registry(&self) -> &WorkspaceRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut WorkspaceRegistry {
&mut self.registry
}
#[must_use]
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
}
}
#[derive(Debug, Clone)]
pub struct MatchInfo {
pub name: String,
pub qualified_name: Option<String>,
pub kind: NodeKind,
pub language: Option<String>,
pub file_path: PathBuf,
pub start_line: u32,
pub start_column: u32,
pub end_line: u32,
pub end_column: u32,
pub is_static: bool,
pub signature: Option<String>,
pub doc: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NodeWithRepo {
pub match_info: MatchInfo,
pub repo_name: String,
pub repo_id: super::registry::WorkspaceRepoId,
pub repo_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct WorkspaceStats {
pub total_repos: usize,
pub indexed_repos: usize,
pub total_symbols: u64,
}