use anyhow::{Result, anyhow};
use globset::{GlobBuilder, GlobMatcher};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::runtime::Handle;
use tokio::sync::RwLock;
use super::diagnostics_server::DiagnosticsServer;
use super::file_tools::GlobServer;
use super::filesystem_manager::FilesystemManager;
use super::grep_server::GrepServer;
use super::handler::expand_tilde;
use super::path_security::PathValidator;
use crate::config::Config;
use crate::lsp::LspClientManager;
use crate::session::MessageLog;
pub struct ResolvedGlob {
matcher: GlobMatcher,
match_full_path: bool,
override_root: Option<PathBuf>,
}
impl ResolvedGlob {
pub fn new(pattern: &str) -> Result<Self> {
let expanded = expand_tilde(pattern);
let matcher = GlobBuilder::new(&expanded)
.literal_separator(true)
.build()
.map_err(|e| anyhow!("Invalid glob pattern: {e}"))?
.compile_matcher();
if Path::new(&expanded).is_absolute() {
let base = Self::base_dir(&expanded);
Ok(Self {
matcher,
match_full_path: true,
override_root: Some(base),
})
} else {
Ok(Self {
matcher,
match_full_path: false,
override_root: None,
})
}
}
#[must_use]
pub fn is_match(&self, path: &Path, root: &Path) -> bool {
if self.match_full_path {
self.matcher.is_match(path)
} else {
let rel = path.strip_prefix(root).unwrap_or(path);
self.matcher.is_match(rel)
}
}
#[must_use]
pub fn override_root(&self) -> Option<&Path> {
self.override_root.as_deref()
}
fn base_dir(pattern: &str) -> PathBuf {
let mut base = PathBuf::new();
for component in Path::new(pattern).components() {
let s = component.as_os_str().to_string_lossy();
if s.contains('*') || s.contains('?') || s.contains('[') || s.contains('{') {
break;
}
base.push(component);
}
if base.as_os_str().is_empty() {
PathBuf::from("/")
} else {
base
}
}
}
pub struct Toolbox {
pub grep: GrepServer,
pub glob: GlobServer,
pub diagnostics: Arc<DiagnosticsServer>,
pub(super) client_manager: Arc<LspClientManager>,
fs_manager: Arc<FilesystemManager>,
path_validator: Arc<RwLock<PathValidator>>,
pub runtime: Handle,
}
impl Toolbox {
#[must_use]
pub fn new(
config: Config,
roots: Vec<PathBuf>,
message_log: Arc<MessageLog>,
session_id: String,
runtime: Handle,
) -> Self {
let fs_manager = Arc::new(FilesystemManager::new());
fs_manager.set_roots(roots.clone());
let path_validator = Arc::new(RwLock::new(PathValidator::new(roots.clone())));
let client_manager = Arc::new(LspClientManager::new(
config,
roots,
message_log,
fs_manager.clone(),
session_id,
));
let diagnostics = Arc::new(DiagnosticsServer::new(
client_manager.clone(),
path_validator.clone(),
));
let notified_offline = Arc::new(std::sync::Mutex::new(HashSet::new()));
let grep = GrepServer {
client_manager: client_manager.clone(),
fs_manager: fs_manager.clone(),
notified_offline: notified_offline.clone(),
};
let glob = GlobServer {
client_manager: client_manager.clone(),
fs_manager: fs_manager.clone(),
notified_offline,
};
Self {
grep,
glob,
diagnostics,
client_manager,
fs_manager,
path_validator,
runtime,
}
}
pub async fn spawn_all(&self) {
self.client_manager.spawn_all().await;
}
pub async fn sync_roots(&self, roots: Vec<PathBuf>) -> Result<()> {
self.fs_manager.set_roots(roots.clone());
self.path_validator
.write()
.await
.update_roots(roots.clone());
self.client_manager.sync_roots(roots).await?;
let cm = self.client_manager.clone();
tokio::spawn(async move { cm.spawn_all().await });
Ok(())
}
pub async fn shutdown(&self) {
self.client_manager.shutdown_all().await;
}
}
#[cfg(test)]
#[allow(clippy::expect_used, reason = "test assertions")]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn expand_tilde_home_prefix() {
let home = std::env::var("HOME").expect("HOME must be set");
assert_eq!(expand_tilde("~/foo/bar"), format!("{home}/foo/bar"));
}
#[test]
fn expand_tilde_bare() {
let home = std::env::var("HOME").expect("HOME must be set");
assert_eq!(expand_tilde("~"), home);
}
#[test]
fn expand_tilde_no_op_for_absolute() {
assert_eq!(expand_tilde("/usr/bin"), "/usr/bin");
}
#[test]
fn expand_tilde_no_op_for_relative() {
assert_eq!(expand_tilde("src/main.rs"), "src/main.rs");
}
#[test]
fn expand_tilde_no_op_for_mid_tilde() {
assert_eq!(expand_tilde("foo/~/bar"), "foo/~/bar");
}
#[test]
fn base_dir_strips_at_star() {
let base = ResolvedGlob::base_dir("/home/user/projects/*");
assert_eq!(base, Path::new("/home/user/projects"));
}
#[test]
fn base_dir_strips_at_double_star() {
let base = ResolvedGlob::base_dir("/home/user/**/*.rs");
assert_eq!(base, Path::new("/home/user"));
}
#[test]
fn base_dir_strips_at_question_mark() {
let base = ResolvedGlob::base_dir("/tmp/foo?/bar");
assert_eq!(base, Path::new("/tmp"));
}
#[test]
fn base_dir_strips_at_bracket() {
let base = ResolvedGlob::base_dir("/tmp/[abc]/bar");
assert_eq!(base, Path::new("/tmp"));
}
#[test]
fn base_dir_no_metachar_returns_full_path() {
let base = ResolvedGlob::base_dir("/home/user/projects/src");
assert_eq!(base, Path::new("/home/user/projects/src"));
}
#[test]
fn base_dir_only_metachar_returns_root() {
let base = ResolvedGlob::base_dir("*");
assert_eq!(base, Path::new("/"));
}
#[test]
fn resolved_glob_relative_pattern() {
let rg = ResolvedGlob::new("src/**/*.rs").expect("valid glob");
assert!(rg.override_root().is_none());
assert!(!rg.match_full_path);
}
#[test]
fn resolved_glob_absolute_pattern() {
let rg = ResolvedGlob::new("/tmp/project/*.rs").expect("valid glob");
assert_eq!(rg.override_root(), Some(Path::new("/tmp/project")));
assert!(rg.match_full_path);
}
#[test]
fn resolved_glob_tilde_becomes_absolute() {
let rg = ResolvedGlob::new("~/projects/*.rs").expect("valid glob");
assert!(rg.override_root().is_some());
assert!(rg.match_full_path);
}
#[test]
fn resolved_glob_invalid_pattern() {
assert!(ResolvedGlob::new("[invalid").is_err());
}
#[test]
fn is_match_relative_strips_root() {
let rg = ResolvedGlob::new("src/**/*.rs").expect("valid glob");
let root = Path::new("/workspace");
assert!(rg.is_match(Path::new("/workspace/src/lib.rs"), root));
assert!(rg.is_match(Path::new("/workspace/src/deep/mod.rs"), root));
assert!(!rg.is_match(Path::new("/workspace/tests/foo.rs"), root));
}
#[test]
fn is_match_relative_star_no_cross_directory() {
let rg = ResolvedGlob::new("src/*.rs").expect("valid glob");
let root = Path::new("/workspace");
assert!(rg.is_match(Path::new("/workspace/src/lib.rs"), root));
assert!(!rg.is_match(Path::new("/workspace/src/deep/mod.rs"), root));
}
#[test]
fn is_match_absolute_uses_full_path() {
let rg = ResolvedGlob::new("/tmp/project/*.rs").expect("valid glob");
let root = Path::new("/tmp/project");
assert!(rg.is_match(Path::new("/tmp/project/main.rs"), root));
assert!(!rg.is_match(Path::new("/tmp/project/sub/lib.rs"), root));
assert!(!rg.is_match(Path::new("/other/main.rs"), root));
}
#[test]
fn is_match_absolute_double_star() {
let rg = ResolvedGlob::new("/tmp/project/**/*.rs").expect("valid glob");
let root = Path::new("/tmp/project");
assert!(rg.is_match(Path::new("/tmp/project/main.rs"), root));
assert!(rg.is_match(Path::new("/tmp/project/sub/lib.rs"), root));
assert!(!rg.is_match(Path::new("/other/main.rs"), root));
}
#[test]
fn is_match_relative_wrong_root_still_tries() {
let rg = ResolvedGlob::new("*.txt").expect("valid glob");
assert!(rg.is_match(Path::new("notes.txt"), Path::new("/nonexistent")));
}
}