diff options
Diffstat (limited to 'support/texlab/src/workspace/mod.rs')
-rw-r--r-- | support/texlab/src/workspace/mod.rs | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/support/texlab/src/workspace/mod.rs b/support/texlab/src/workspace/mod.rs new file mode 100644 index 0000000000..5ad2c21bf2 --- /dev/null +++ b/support/texlab/src/workspace/mod.rs @@ -0,0 +1,417 @@ +mod feature; +mod outline; +mod uri; + +pub use self::feature::*; +pub use self::outline::*; +pub use self::uri::*; +use crate::completion::DATABASE; +use crate::syntax::*; +use log::*; +use lsp_types::*; +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; +use tex::Language; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Document { + pub uri: Uri, + pub text: String, + pub tree: SyntaxTree, + pub modified: SystemTime, +} + +impl Document { + pub fn new(uri: Uri, text: String, tree: SyntaxTree) -> Self { + Self { + uri, + text, + tree, + modified: SystemTime::now(), + } + } + + pub fn parse(uri: Uri, text: String, language: Language) -> Self { + let tree = SyntaxTree::parse(&uri, &text, language); + Self::new(uri, text, tree) + } + + pub fn is_file(&self) -> bool { + self.uri.scheme() == "file" + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Workspace { + pub documents: Vec<Arc<Document>>, +} + +impl Workspace { + pub fn new() -> Self { + Workspace { + documents: Vec::new(), + } + } + + pub fn find(&self, uri: &Uri) -> Option<Arc<Document>> { + self.documents + .iter() + .find(|document| &document.uri == uri) + .map(|document| Arc::clone(&document)) + } + + pub fn related_documents(&self, uri: &Uri) -> Vec<Arc<Document>> { + let edges = self.build_dependency_graph(); + let mut results = Vec::new(); + if let Some(start) = self.find(uri) { + let mut visited: Vec<Arc<Document>> = Vec::new(); + let mut stack = vec![start]; + while let Some(current) = stack.pop() { + if visited.contains(¤t) { + continue; + } + visited.push(Arc::clone(¤t)); + + results.push(Arc::clone(¤t)); + for edge in &edges { + if edge.0 == current { + stack.push(Arc::clone(&edge.1)); + } + } + } + } + results + } + + fn build_dependency_graph(&self) -> Vec<(Arc<Document>, Arc<Document>)> { + let mut edges: Vec<(Arc<Document>, Arc<Document>)> = Vec::new(); + for parent in self.documents.iter().filter(|document| document.is_file()) { + if let SyntaxTree::Latex(tree) = &parent.tree { + for include in &tree.includes { + for targets in &include.all_targets { + for target in targets { + if let Some(ref child) = self.find(target) { + edges.push((Arc::clone(&parent), Arc::clone(&child))); + edges.push((Arc::clone(&child), Arc::clone(&parent))); + } + } + } + } + + let tex_path = parent.uri.to_file_path().unwrap(); + let aux_path = tex_path.with_extension("aux"); + if let Some(child) = self.find(&Uri::from_file_path(aux_path).unwrap()) { + edges.push((Arc::clone(&parent), Arc::clone(&child))); + edges.push((Arc::clone(&child), Arc::clone(&parent))); + } + } + } + edges + } + + pub fn find_parent(&self, uri: &Uri) -> Option<Arc<Document>> { + for document in self.related_documents(uri) { + if let SyntaxTree::Latex(tree) = &document.tree { + if tree.env.is_standalone { + return Some(document); + } + } + } + None + } + + pub fn unresolved_includes(&self) -> Vec<PathBuf> { + let mut includes = Vec::new(); + for document in &self.documents { + if let SyntaxTree::Latex(tree) = &document.tree { + for include in &tree.includes { + match include.kind { + LatexIncludeKind::Bibliography | LatexIncludeKind::Latex => (), + LatexIncludeKind::Everything + | LatexIncludeKind::Image + | LatexIncludeKind::Pdf + | LatexIncludeKind::Svg => continue, + LatexIncludeKind::Package | LatexIncludeKind::Class => { + if include + .paths() + .iter() + .all(|name| DATABASE.contains(name.text())) + { + continue; + } + } + } + + for targets in &include.all_targets { + if targets.iter().any(|target| self.find(target).is_some()) { + continue; + } + + for target in targets { + let path = target.to_file_path().unwrap(); + if path.exists() { + includes.push(path); + } + } + } + } + + let aux_path = document.uri.to_file_path().unwrap().with_extension("aux"); + if self + .find(&Uri::from_file_path(&aux_path).unwrap()) + .is_none() + { + includes.push(aux_path); + } + } + } + includes + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DocumentView { + pub workspace: Arc<Workspace>, + pub document: Arc<Document>, + pub related_documents: Vec<Arc<Document>>, +} + +impl DocumentView { + pub fn new(workspace: Arc<Workspace>, document: Arc<Document>) -> Self { + let related_documents = workspace.related_documents(&document.uri); + Self { + workspace, + document, + related_documents, + } + } +} + +#[derive(Debug)] +pub enum LoadError { + UnknownLanguage, + InvalidPath, + IO(std::io::Error), +} + +#[derive(Debug, Default)] +pub struct WorkspaceManager { + workspace: Mutex<Arc<Workspace>>, +} + +impl WorkspaceManager { + pub fn get(&self) -> Arc<Workspace> { + let workspace = self.workspace.lock().unwrap(); + Arc::clone(&workspace) + } + + pub fn add(&self, document: TextDocumentItem) { + let language = match Language::by_language_id(&document.language_id) { + Some(language) => language, + None => { + error!("Invalid language id: {}", &document.language_id); + return; + } + }; + + let mut workspace = self.workspace.lock().unwrap(); + *workspace = Self::add_or_update(&workspace, document.uri.into(), document.text, language); + } + + pub fn load(&self, path: &Path) -> Result<(), LoadError> { + let language = match path + .extension() + .and_then(OsStr::to_str) + .and_then(Language::by_extension) + { + Some(language) => language, + None => { + warn!("Could not determine language: {}", path.to_string_lossy()); + return Err(LoadError::UnknownLanguage); + } + }; + + let uri = match Uri::from_file_path(path) { + Ok(uri) => uri, + Err(_) => { + error!("Invalid path: {}", path.to_string_lossy()); + return Err(LoadError::InvalidPath); + } + }; + + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(why) => { + warn!("Could not open file: {}", path.to_string_lossy()); + return Err(LoadError::IO(why)); + } + }; + + let mut workspace = self.workspace.lock().unwrap(); + *workspace = Self::add_or_update(&workspace, uri, text, language); + Ok(()) + } + + pub fn update(&self, uri: Uri, text: String) { + let mut workspace = self.workspace.lock().unwrap(); + + let old_document = match workspace.documents.iter().find(|x| x.uri == uri) { + Some(document) => document, + None => { + warn!("Document not found: {}", uri); + return; + } + }; + + let language = match old_document.tree { + SyntaxTree::Latex(_) => Language::Latex, + SyntaxTree::Bibtex(_) => Language::Bibtex, + }; + + *workspace = Self::add_or_update(&workspace, uri, text, language); + } + + fn add_or_update( + workspace: &Workspace, + uri: Uri, + text: String, + language: Language, + ) -> Arc<Workspace> { + let document = Document::parse(uri, text, language); + let mut documents: Vec<Arc<Document>> = workspace + .documents + .iter() + .filter(|x| x.uri != document.uri) + .cloned() + .collect(); + + documents.push(Arc::new(document)); + Arc::new(Workspace { documents }) + } +} + +#[derive(Debug, Default)] +pub struct WorkspaceBuilder { + pub workspace: Workspace, +} + +impl WorkspaceBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn document(&mut self, name: &str, text: &str) -> Uri { + let path = std::env::temp_dir().join(name); + let language = Language::by_extension(path.extension().unwrap().to_str().unwrap()).unwrap(); + let uri = Uri::from_file_path(path).unwrap(); + let document = Document::parse(uri.clone(), text.to_owned(), language); + self.workspace.documents.push(Arc::new(document)); + uri + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn verify_documents(expected: Vec<Uri>, actual: Vec<Arc<Document>>) { + assert_eq!(expected.len(), actual.len()); + for i in 0..expected.len() { + assert_eq!(expected[i], actual[i].uri); + } + } + + #[test] + fn test_related_documents_append_extensions() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\include{bar/baz}"); + let uri2 = builder.document("bar/baz.tex", ""); + let documents = builder.workspace.related_documents(&uri1); + verify_documents(vec![uri1, uri2], documents); + } + + #[test] + fn test_related_documents_relative_path() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", ""); + let uri2 = builder.document("bar/baz.tex", "\\input{../foo.tex}"); + let documents = builder.workspace.related_documents(&uri1); + verify_documents(vec![uri1, uri2], documents); + } + + #[test] + fn test_related_documents_invalid_includes() { + let mut builder = WorkspaceBuilder::new(); + let uri = builder.document("foo.tex", "\\include{<foo>?|bar|:}\n\\include{}"); + let documents = builder.workspace.related_documents(&uri); + verify_documents(vec![uri], documents); + } + + #[test] + fn test_related_documents_bibliographies() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\addbibresource{bar.bib}"); + let uri2 = builder.document("bar.bib", ""); + let documents = builder.workspace.related_documents(&uri2); + verify_documents(vec![uri2, uri1], documents); + } + + #[test] + fn test_related_documents_unresolvable_include() { + let mut builder = WorkspaceBuilder::new(); + let uri = builder.document("foo.tex", "\\include{bar.tex}"); + builder.document("baz.tex", ""); + let documents = builder.workspace.related_documents(&uri); + verify_documents(vec![uri], documents); + } + + #[test] + fn test_related_documents_include_cycles() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\input{bar.tex}"); + let uri2 = builder.document("bar.tex", "\\input{foo.tex}"); + let documents = builder.workspace.related_documents(&uri1); + verify_documents(vec![uri1, uri2], documents); + } + + #[test] + fn test_related_documents_same_parent() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("test.tex", "\\include{test1}\\include{test2}"); + let uri2 = builder.document("test1.tex", "\\label{foo}"); + let uri3 = builder.document("test2.tex", "\\ref{foo}"); + let documents = builder.workspace.related_documents(&uri3); + verify_documents(vec![uri3, uri1, uri2], documents); + } + + #[test] + fn test_related_documents_aux_file() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\include{bar}"); + let uri2 = builder.document("bar.tex", ""); + let uri3 = builder.document("foo.aux", ""); + let documents = builder.workspace.related_documents(&uri2); + verify_documents(vec![uri2, uri1, uri3], documents); + } + + #[test] + fn test_find_parent() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", ""); + let uri2 = builder.document("bar.tex", "\\begin{document}\\include{foo}\\end{document}"); + let document = builder.workspace.find_parent(&uri1).unwrap(); + assert_eq!(uri2, document.uri); + } + + #[test] + fn test_find_parent_no_parent() { + let mut builder = WorkspaceBuilder::new(); + let uri = builder.document("foo.tex", ""); + builder.document("bar.tex", "\\begin{document}\\end{document}"); + let document = builder.workspace.find_parent(&uri); + assert_eq!(None, document); + } +} |