use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use scryer_db::{Project, ProjectRegistry};
#[derive(Clone)]
pub struct ProjectContextResolver {
registry: Arc<ProjectRegistry>,
active_project_id: Arc<RwLock<Option<u64>>>,
session_roots: Arc<RwLock<Vec<PathBuf>>>,
session_cwd: Arc<RwLock<Option<PathBuf>>>,
}
impl ProjectContextResolver {
pub fn new(registry: Arc<ProjectRegistry>) -> Self {
Self {
registry,
active_project_id: Arc::new(RwLock::new(None)),
session_roots: Arc::new(RwLock::new(Vec::new())),
session_cwd: Arc::new(RwLock::new(None)),
}
}
pub fn registry(&self) -> &Arc<ProjectRegistry> {
&self.registry
}
pub async fn set_active_project(&self, project_id: u64) {
let mut guard = self.active_project_id.write().await;
*guard = Some(project_id);
}
pub async fn clear_active_project(&self, project_id: u64) {
let mut guard = self.active_project_id.write().await;
if *guard == Some(project_id) {
*guard = None;
}
}
pub async fn get_active_project(&self) -> Option<u64> {
*self.active_project_id.read().await
}
pub async fn set_session_roots(&self, roots: Vec<PathBuf>) {
let mut guard = self.session_roots.write().await;
*guard = roots;
}
pub async fn set_session_cwd(&self, cwd: PathBuf) {
{
let mut roots = self.session_roots.write().await;
if roots.is_empty() {
roots.push(cwd.clone());
}
}
*self.session_cwd.write().await = Some(cwd);
}
pub async fn session_cwd(&self) -> Option<PathBuf> {
self.session_cwd.read().await.clone()
}
pub async fn absolutize(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
return path.to_path_buf();
}
match self.session_cwd.read().await.as_ref() {
Some(cwd) => cwd.join(path),
None => path.to_path_buf(),
}
}
pub async fn resolve_project(
&self,
file_path: Option<&Path>,
project_param: Option<&str>,
) -> anyhow::Result<(Project, Option<PathBuf>)> {
let absolute = match file_path {
Some(path) => Some(self.absolutize(path).await),
None => None,
};
let file_path = absolute.as_deref();
let explicit_project = match project_param.map(str::trim).filter(|p| !p.is_empty()) {
Some(wanted) => Some(self.registry.find(wanted).await.ok_or_else(|| {
anyhow::anyhow!(
"No registered project matches '{wanted}'. Use list_projects to see project slugs, IDs and root paths, or index_workspace to register one."
)
})?),
None => None,
};
if let Some(path) = file_path
&& let Some((proj, rel_path)) = self.registry.resolve_path(path).await
{
return Ok((proj, Some(rel_path)));
}
if let Some(proj) = explicit_project {
let rel = file_path.and_then(|p| compute_relative(&proj, p));
return Ok((proj, rel));
}
let active_id = *self.active_project_id.read().await;
if let Some(id) = active_id {
let all = self.registry.list_projects().await;
if let Some(proj) = all.into_iter().find(|p| p.id == id) {
let rel = file_path.and_then(|p| compute_relative(&proj, p));
return Ok((proj, rel));
}
}
let session_roots = self.session_roots.read().await;
for root in session_roots.iter() {
if let Some((proj, _)) = self.registry.resolve_path(root).await {
let rel = file_path.and_then(|p| compute_relative(&proj, p));
return Ok((proj, rel));
}
}
let all = self.registry.list_projects().await;
if all.len() == 1 {
let proj = all[0].clone();
let rel = file_path.and_then(|p| compute_relative(&proj, p));
return Ok((proj, rel));
}
anyhow::bail!(
"Could not resolve project context: file_path={:?}, project={:?}. No matching project found in registry.",
file_path,
project_param
)
}
}
fn compute_relative(project: &Project, file_path: &Path) -> Option<PathBuf> {
let canonical_file = dunce::canonicalize(file_path).unwrap_or_else(|_| file_path.to_path_buf());
let canonical_root = dunce::canonicalize(&project.root_path)
.unwrap_or_else(|_| PathBuf::from(&project.root_path));
canonical_file
.strip_prefix(&canonical_root)
.ok()
.map(|p| p.to_path_buf())
}