use std::cell::{Ref, RefCell, RefMut};
use std::path::{Component, Path, PathBuf};
use std::sync::mpsc;
use notify::RecommendedWatcher;
use crate::backup::BackupStore;
use crate::callgraph::CallGraph;
use crate::checkpoint::CheckpointStore;
use crate::config::Config;
use crate::language::LanguageProvider;
use crate::lsp::manager::LspManager;
use crate::search_index::SearchIndex;
use crate::semantic_index::SemanticIndex;
#[derive(Debug, Clone)]
pub enum SemanticIndexStatus {
Disabled,
Building {
stage: String,
files: Option<usize>,
entries_done: Option<usize>,
entries_total: Option<usize>,
},
Ready,
Failed(String),
}
pub enum SemanticIndexEvent {
Progress {
stage: String,
files: Option<usize>,
entries_done: Option<usize>,
entries_total: Option<usize>,
},
Ready(SemanticIndex),
Failed(String),
}
fn normalize_path(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::ParentDir => {
if !result.pop() {
result.push(component);
}
}
Component::CurDir => {} _ => result.push(component),
}
}
result
}
fn resolve_with_existing_ancestors(path: &Path) -> PathBuf {
let mut existing = path.to_path_buf();
let mut tail_segments = Vec::new();
while !existing.exists() {
if let Some(name) = existing.file_name() {
tail_segments.push(name.to_owned());
} else {
break;
}
existing = match existing.parent() {
Some(parent) => parent.to_path_buf(),
None => break,
};
}
let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
for segment in tail_segments.into_iter().rev() {
resolved.push(segment);
}
resolved
}
pub struct AppContext {
provider: Box<dyn LanguageProvider>,
backup: RefCell<BackupStore>,
checkpoint: RefCell<CheckpointStore>,
config: RefCell<Config>,
callgraph: RefCell<Option<CallGraph>>,
search_index: RefCell<Option<SearchIndex>>,
search_index_rx:
RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>,
semantic_index: RefCell<Option<SemanticIndex>>,
semantic_index_rx: RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>>,
semantic_index_status: RefCell<SemanticIndexStatus>,
semantic_embedding_model: RefCell<Option<crate::semantic_index::EmbeddingModel>>,
watcher: RefCell<Option<RecommendedWatcher>>,
watcher_rx: RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>>,
lsp_manager: RefCell<LspManager>,
}
impl AppContext {
pub fn new(provider: Box<dyn LanguageProvider>, config: Config) -> Self {
AppContext {
provider,
backup: RefCell::new(BackupStore::new()),
checkpoint: RefCell::new(CheckpointStore::new()),
config: RefCell::new(config),
callgraph: RefCell::new(None),
search_index: RefCell::new(None),
search_index_rx: RefCell::new(None),
semantic_index: RefCell::new(None),
semantic_index_rx: RefCell::new(None),
semantic_index_status: RefCell::new(SemanticIndexStatus::Disabled),
semantic_embedding_model: RefCell::new(None),
watcher: RefCell::new(None),
watcher_rx: RefCell::new(None),
lsp_manager: RefCell::new(LspManager::new()),
}
}
pub fn provider(&self) -> &dyn LanguageProvider {
self.provider.as_ref()
}
pub fn backup(&self) -> &RefCell<BackupStore> {
&self.backup
}
pub fn checkpoint(&self) -> &RefCell<CheckpointStore> {
&self.checkpoint
}
pub fn config(&self) -> Ref<'_, Config> {
self.config.borrow()
}
pub fn config_mut(&self) -> RefMut<'_, Config> {
self.config.borrow_mut()
}
pub fn callgraph(&self) -> &RefCell<Option<CallGraph>> {
&self.callgraph
}
pub fn search_index(&self) -> &RefCell<Option<SearchIndex>> {
&self.search_index
}
pub fn search_index_rx(
&self,
) -> &RefCell<Option<crossbeam_channel::Receiver<(SearchIndex, crate::parser::SymbolCache)>>>
{
&self.search_index_rx
}
pub fn semantic_index(&self) -> &RefCell<Option<SemanticIndex>> {
&self.semantic_index
}
pub fn semantic_index_rx(
&self,
) -> &RefCell<Option<crossbeam_channel::Receiver<SemanticIndexEvent>>> {
&self.semantic_index_rx
}
pub fn semantic_index_status(&self) -> &RefCell<SemanticIndexStatus> {
&self.semantic_index_status
}
pub fn semantic_embedding_model(
&self,
) -> &RefCell<Option<crate::semantic_index::EmbeddingModel>> {
&self.semantic_embedding_model
}
pub fn watcher(&self) -> &RefCell<Option<RecommendedWatcher>> {
&self.watcher
}
pub fn watcher_rx(&self) -> &RefCell<Option<mpsc::Receiver<notify::Result<notify::Event>>>> {
&self.watcher_rx
}
pub fn lsp(&self) -> RefMut<'_, LspManager> {
self.lsp_manager.borrow_mut()
}
pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
if let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() {
let config = self.config();
if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
log::warn!("sync error for {}: {}", file_path.display(), e);
}
}
}
pub fn lsp_notify_and_collect_diagnostics(
&self,
file_path: &Path,
content: &str,
timeout: std::time::Duration,
) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
let Ok(mut lsp) = self.lsp_manager.try_borrow_mut() else {
return Vec::new();
};
lsp.drain_events();
let config = self.config();
if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
log::warn!("sync error for {}: {}", file_path.display(), e);
return Vec::new();
}
lsp.wait_for_diagnostics(file_path, &config, timeout)
}
pub fn lsp_post_write(
&self,
file_path: &Path,
content: &str,
params: &serde_json::Value,
) -> Vec<crate::lsp::diagnostics::StoredDiagnostic> {
let wants_diagnostics = params
.get("diagnostics")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !wants_diagnostics {
self.lsp_notify_file_changed(file_path, content);
return Vec::new();
}
let wait_ms = params
.get("wait_ms")
.and_then(|v| v.as_u64())
.unwrap_or(1500)
.min(10_000);
self.lsp_notify_and_collect_diagnostics(
file_path,
content,
std::time::Duration::from_millis(wait_ms),
)
}
pub fn validate_path(
&self,
req_id: &str,
path: &Path,
) -> Result<std::path::PathBuf, crate::protocol::Response> {
let config = self.config();
if !config.restrict_to_project_root {
return Ok(path.to_path_buf());
}
let root = match &config.project_root {
Some(r) => r.clone(),
None => return Ok(path.to_path_buf()), };
drop(config);
let resolved = std::fs::canonicalize(path)
.unwrap_or_else(|_| resolve_with_existing_ancestors(&normalize_path(path)));
let resolved_root = std::fs::canonicalize(&root).unwrap_or(root);
if !resolved.starts_with(&resolved_root) {
return Err(crate::protocol::Response::error(
req_id,
"path_outside_root",
format!(
"path '{}' is outside the project root '{}'",
path.display(),
resolved_root.display()
),
));
}
Ok(resolved)
}
pub fn lsp_server_count(&self) -> usize {
self.lsp_manager
.try_borrow()
.map(|lsp| lsp.server_count())
.unwrap_or(0)
}
pub fn symbol_cache_stats(&self) -> serde_json::Value {
if let Some(tsp) = self
.provider
.as_any()
.downcast_ref::<crate::parser::TreeSitterProvider>()
{
let (local, warm) = tsp.symbol_cache_stats();
serde_json::json!({
"local_entries": local,
"warm_entries": warm,
})
} else {
serde_json::json!({
"local_entries": 0,
"warm_entries": 0,
})
}
}
}