diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f08dd51..f8e5a865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Improve support for handling `Tectonic.toml` projects ([#1014](https://github.com/latex-lsp/texlab/issues/1014)) +- Cache results of project detection to improve performance + ### Fixed - Don't return document symbol with empty name if `\item[]` is encountered ([#1040](https://github.com/latex-lsp/texlab/issues/1040)) diff --git a/crates/base-db/src/deps.rs b/crates/base-db/src/deps.rs new file mode 100644 index 00000000..06689a0d --- /dev/null +++ b/crates/base-db/src/deps.rs @@ -0,0 +1,11 @@ +mod discover; +mod graph; +mod project; +mod root; + +pub use self::{ + discover::{discover, watch}, + graph::{DirectLinkData, Edge, EdgeData, Graph}, + project::{parents, Project}, + root::ProjectRoot, +}; diff --git a/crates/base-db/src/deps/discover.rs b/crates/base-db/src/deps/discover.rs new file mode 100644 index 00000000..06905905 --- /dev/null +++ b/crates/base-db/src/deps/discover.rs @@ -0,0 +1,117 @@ +use std::path::PathBuf; + +use distro::Language; +use itertools::Itertools; +use rustc_hash::FxHashSet; + +use crate::Workspace; + +use super::ProjectRoot; + +pub fn watch( + workspace: &mut Workspace, + watcher: &mut dyn notify::Watcher, + watched_dirs: &mut FxHashSet, +) { + let roots = workspace + .iter() + .map(|document| &document.dir) + .filter(|dir| dir.scheme() == "file") + .unique() + .map(|dir| ProjectRoot::walk_and_find(workspace, dir)); + + for root in roots { + for uri in [&root.src_dir, &root.aux_dir, &root.log_dir, &root.pdf_dir] { + if let Ok(path) = uri.to_file_path() { + if watched_dirs.insert(path.clone()) { + let _ = watcher.watch(&path, notify::RecursiveMode::NonRecursive); + } + } + } + } +} + +pub fn discover(workspace: &mut Workspace, checked_paths: &mut FxHashSet) { + loop { + let mut changed = false; + changed |= discover_parents(workspace, checked_paths); + changed |= discover_children(workspace, checked_paths); + if !changed { + break; + } + } +} + +fn discover_parents(workspace: &mut Workspace, checked_paths: &mut FxHashSet) -> bool { + let dirs = workspace + .iter() + .filter(|document| document.language != Language::Bib) + .filter_map(|document| document.path.as_deref()) + .flat_map(|path| path.ancestors().skip(1)) + .filter(|path| workspace.contains(path)) + .map(|path| path.to_path_buf()) + .collect::>(); + + let mut changed = false; + for dir in dirs { + if workspace + .iter() + .filter(|document| matches!(document.language, Language::Root | Language::Tectonic)) + .filter_map(|document| document.path.as_deref()) + .filter_map(|path| path.parent()) + .any(|marker| dir.starts_with(marker)) + { + continue; + } + + let Ok(entries) = std::fs::read_dir(dir) else { + continue; + }; + + for file in entries + .flatten() + .filter(|entry| entry.file_type().map_or(false, |type_| type_.is_file())) + .map(|entry| entry.path()) + { + let Some(lang) = Language::from_path(&file) else { + continue; + }; + + if !matches!( + lang, + Language::Tex | Language::Root | Language::Tectonic | Language::Latexmkrc + ) { + continue; + } + + if workspace.lookup_path(&file).is_none() && file.exists() { + changed |= workspace.load(&file, lang).is_ok(); + checked_paths.insert(file); + } + } + } + + changed +} + +fn discover_children(workspace: &mut Workspace, checked_paths: &mut FxHashSet) -> bool { + let files = workspace + .graphs() + .values() + .flat_map(|graph| graph.missing.iter()) + .filter(|uri| uri.scheme() == "file") + .flat_map(|uri| uri.to_file_path()) + .collect::>(); + + let mut changed = false; + for file in files { + let language = Language::from_path(&file).unwrap_or(Language::Tex); + + if workspace.lookup_path(&file).is_none() && file.exists() { + changed |= workspace.load(&file, language).is_ok(); + checked_paths.insert(file); + } + } + + changed +} diff --git a/crates/base-db/src/deps/graph.rs b/crates/base-db/src/deps/graph.rs new file mode 100644 index 00000000..a90d8fa2 --- /dev/null +++ b/crates/base-db/src/deps/graph.rs @@ -0,0 +1,256 @@ +use std::{ffi::OsStr, path::PathBuf, rc::Rc}; + +use distro::Language; +use itertools::Itertools; +use once_cell::sync::Lazy; +use percent_encoding::percent_decode_str; +use rustc_hash::FxHashSet; +use url::Url; + +use crate::{semantics, Document, Workspace}; + +use super::ProjectRoot; + +pub(crate) static HOME_DIR: Lazy> = Lazy::new(dirs::home_dir); + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct Edge { + pub source: Url, + pub target: Url, + pub data: EdgeData, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum EdgeData { + DirectLink(DirectLinkData), + AdditionalFiles, + Artifact, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct DirectLinkData { + pub link: semantics::tex::Link, + pub new_root: Option, +} + +#[derive(Debug, Clone, Copy)] +struct Start<'a, 'b> { + source: &'a Document, + root: &'b ProjectRoot, +} + +#[derive(Debug)] +pub struct Graph { + pub missing: Vec, + pub edges: Vec, + pub start: Url, +} + +impl Graph { + pub fn new(workspace: &Workspace, start: &Document) -> Self { + let mut graph = Self { + missing: Vec::new(), + edges: Vec::new(), + start: start.uri.clone(), + }; + + let root = ProjectRoot::walk_and_find(workspace, &start.dir); + + let mut stack = vec![(start, Rc::new(root))]; + let mut visited = FxHashSet::default(); + + while let Some((source, root)) = stack.pop() { + let index = graph.edges.len(); + + graph.process( + workspace, + Start { + source, + root: &root, + }, + ); + + for edge in &graph.edges[index..] { + if visited.insert(edge.target.clone()) { + let new_root = match &edge.data { + EdgeData::DirectLink(data) => data.new_root.clone(), + _ => None, + }; + + let new_root = new_root.map_or_else(|| Rc::clone(&root), Rc::new); + + stack.push((workspace.lookup(&edge.target).unwrap(), new_root)); + } + } + } + + graph + } + + pub fn preorder<'a: 'b, 'b>( + &'b self, + workspace: &'a Workspace, + ) -> impl DoubleEndedIterator + '_ { + std::iter::once(&self.start) + .chain(self.edges.iter().map(|group| &group.target)) + .unique() + .filter_map(|uri| workspace.lookup(uri)) + } + + fn process(&mut self, workspace: &Workspace, start: Start) { + self.add_direct_links(workspace, start); + self.add_artifacts(workspace, start); + self.add_additional_files(workspace, start); + } + + fn add_additional_files(&mut self, workspace: &Workspace, start: Start) { + for uri in &start.root.additional_files { + match workspace.lookup(uri) { + Some(target) => { + self.edges.push(Edge { + source: start.source.uri.clone(), + target: target.uri.clone(), + data: EdgeData::AdditionalFiles, + }); + } + None => { + self.missing.push(uri.clone()); + } + } + } + } + + fn add_direct_links(&mut self, workspace: &Workspace, start: Start) -> Option<()> { + let data = start.source.data.as_tex()?; + + for link in &data.semantics.links { + self.add_direct_link(workspace, start, link); + } + + Some(()) + } + + fn add_direct_link( + &mut self, + workspace: &Workspace, + start: Start, + link: &semantics::tex::Link, + ) { + let home_dir = HOME_DIR.as_deref(); + + let stem = &link.path.text; + let mut file_names = vec![stem.clone()]; + link.kind + .extensions() + .iter() + .map(|ext| format!("{stem}.{ext}")) + .for_each(|name| file_names.push(name)); + + let file_name_db = &workspace.distro().file_name_db; + let distro_files = file_names + .iter() + .filter_map(|name| file_name_db.get(name)) + .filter(|path| { + home_dir.map_or(false, |dir| path.starts_with(dir)) + || Language::from_path(path) == Some(Language::Bib) + }) + .flat_map(Url::from_file_path); + + for target_uri in file_names + .iter() + .flat_map(|file_name| start.root.src_dir.join(file_name)) + .chain(distro_files) + { + match workspace.lookup(&target_uri) { + Some(target) => { + let new_root = link + .base_dir + .as_deref() + .and_then(|path| start.root.src_dir.join(path).ok()) + .map(|dir| ProjectRoot::walk_and_find(workspace, &dir)); + + let link_data = DirectLinkData { + link: link.clone(), + new_root, + }; + + self.edges.push(Edge { + source: start.source.uri.clone(), + target: target.uri.clone(), + data: EdgeData::DirectLink(link_data), + }); + + break; + } + None => { + self.missing.push(target_uri); + } + }; + } + } + + fn add_artifacts(&mut self, workspace: &Workspace, start: Start) { + if start.source.language != Language::Tex { + return; + } + + let root = start.root; + let relative_path = root.compile_dir.make_relative(&start.source.uri).unwrap(); + + self.add_artifact( + workspace, + start.source, + &root.aux_dir.join(&relative_path).unwrap(), + "aux", + ); + + self.add_artifact(workspace, start.source, &root.aux_dir, "aux"); + self.add_artifact(workspace, start.source, &root.compile_dir, "aux"); + + self.add_artifact( + workspace, + start.source, + &root.log_dir.join(&relative_path).unwrap(), + "log", + ); + + self.add_artifact(workspace, start.source, &root.log_dir, "log"); + self.add_artifact(workspace, start.source, &root.compile_dir, "log"); + } + + fn add_artifact( + &mut self, + workspace: &Workspace, + source: &Document, + dir: &Url, + extension: &str, + ) { + let mut path = PathBuf::from( + percent_decode_str(source.uri.path()) + .decode_utf8_lossy() + .as_ref(), + ); + + path.set_extension(extension); + let Some(target_uri) = path + .file_name() + .and_then(OsStr::to_str) + .and_then(|name| dir.join(name).ok()) + else { + return; + }; + + match workspace.lookup(&target_uri) { + Some(target) => { + self.edges.push(Edge { + source: source.uri.clone(), + target: target.uri.clone(), + data: EdgeData::Artifact, + }); + } + None => { + self.missing.push(target_uri); + } + } + } +} diff --git a/crates/base-db/src/deps/project.rs b/crates/base-db/src/deps/project.rs new file mode 100644 index 00000000..e1b941e6 --- /dev/null +++ b/crates/base-db/src/deps/project.rs @@ -0,0 +1,39 @@ +use itertools::Itertools; +use rustc_hash::FxHashSet; + +use crate::{Document, Workspace}; + +#[derive(Debug, Clone)] +pub struct Project<'a> { + pub documents: FxHashSet<&'a Document>, +} + +impl<'a> Project<'a> { + pub fn from_child(workspace: &'a Workspace, child: &'a Document) -> Self { + let mut documents = FxHashSet::default(); + for graph in workspace.graphs().values() { + if graph.preorder(workspace).contains(&child) { + documents.extend(graph.preorder(workspace)); + } + } + + Self { documents } + } +} + +pub fn parents<'a>(workspace: &'a Workspace, child: &'a Document) -> FxHashSet<&'a Document> { + workspace + .iter() + .filter(|document| { + document + .data + .as_tex() + .map_or(false, |data| data.semantics.can_be_root) + }) + .filter(|parent| { + let graph = &workspace.graphs()[&parent.uri]; + let mut nodes = graph.preorder(workspace); + nodes.contains(&child) + }) + .collect() +} diff --git a/crates/base-db/src/deps/root.rs b/crates/base-db/src/deps/root.rs new file mode 100644 index 00000000..7e2266e6 --- /dev/null +++ b/crates/base-db/src/deps/root.rs @@ -0,0 +1,157 @@ +use url::Url; + +use crate::{DocumentData, Workspace}; + +#[derive(PartialEq, Eq, Clone, Hash)] +pub struct ProjectRoot { + pub compile_dir: Url, + pub src_dir: Url, + pub aux_dir: Url, + pub log_dir: Url, + pub pdf_dir: Url, + pub additional_files: Vec, +} + +impl ProjectRoot { + pub fn walk_and_find(workspace: &Workspace, dir: &Url) -> Self { + let mut current = dir.clone(); + loop { + let root = Self::from_rootfile(workspace, ¤t) + .or_else(|| Self::from_tectonic(workspace, ¤t)) + .or_else(|| Self::from_latexmkrc(workspace, ¤t)); + + if let Some(root) = root { + break root; + } + + let Ok(parent) = current.join("..") else { + break Self::from_config(workspace, &dir); + }; + + if current == parent { + break Self::from_config(workspace, &dir); + } + + current = parent; + } + } + + pub fn from_tectonic(workspace: &Workspace, dir: &Url) -> Option { + let exists = workspace + .iter() + .filter(|document| document.dir == *dir) + .any(|document| matches!(document.data, DocumentData::Tectonic)); + + if !exists { + return None; + } + + let compile_dir = dir.clone(); + let src_dir = dir.join("src/").unwrap(); + let out_dir = dir.join("build/").unwrap(); + let aux_dir = out_dir.clone(); + let log_dir = out_dir.clone(); + let pdf_dir = out_dir; + let additional_files = vec![ + src_dir.join("_preamble.tex").unwrap(), + src_dir.join("_postamble.tex").unwrap(), + ]; + + Some(Self { + compile_dir, + src_dir, + aux_dir, + log_dir, + pdf_dir, + additional_files, + }) + } + + pub fn from_latexmkrc(workspace: &Workspace, dir: &Url) -> Option { + let rcfile = workspace + .iter() + .filter(|document| document.dir == *dir) + .find_map(|document| document.data.as_latexmkrc())?; + + let compile_dir = dir.clone(); + let src_dir = dir.clone(); + let aux_dir = rcfile + .aux_dir + .as_ref() + .and_then(|path| append_dir(dir, path).ok()) + .unwrap_or_else(|| dir.clone()); + + let out_dir = rcfile + .out_dir + .as_ref() + .and_then(|path| append_dir(dir, path).ok()) + .unwrap_or_else(|| dir.clone()); + + let log_dir = out_dir.clone(); + let pdf_dir = out_dir; + let additional_files = vec![]; + + Some(Self { + compile_dir, + src_dir, + aux_dir, + log_dir, + pdf_dir, + additional_files, + }) + } + + pub fn from_rootfile(workspace: &Workspace, dir: &Url) -> Option { + let exists = workspace + .iter() + .filter(|document| document.dir == *dir) + .any(|document| matches!(document.data, DocumentData::Root)); + + if !exists { + return None; + } + + Some(Self::from_config(workspace, dir)) + } + + pub fn from_config(workspace: &Workspace, dir: &Url) -> Self { + let compile_dir = dir.clone(); + let src_dir = dir.clone(); + let config = workspace.config(); + let aux_dir = append_dir(dir, &config.build.aux_dir).unwrap_or_else(|_| dir.clone()); + let log_dir = append_dir(dir, &config.build.log_dir).unwrap_or_else(|_| dir.clone()); + let pdf_dir = append_dir(dir, &config.build.pdf_dir).unwrap_or_else(|_| dir.clone()); + let additional_files = vec![]; + + Self { + compile_dir, + src_dir, + aux_dir, + log_dir, + pdf_dir, + additional_files, + } + } +} + +fn append_dir(dir: &Url, path: &str) -> Result { + let mut path = String::from(path); + if !path.ends_with('/') { + path.push('/'); + } + + dir.join(&path) +} + +impl std::fmt::Debug for ProjectRoot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProjectRoot") + .field("compile_dir", &self.compile_dir.as_str()) + .field("src_dir", &self.src_dir.as_str()) + .field("aux_dir", &self.aux_dir.as_str()) + .field("log_dir", &self.log_dir.as_str()) + .field("pdf_dir", &self.pdf_dir.as_str()) + .field("additional_files", &self.additional_files) + .finish() + } +} diff --git a/crates/base-db/src/graph.rs b/crates/base-db/src/graph.rs deleted file mode 100644 index 2148c7e2..00000000 --- a/crates/base-db/src/graph.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ffi::OsStr, path::PathBuf}; - -use distro::Language; -use itertools::Itertools; -use once_cell::sync::Lazy; -use percent_encoding::percent_decode_str; -use rustc_hash::FxHashSet; -use url::Url; - -use crate::{semantics, Document, DocumentData, Workspace}; - -pub static HOME_DIR: Lazy> = Lazy::new(dirs::home_dir); - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub struct Edge<'a> { - pub source: &'a Document, - pub target: &'a Document, - pub weight: Option>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub struct EdgeWeight<'a> { - pub link: &'a semantics::tex::Link, - pub old_base_dir: Url, - pub new_base_dir: Url, -} - -#[derive(Debug)] -pub struct Graph<'a> { - pub workspace: &'a Workspace, - pub start: &'a Document, - pub edges: Vec>, - pub missing: Vec, -} - -impl<'a> Graph<'a> { - pub fn new(workspace: &'a Workspace, start: &'a Document) -> Self { - let mut graph = Self { - workspace, - start, - edges: Vec::new(), - missing: Vec::new(), - }; - - let base_dir = workspace.current_dir(&start.dir); - let mut stack = vec![(start, base_dir)]; - let mut visited = FxHashSet::default(); - - while let Some((source, base_dir)) = stack.pop() { - let index = graph.edges.len(); - graph.add_explicit_edges(source, &base_dir); - for edge in &graph.edges[index..] { - let Some(weight) = edge.weight.as_ref() else { - continue; - }; - - if visited.insert(&edge.target.uri) { - stack.push((edge.target, weight.new_base_dir.clone())); - } - } - - graph.add_implicit_edges(source, &base_dir); - } - - graph - } - - pub fn preorder(&self) -> impl DoubleEndedIterator + '_ { - std::iter::once(self.start) - .chain(self.edges.iter().map(|group| group.target)) - .unique_by(|document| &document.uri) - } - - fn add_explicit_edges(&mut self, source: &'a Document, base_dir: &Url) { - let DocumentData::Tex(data) = &source.data else { - return; - }; - - let uri = source.uri.as_str(); - let is_pkg = uri.ends_with(".sty") || uri.ends_with(".cls"); - if is_pkg && !self.workspace.config().syntax.follow_package_links { - return; - } - - for link in &data.semantics.links { - self.add_link(source, base_dir, link); - } - } - - fn add_link(&mut self, source: &'a Document, base_dir: &Url, link: &'a semantics::tex::Link) { - let home_dir = HOME_DIR.as_deref(); - - let stem = &link.path.text; - let mut file_names = vec![stem.clone()]; - link.kind - .extensions() - .iter() - .map(|ext| format!("{stem}.{ext}")) - .for_each(|name| file_names.push(name)); - - let file_name_db = &self.workspace.distro().file_name_db; - let distro_files = file_names - .iter() - .filter_map(|name| file_name_db.get(name)) - .filter(|path| { - home_dir.map_or(false, |dir| path.starts_with(dir)) - || Language::from_path(path) == Some(Language::Bib) - }) - .flat_map(Url::from_file_path); - - for target_uri in file_names - .iter() - .flat_map(|file_name| base_dir.join(file_name)) - .chain(distro_files) - { - match self.workspace.lookup(&target_uri) { - Some(target) => { - let new_base_dir = link - .base_dir - .as_deref() - .and_then(|path| base_dir.join(path).ok()) - .unwrap_or_else(|| base_dir.clone()); - - let weight = Some(EdgeWeight { - link, - old_base_dir: base_dir.clone(), - new_base_dir, - }); - - self.edges.push(Edge { - source, - target, - weight, - }); - - break; - } - None => { - self.missing.push(target_uri); - } - }; - } - } - - fn add_implicit_edges(&mut self, source: &'a Document, base_dir: &Url) { - if source.language == Language::Tex { - let aux_dir = self.workspace.aux_dir(base_dir); - let log_dir = self.workspace.log_dir(base_dir); - - let relative_path = base_dir.make_relative(&source.uri).unwrap(); - - self.add_artifact(source, &aux_dir.join(&relative_path).unwrap(), "aux"); - self.add_artifact(source, &aux_dir, "aux"); - self.add_artifact(source, base_dir, "aux"); - - self.add_artifact(source, &log_dir.join(&relative_path).unwrap(), "log"); - self.add_artifact(source, &log_dir, "log"); - self.add_artifact(source, base_dir, "log"); - } - } - - fn add_artifact(&mut self, source: &'a Document, base_dir: &Url, extension: &str) { - let mut path = PathBuf::from( - percent_decode_str(source.uri.path()) - .decode_utf8_lossy() - .as_ref(), - ); - - path.set_extension(extension); - let Some(target_uri) = path - .file_name() - .and_then(OsStr::to_str) - .and_then(|name| base_dir.join(name).ok()) - else { - return; - }; - - match self.workspace.lookup(&target_uri) { - Some(target) => { - self.edges.push(Edge { - source, - target, - weight: None, - }); - } - None => { - self.missing.push(target_uri); - } - } - } -} diff --git a/crates/base-db/src/lib.rs b/crates/base-db/src/lib.rs index d2db7d43..7e0233b2 100644 --- a/crates/base-db/src/lib.rs +++ b/crates/base-db/src/lib.rs @@ -1,7 +1,7 @@ mod config; pub mod data; +pub mod deps; mod document; -pub mod graph; pub mod semantics; pub mod util; mod workspace; @@ -11,13 +11,13 @@ pub use self::{config::*, document::*, workspace::*}; #[derive(Debug)] pub struct FeatureParams<'a> { pub document: &'a Document, - pub project: Project<'a>, + pub project: deps::Project<'a>, pub workspace: &'a Workspace, } impl<'a> FeatureParams<'a> { pub fn new(workspace: &'a Workspace, document: &'a Document) -> Self { - let project = workspace.project(document); + let project = deps::Project::from_child(workspace, document); Self { document, project, diff --git a/crates/base-db/src/util/label.rs b/crates/base-db/src/util/label.rs index af0889ba..b813035a 100644 --- a/crates/base-db/src/util/label.rs +++ b/crates/base-db/src/util/label.rs @@ -5,8 +5,9 @@ use rowan::TextRange; use self::RenderedObject::*; use crate::{ + deps::Project, semantics::tex::{Label, LabelObject}, - Project, Workspace, + Workspace, }; #[derive(Debug, PartialEq, Eq, Clone, Copy)] diff --git a/crates/base-db/src/util/queries.rs b/crates/base-db/src/util/queries.rs index 8c205891..6ddf2010 100644 --- a/crates/base-db/src/util/queries.rs +++ b/crates/base-db/src/util/queries.rs @@ -4,8 +4,9 @@ use rustc_hash::FxHashMap; use url::Url; use crate::{ + deps::Project, semantics::{bib, tex}, - Document, DocumentLocation, Project, Workspace, + Document, DocumentLocation, Workspace, }; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] @@ -184,7 +185,7 @@ impl<'a> Conflict<'a> { let projects: FxHashMap<&Url, Project> = workspace .iter() - .map(|document| (&document.uri, workspace.project(document))) + .map(|document| (&document.uri, Project::from_child(workspace, document))) .collect(); let mut conflicts = Vec::new(); diff --git a/crates/base-db/src/workspace.rs b/crates/base-db/src/workspace.rs index 0fe347f1..1afd4712 100644 --- a/crates/base-db/src/workspace.rs +++ b/crates/base-db/src/workspace.rs @@ -4,14 +4,12 @@ use std::{ }; use distro::{Distro, Language}; -use itertools::Itertools; use line_index::LineCol; use rowan::{TextLen, TextRange}; -use rustc_hash::FxHashSet; -use syntax::latexmkrc::LatexmkrcData; +use rustc_hash::{FxHashMap, FxHashSet}; use url::Url; -use crate::{graph, Config, Document, DocumentData, DocumentParams, Owner}; +use crate::{deps, Config, Document, DocumentParams, Owner}; #[derive(Debug, Default)] pub struct Workspace { @@ -19,6 +17,7 @@ pub struct Workspace { config: Config, distro: Distro, folders: Vec, + graphs: FxHashMap, } impl Workspace { @@ -47,6 +46,10 @@ impl Workspace { &self.distro } + pub fn graphs(&self) -> &FxHashMap { + &self.graphs + } + pub fn open( &mut self, uri: Url, @@ -65,6 +68,11 @@ impl Workspace { cursor, config: &self.config, })); + + self.graphs = self + .iter() + .map(|start| (start.uri.clone(), deps::Graph::new(self, start))) + .collect(); } pub fn load(&mut self, path: &Path, language: Language) -> std::io::Result<()> { @@ -114,83 +122,6 @@ impl Workspace { Some(()) } - pub fn watch( - &mut self, - watcher: &mut dyn notify::Watcher, - watched_dirs: &mut FxHashSet, - ) { - self.iter() - .filter(|document| document.uri.scheme() == "file") - .flat_map(|document| { - let current_dir = &self.current_dir(&document.dir); - let doc_dir = document.dir.to_file_path(); - let aux_dir = self.aux_dir(current_dir).to_file_path(); - let log_dir = self.log_dir(current_dir).to_file_path(); - [aux_dir, log_dir, doc_dir] - }) - .flatten() - .for_each(|path| { - if !watched_dirs.contains(&path) { - let _ = watcher.watch(&path, notify::RecursiveMode::NonRecursive); - watched_dirs.insert(path); - } - }); - } - - pub fn current_dir(&self, base_dir: &Url) -> Url { - let root_dir = self.config.root_dir.as_deref(); - if let Some(dir) = root_dir.and_then(|path| base_dir.join(path).ok()) { - return dir; - } - - self.iter() - .filter(|document| matches!(document.data, DocumentData::Root | DocumentData::Tectonic)) - .flat_map(|document| document.uri.join(".")) - .find(|root_dir| base_dir.as_str().starts_with(root_dir.as_str())) - .unwrap_or_else(|| base_dir.clone()) - } - - pub fn aux_dir(&self, base_dir: &Url) -> Url { - self.output_dir(base_dir, &self.config.build.aux_dir, |data| { - data.aux_dir.as_deref() - }) - } - - pub fn log_dir(&self, base_dir: &Url) -> Url { - self.output_dir(base_dir, &self.config.build.log_dir, |_| None) - } - - pub fn pdf_dir(&self, base_dir: &Url) -> Url { - self.output_dir(base_dir, &self.config.build.pdf_dir, |_| None) - } - - fn current_latexmkrc(&self, base_dir: &Url) -> Option<&LatexmkrcData> { - self.documents - .iter() - .filter(|document| document.language == Language::Latexmkrc) - .find(|document| document.uri.join(".").as_ref() == Ok(base_dir)) - .and_then(|document| document.data.as_latexmkrc()) - } - - fn output_dir( - &self, - base_dir: &Url, - config: &str, - extract_latexmkrc: impl FnOnce(&LatexmkrcData) -> Option<&str>, - ) -> Url { - let mut dir: String = self - .current_latexmkrc(base_dir) - .and_then(|data| extract_latexmkrc(data).or_else(|| data.out_dir.as_deref())) - .unwrap_or(config) - .into(); - - if !dir.ends_with('/') { - dir.push('/'); - } - - base_dir.join(&dir).unwrap_or_else(|_| base_dir.clone()) - } - pub fn contains(&self, path: &Path) -> bool { if self.folders.is_empty() { return true; @@ -199,35 +130,6 @@ impl Workspace { self.folders.iter().any(|dir| path.starts_with(dir)) } - pub fn project(&self, child: &Document) -> Project { - let mut documents = FxHashSet::default(); - for graph in self - .iter() - .map(|start| graph::Graph::new(self, start)) - .filter(|graph| graph.preorder().contains(&child)) - { - documents.extend(graph.preorder()); - } - - Project { documents } - } - - pub fn parents(&self, child: &Document) -> FxHashSet<&Document> { - self.iter() - .filter(|document| { - let DocumentData::Tex(data) = &document.data else { - return false; - }; - data.semantics.can_be_root - }) - .filter(|parent| { - let graph = graph::Graph::new(self, parent); - let mut nodes = graph.preorder(); - nodes.contains(&child) - }) - .collect() - } - pub fn set_config(&mut self, config: Config) { self.config = config; self.reload(); @@ -280,94 +182,4 @@ impl Workspace { self.documents.insert(document); Some(()) } - - pub fn discover(&mut self, checked_paths: &mut FxHashSet) { - loop { - let mut changed = false; - changed |= self.discover_parents(checked_paths); - changed |= self.discover_children(checked_paths); - if !changed { - break; - } - } - } - - fn discover_parents(&mut self, checked_paths: &mut FxHashSet) -> bool { - let dirs = self - .iter() - .filter(|document| document.language != Language::Bib) - .filter_map(|document| document.path.as_deref()) - .flat_map(|path| path.ancestors().skip(1)) - .filter(|path| self.contains(path)) - .map(|path| path.to_path_buf()) - .collect::>(); - - let mut changed = false; - for dir in dirs { - if self - .iter() - .filter(|document| matches!(document.language, Language::Root | Language::Tectonic)) - .filter_map(|document| document.path.as_deref()) - .filter_map(|path| path.parent()) - .any(|marker| dir.starts_with(marker)) - { - continue; - } - - let Ok(entries) = std::fs::read_dir(dir) else { - continue; - }; - - for file in entries - .flatten() - .filter(|entry| entry.file_type().map_or(false, |type_| type_.is_file())) - .map(|entry| entry.path()) - { - let Some(lang) = Language::from_path(&file) else { - continue; - }; - - if !matches!( - lang, - Language::Tex | Language::Root | Language::Tectonic | Language::Latexmkrc - ) { - continue; - } - - if self.lookup_path(&file).is_none() && file.exists() { - changed |= self.load(&file, lang).is_ok(); - checked_paths.insert(file); - } - } - } - - changed - } - - fn discover_children(&mut self, checked_paths: &mut FxHashSet) -> bool { - let files = self - .iter() - .map(|start| graph::Graph::new(self, start)) - .flat_map(|graph| graph.missing) - .filter(|uri| uri.scheme() == "file") - .flat_map(|uri| uri.to_file_path()) - .collect::>(); - - let mut changed = false; - for file in files { - let language = Language::from_path(&file).unwrap_or(Language::Tex); - - if self.lookup_path(&file).is_none() && file.exists() { - changed |= self.load(&file, language).is_ok(); - checked_paths.insert(file); - } - } - - changed - } -} - -#[derive(Debug)] -pub struct Project<'a> { - pub documents: FxHashSet<&'a Document>, } diff --git a/crates/commands/src/build.rs b/crates/commands/src/build.rs index 47e156ef..34d39f26 100644 --- a/crates/commands/src/build.rs +++ b/crates/commands/src/build.rs @@ -6,7 +6,10 @@ use std::{ }; use anyhow::Result; -use base_db::Workspace; +use base_db::{ + deps::{self, ProjectRoot}, + Workspace, +}; use bstr::io::BufReadExt; use crossbeam_channel::Sender; use thiserror::Error; @@ -39,8 +42,7 @@ impl BuildCommand { return Err(BuildError::NotFound(uri.clone())); }; - let document = workspace - .parents(document) + let document = deps::parents(workspace, document) .into_iter() .next() .unwrap_or(document); @@ -53,7 +55,9 @@ impl BuildCommand { let program = config.program.clone(); let args = replace_placeholders(&config.args, &[('f', path)]); - let Ok(working_dir) = workspace.current_dir(&document.dir).to_file_path() else { + let root = ProjectRoot::walk_and_find(workspace, &document.dir); + + let Ok(working_dir) = root.compile_dir.to_file_path() else { return Err(BuildError::NotLocal(document.uri.clone())); }; diff --git a/crates/commands/src/clean.rs b/crates/commands/src/clean.rs index 706e7dfa..d4ec6b24 100644 --- a/crates/commands/src/clean.rs +++ b/crates/commands/src/clean.rs @@ -1,7 +1,7 @@ use std::process::Stdio; use anyhow::Result; -use base_db::{Document, Workspace}; +use base_db::{deps::ProjectRoot, Document, Workspace}; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] pub enum CleanTarget { @@ -21,7 +21,7 @@ impl CleanCommand { anyhow::bail!("document '{}' is not a local file", document.uri) }; - let base_dir = workspace.current_dir(&document.dir); + let root = ProjectRoot::walk_and_find(workspace, &document.dir); let flag = match target { CleanTarget::Auxiliary => "-c", @@ -29,8 +29,8 @@ impl CleanCommand { }; let out_dir = match target { - CleanTarget::Auxiliary => workspace.aux_dir(&base_dir), - CleanTarget::Artifacts => workspace.pdf_dir(&base_dir), + CleanTarget::Auxiliary => root.aux_dir, + CleanTarget::Artifacts => root.pdf_dir, }; let out_dir_path = out_dir.to_file_path().unwrap(); diff --git a/crates/commands/src/dep_graph.rs b/crates/commands/src/dep_graph.rs index 1388756d..fe9ff382 100644 --- a/crates/commands/src/dep_graph.rs +++ b/crates/commands/src/dep_graph.rs @@ -1,22 +1,20 @@ use std::io::Write; use anyhow::Result; -use base_db::{graph, Document, Workspace}; +use base_db::Workspace; use itertools::Itertools; use rustc_hash::FxHashMap; pub fn show_dependency_graph(workspace: &Workspace) -> Result { - let documents = workspace - .iter() - .enumerate() - .map(|(i, doc)| (doc, format!("v{i:0>5}"))) - .collect::>(); + let mut documents = FxHashMap::default(); let mut writer = Vec::new(); writeln!(&mut writer, "digraph G {{")?; writeln!(&mut writer, "rankdir = LR;")?; - for (document, node) in &documents { + for (i, document) in workspace.iter().enumerate() { + let node = format!("v{i:0>5}"); + let label = document.uri.as_str(); let shape = if document .data @@ -35,19 +33,22 @@ pub fn show_dependency_graph(workspace: &Workspace) -> Result { }; writeln!(&mut writer, "\t{node} [label=\"{label}\", shape={shape}];")?; + documents.insert(&document.uri, node); } for edge in workspace - .iter() - .flat_map(|start| graph::Graph::new(workspace, start).edges) + .graphs() + .values() + .flat_map(|graph| &graph.edges) .unique() { - let source = &documents[edge.source]; - let target = &documents[edge.target]; - let label = edge - .weight - .as_ref() - .map_or("", |weight| &weight.link.path.text); + let source = &documents[&edge.source]; + let target = &documents[&edge.target]; + let label = match &edge.data { + base_db::deps::EdgeData::DirectLink(data) => &data.link.path.text, + base_db::deps::EdgeData::AdditionalFiles => "", + base_db::deps::EdgeData::Artifact => "", + }; writeln!(&mut writer, "\t{source} -> {target} [label=\"{label}\"];")?; } diff --git a/crates/commands/src/fwd_search.rs b/crates/commands/src/fwd_search.rs index 4e345068..f1991cd4 100644 --- a/crates/commands/src/fwd_search.rs +++ b/crates/commands/src/fwd_search.rs @@ -1,7 +1,10 @@ use std::{path::PathBuf, process::Stdio}; use anyhow::Result; -use base_db::{Document, Workspace}; +use base_db::{ + deps::{self, ProjectRoot}, + Document, Workspace, +}; use thiserror::Error; use url::Url; @@ -53,7 +56,10 @@ impl ForwardSearch { .lookup(uri) .ok_or_else(|| ForwardSearchError::TexNotFound(uri.clone()))?; - let parent = workspace.parents(child).into_iter().next().unwrap_or(child); + let parent = deps::parents(workspace, child) + .into_iter() + .next() + .unwrap_or(child); log::debug!("[FwdSearch] root_document={}", parent.uri,); @@ -78,12 +84,12 @@ impl ForwardSearch { } fn find_pdf(workspace: &Workspace, document: &Document) -> Result { - let base_dir = workspace.current_dir(&document.dir); - let pdf_dir = workspace.pdf_dir(&base_dir); + let root = ProjectRoot::walk_and_find(workspace, &document.dir); - log::debug!("[FwdSearch] base_dir={base_dir}, pdf_dir={pdf_dir}"); + log::debug!("[FwdSearch] root={root:#?}"); - let pdf_dir = pdf_dir + let pdf_dir = root + .pdf_dir .to_file_path() .map_err(|()| ForwardSearchError::InvalidPath(document.uri.clone()))?; diff --git a/crates/completion/src/providers/include.rs b/crates/completion/src/providers/include.rs index f8bb55df..62466247 100644 --- a/crates/completion/src/providers/include.rs +++ b/crates/completion/src/providers/include.rs @@ -3,7 +3,10 @@ use std::{ path::{Path, PathBuf}, }; -use base_db::{DocumentData, FeatureParams}; +use base_db::{ + deps::{self, ProjectRoot}, + DocumentData, FeatureParams, +}; use rowan::{ast::AstNode, TextLen, TextRange}; use syntax::latex; @@ -112,13 +115,13 @@ fn current_dir( graphics_path: Option<&str>, ) -> Option { let workspace = ¶ms.workspace; - let parent = workspace - .parents(params.document) + let parent = deps::parents(&workspace, params.document) .iter() .next() .map_or(params.document, Clone::clone); - let path = workspace.current_dir(&parent.dir).to_file_path().ok()?; + let root = ProjectRoot::walk_and_find(workspace, &parent.dir); + let path = root.src_dir.to_file_path().ok()?; let mut path = PathBuf::from(path.to_str()?.replace('\\', "/")); diff --git a/crates/definition/src/include.rs b/crates/definition/src/include.rs index bc9311ba..c9a5a698 100644 --- a/crates/definition/src/include.rs +++ b/crates/definition/src/include.rs @@ -1,3 +1,4 @@ +use base_db::deps; use rowan::TextRange; use crate::DefinitionContext; @@ -7,18 +8,22 @@ use super::DefinitionResult; pub(super) fn goto_definition(context: &mut DefinitionContext) -> Option<()> { let feature = &context.params.feature; let start = feature.document; - let parents = feature.workspace.parents(start); + let parents = deps::parents(feature.workspace, start); let results = parents .into_iter() .chain(std::iter::once(start)) - .flat_map(|parent| base_db::graph::Graph::new(feature.workspace, parent).edges) - .filter(|edge| edge.source == start) + .flat_map(|parent| &feature.workspace.graphs()[&parent.uri].edges) + .filter(|edge| edge.source == start.uri) .flat_map(|edge| { - let origin_selection_range = edge.weight?.link.path.range; + let deps::EdgeData::DirectLink(data) = &edge.data else { + return None; + }; + + let origin_selection_range = data.link.path.range; if origin_selection_range.contains_inclusive(context.params.offset) { Some(DefinitionResult { origin_selection_range, - target: edge.target, + target: feature.workspace.lookup(&edge.target).unwrap(), target_range: TextRange::default(), target_selection_range: TextRange::default(), }) diff --git a/crates/diagnostics/src/build_log.rs b/crates/diagnostics/src/build_log.rs index 352e18d0..f4180f39 100644 --- a/crates/diagnostics/src/build_log.rs +++ b/crates/diagnostics/src/build_log.rs @@ -1,4 +1,4 @@ -use base_db::{Document, Workspace}; +use base_db::{deps, Document, Workspace}; use line_index::LineCol; use multimap::MultiMap; use rowan::{TextLen, TextRange, TextSize}; @@ -17,7 +17,7 @@ pub fn update( let data = log_document.data.as_log()?; - let parents = workspace.parents(log_document); + let parents = deps::parents(workspace, log_document); let root_document = parents.iter().next()?; let base_path = root_document diff --git a/crates/diagnostics/src/chktex.rs b/crates/diagnostics/src/chktex.rs index a342f783..195ea968 100644 --- a/crates/diagnostics/src/chktex.rs +++ b/crates/diagnostics/src/chktex.rs @@ -4,7 +4,10 @@ use std::{ process::Stdio, }; -use base_db::{Document, Workspace}; +use base_db::{ + deps::{self, ProjectRoot}, + Document, Workspace, +}; use encoding_rs_io::DecodeReaderBytesBuilder; use line_index::LineCol; use once_cell::sync::Lazy; @@ -23,8 +26,7 @@ impl Command { pub fn new(workspace: &Workspace, document: &Document) -> Option { document.data.as_tex()?; - let parent = workspace - .parents(document) + let parent = deps::parents(workspace, document) .into_iter() .next() .unwrap_or(document); @@ -34,7 +36,9 @@ impl Command { return None; } - let working_dir = workspace.current_dir(&parent.dir).to_file_path().ok()?; + let root = ProjectRoot::walk_and_find(workspace, &parent.dir); + + let working_dir = root.src_dir.to_file_path().ok()?; log::debug!("Calling ChkTeX from directory: {}", working_dir.display()); let text = document.text.clone(); diff --git a/crates/diagnostics/src/citations.rs b/crates/diagnostics/src/citations.rs index ea4ef03d..84b9a10b 100644 --- a/crates/diagnostics/src/citations.rs +++ b/crates/diagnostics/src/citations.rs @@ -1,7 +1,8 @@ use base_db::{ + deps::Project, semantics::{bib::Entry, tex::Citation}, util::queries::{self, Object}, - Document, Project, Workspace, + Document, Workspace, }; use rustc_hash::{FxHashMap, FxHashSet}; use url::Url; diff --git a/crates/diagnostics/src/labels.rs b/crates/diagnostics/src/labels.rs index 9a39f3f2..15b5aaad 100644 --- a/crates/diagnostics/src/labels.rs +++ b/crates/diagnostics/src/labels.rs @@ -13,11 +13,6 @@ pub fn detect_undefined_and_unused_labels( workspace: &Workspace, results: &mut FxHashMap>, ) { - let graphs: Vec<_> = workspace - .iter() - .map(|start| base_db::graph::Graph::new(workspace, start)) - .collect(); - for document in workspace.iter() { let DocumentData::Tex(data) = &document.data else { continue; @@ -25,10 +20,11 @@ pub fn detect_undefined_and_unused_labels( let mut label_refs = FxHashSet::default(); let mut label_defs = FxHashSet::default(); - let project = graphs - .iter() - .filter(|graph| graph.preorder().contains(&document)) - .flat_map(|graph| graph.preorder()); + let project = workspace + .graphs() + .values() + .filter(|graph| graph.preorder(workspace).contains(&document)) + .flat_map(|graph| graph.preorder(workspace)); for label in project .filter_map(|child| child.data.as_tex()) diff --git a/crates/diagnostics/src/manager.rs b/crates/diagnostics/src/manager.rs index 38c0a514..81347410 100644 --- a/crates/diagnostics/src/manager.rs +++ b/crates/diagnostics/src/manager.rs @@ -1,4 +1,4 @@ -use base_db::{util::filter_regex_patterns, Document, Owner, Workspace}; +use base_db::{deps::Project, util::filter_regex_patterns, Document, Owner, Workspace}; use multimap::MultiMap; use rustc_hash::FxHashMap; use url::Url; @@ -66,7 +66,7 @@ impl Manager { .iter() .filter(|document| Self::is_relevant(document)) { - let project = workspace.project(document); + let project = Project::from_child(workspace, document); super::citations::detect_undefined_citations(&project, document, &mut results); super::citations::detect_unused_entries(&project, document, &mut results); } diff --git a/crates/links/src/include.rs b/crates/links/src/include.rs index 1392b16e..2a049d0b 100644 --- a/crates/links/src/include.rs +++ b/crates/links/src/include.rs @@ -1,23 +1,25 @@ -use base_db::{DocumentLocation, FeatureParams}; +use base_db::{ + deps::{self, EdgeData}, + DocumentLocation, FeatureParams, +}; pub(super) fn find_links<'a>( params: &FeatureParams<'a>, results: &mut Vec>, ) -> Option<()> { let document = params.document; - let parent = *params - .workspace - .parents(document) + let parent = *deps::parents(params.workspace, document) .iter() .next() .unwrap_or(&document); - let graph = base_db::graph::Graph::new(params.workspace, parent); + let graph = ¶ms.workspace.graphs()[&parent.uri]; for edge in &graph.edges { - if edge.source == document { - if let Some(weight) = &edge.weight { - results.push(DocumentLocation::new(edge.target, weight.link.path.range)); + if edge.source == document.uri { + if let EdgeData::DirectLink(data) = &edge.data { + let target = params.workspace.lookup(&edge.target).unwrap(); + results.push(DocumentLocation::new(target, data.link.path.range)); } } } diff --git a/crates/symbols/src/document.rs b/crates/symbols/src/document.rs index e8f10870..791f1199 100644 --- a/crates/symbols/src/document.rs +++ b/crates/symbols/src/document.rs @@ -1,12 +1,12 @@ mod bib; mod tex; -use base_db::{util, Document, DocumentData, SymbolConfig, Workspace}; +use base_db::{deps::Project, util, Document, DocumentData, SymbolConfig, Workspace}; use crate::Symbol; pub fn document_symbols(workspace: &Workspace, document: &Document) -> Vec { - let project = workspace.project(document); + let project = Project::from_child(workspace, document); let mut symbols = match &document.data { DocumentData::Tex(data) => { let builder = tex::SymbolBuilder::new(&project, workspace.config()); diff --git a/crates/symbols/src/document/tex.rs b/crates/symbols/src/document/tex.rs index 2d8fdd20..e0df9c85 100644 --- a/crates/symbols/src/document/tex.rs +++ b/crates/symbols/src/document/tex.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use base_db::{semantics::Span, util::FloatKind, Config, Project}; +use base_db::{deps::Project, semantics::Span, util::FloatKind, Config}; use rowan::ast::AstNode; use syntax::latex::{self, HasBrack, HasCurly, LatexLanguage}; use titlecase::titlecase; diff --git a/crates/symbols/src/workspace/sort.rs b/crates/symbols/src/workspace/sort.rs index 75d80a02..adf455aa 100644 --- a/crates/symbols/src/workspace/sort.rs +++ b/crates/symbols/src/workspace/sort.rs @@ -1,4 +1,4 @@ -use base_db::{graph, Document, Workspace}; +use base_db::{Document, Workspace}; use itertools::Itertools; use url::Url; @@ -26,8 +26,8 @@ impl<'a> From<&'a Workspace> for ProjectOrdering<'a> { }) .chain(workspace.iter()) .flat_map(|document| { - let graph = graph::Graph::new(workspace, document); - graph.preorder().rev().collect_vec() + let graph = &workspace.graphs()[&document.uri]; + graph.preorder(workspace).rev().collect_vec() }) .unique() .collect_vec(); diff --git a/crates/texlab/src/features/formatting/latexindent.rs b/crates/texlab/src/features/formatting/latexindent.rs index 061c360c..d3fffc20 100644 --- a/crates/texlab/src/features/formatting/latexindent.rs +++ b/crates/texlab/src/features/formatting/latexindent.rs @@ -3,7 +3,7 @@ use std::{ process::{Command, Stdio}, }; -use base_db::{Document, LatexIndentConfig, Workspace}; +use base_db::{deps::ProjectRoot, Document, LatexIndentConfig, Workspace}; use distro::Language; use rowan::TextLen; use tempfile::tempdir; @@ -16,7 +16,8 @@ pub fn format_with_latexindent( ) -> Option> { let config = workspace.config(); let target_dir = tempdir().ok()?; - let source_dir = workspace.current_dir(&document.dir).to_file_path().ok()?; + let root = ProjectRoot::walk_and_find(workspace, &document.dir); + let source_dir = root.src_dir.to_file_path().ok()?; let target_file = target_dir .path() diff --git a/crates/texlab/src/server.rs b/crates/texlab/src/server.rs index fc16775b..70b9a030 100644 --- a/crates/texlab/src/server.rs +++ b/crates/texlab/src/server.rs @@ -11,7 +11,7 @@ use std::{ }; use anyhow::Result; -use base_db::{Config, Owner, Workspace}; +use base_db::{deps, Config, Owner, Workspace}; use commands::{BuildCommand, CleanCommand, CleanTarget, ForwardSearch}; use crossbeam_channel::{Receiver, Sender}; use distro::{Distro, Language}; @@ -237,7 +237,7 @@ impl Server { fn update_workspace(&mut self) { let mut checked_paths = FxHashSet::default(); let mut workspace = self.workspace.write(); - workspace.discover(&mut checked_paths); + base_db::deps::discover(&mut workspace, &mut checked_paths); self.watcher.watch(&mut workspace); for document in checked_paths @@ -1086,6 +1086,6 @@ impl FileWatcher { } pub fn watch(&mut self, workspace: &mut Workspace) { - workspace.watch(self.watcher.watcher(), &mut self.watched_dirs); + deps::watch(workspace, self.watcher.watcher(), &mut self.watched_dirs); } }