use std::collections::HashMap;
use std::path::{Path, PathBuf};
use cpp_code_analysis::{PathPattern, WatchFilter};
use lsp_types::Uri;
use crate::handlers::ClientConfig;
use crate::util::uri_to_file_path;
#[derive(Debug)]
pub struct WorkspaceState {
pub roots: Vec<PathBuf>,
pub open_files: HashMap<Uri, String>,
pub client_config: ClientConfig,
config_file: Option<PathBuf>,
excludes: Vec<PathPattern>,
}
impl WorkspaceState {
pub fn new(client_config: ClientConfig) -> Self {
let mut state = Self {
roots: Vec::new(),
open_files: HashMap::new(),
client_config: ClientConfig::default(),
config_file: None,
excludes: Vec::new(),
};
state.set_client_config(client_config);
state
}
pub fn root(&self) -> Option<&Path> {
self.roots.first().map(PathBuf::as_path)
}
pub fn set_roots(&mut self, roots: Vec<PathBuf>) {
self.roots = roots;
}
pub fn set_config_file(&mut self, config_file: Option<PathBuf>) {
self.config_file = config_file;
}
pub fn config_file(&self) -> Option<&Path> {
self.config_file.as_deref()
}
pub fn set_client_config(&mut self, client_config: ClientConfig) {
self.excludes = client_config
.exclude
.iter()
.filter_map(|written| match PathPattern::new(written) {
Ok(pattern) => Some(pattern),
Err(error) => {
log::warn!("ignoring the exclude pattern {written:?}: {error}");
None
}
})
.collect();
self.client_config = client_config;
}
pub fn sync_open_file(&mut self, uri: Uri, text: String) {
self.open_files.insert(uri, text);
}
pub fn close_open_file(&mut self, uri: &Uri) {
self.open_files.remove(uri);
}
pub fn is_open_file(&self, uri: &Uri) -> bool {
self.open_files.contains_key(uri)
}
pub fn workspace_open_files(&self) -> Vec<(Uri, String)> {
self.open_files
.iter()
.filter(|(uri, _)| self.is_workspace_file(uri))
.map(|(uri, text)| (uri.clone(), text.clone()))
.collect()
}
pub fn is_workspace_file(&self, uri: &Uri) -> bool {
if self.roots.is_empty() {
return true;
}
let Some(path) = uri_to_file_path(uri) else {
return true;
};
self.contains(&path)
}
pub fn contains(&self, path: &Path) -> bool {
if self.roots.is_empty() {
return true;
}
if !self
.roots
.iter()
.any(|root| crate::util::is_inside(path, root))
{
return false;
}
!self
.watch_filter()
.is_some_and(|filter| filter.is_ignored(path))
}
pub fn watch_filter(&self) -> Option<WatchFilter> {
let root = self.root()?;
let mut filter = WatchFilter::new(root);
for pattern in &self.excludes {
filter = filter.ignore_pattern(pattern.clone());
}
Some(filter)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uri(path: &str) -> Uri {
format!("file:///{path}").parse().expect("a file URI")
}
fn path_of(written: &str) -> PathBuf {
uri_to_file_path(&uri(written)).expect("a file URI names a path")
}
fn state(roots: &[&str], exclude: &[&str]) -> WorkspaceState {
let mut state = WorkspaceState::new(ClientConfig {
exclude: exclude.iter().map(|entry| entry.to_string()).collect(),
..ClientConfig::default()
});
state.set_roots(roots.iter().map(|root| path_of(root)).collect());
state
}
#[test]
fn a_file_inside_a_root_is_the_workspaces_and_one_outside_it_is_not() {
let state = state(&["p"], &[]);
assert!(state.is_workspace_file(&uri("p/src/main.cpp")));
assert!(
!state.is_workspace_file(&uri("other/lib.h")),
"a file outside every root is not this workspace's source"
);
}
#[test]
fn an_excluded_directory_is_not_the_workspaces() {
let state = state(&["p"], &["**/build/**", "generated/*.h"]);
assert!(!state.contains(&path_of("p/build/generated.cpp")));
assert!(!state.contains(&path_of("p/generated/api.h")));
assert!(state.contains(&path_of("p/src/main.cpp")));
let filter = state.watch_filter().expect("the root is named");
assert!(filter.is_ignored(&path_of("p/build/generated.cpp")));
assert!(!filter.is_ignored(&path_of("p/src/main.cpp")));
}
#[test]
fn a_buffer_is_open_until_it_is_closed() {
let mut state = state(&["p"], &[]);
let path = uri("p/src/main.cpp");
assert!(!state.is_open_file(&path));
state.sync_open_file(path.clone(), "int x;\n".to_string());
assert!(state.is_open_file(&path));
assert_eq!(state.workspace_open_files().len(), 1);
state.close_open_file(&path);
assert!(!state.is_open_file(&path));
assert!(state.workspace_open_files().is_empty());
}
}