diff --git a/Cargo.lock b/Cargo.lock index 37294039..97a29293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.19" @@ -38,6 +49,12 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +[[package]] +name = "arc-swap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" + [[package]] name = "assert_unordered" version = "0.3.5" @@ -286,6 +303,20 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -320,6 +351,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.12" @@ -416,6 +457,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -534,6 +585,18 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -586,6 +649,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.1" @@ -895,6 +964,16 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.3" @@ -1171,6 +1250,36 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "salsa-2022" +version = "0.1.0" +source = "git+https://github.com/salsa-rs/salsa?branch=master#30b5e9760aadc3570dc2ba176f4d74448c4152ed" +dependencies = [ + "arc-swap", + "crossbeam", + "crossbeam-utils", + "dashmap", + "hashlink", + "indexmap", + "log", + "parking_lot", + "rustc-hash", + "salsa-2022-macros", + "smallvec", +] + +[[package]] +name = "salsa-2022-macros" +version = "0.1.0" +source = "git+https://github.com/salsa-rs/salsa?branch=master#30b5e9760aadc3570dc2ba176f4d74448c4152ed" +dependencies = [ + "eyre", + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1399,6 +1508,7 @@ dependencies = [ "regex", "rowan", "rustc-hash", + "salsa-2022", "serde", "serde_json", "serde_millis", diff --git a/Cargo.toml b/Cargo.toml index c51bd2a6..06280d98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,11 @@ version = "0.99.17" default-features = false features = ["from", "display"] +[dependencies.salsa] +git = "https://github.com/salsa-rs/salsa" +branch = "master" +package = "salsa-2022" + [dev-dependencies] assert_unordered = "0.3.5" criterion = { version = "0.4.0" } diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 00000000..ba67f0e8 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,7 @@ +mod context; +mod document; +mod file; +mod project; +mod workspace; + +pub use self::{context::*, document::*, file::*, project::*, workspace::*}; diff --git a/src/db/context.rs b/src/db/context.rs new file mode 100644 index 00000000..66a56ae2 --- /dev/null +++ b/src/db/context.rs @@ -0,0 +1,10 @@ +use crate::distro::Resolver; + +#[salsa::input(singleton)] +pub struct ServerContext { + #[return_ref] + pub file_name_db: Resolver, + + #[return_ref] + pub artifact_dir: String, +} diff --git a/src/db/document.rs b/src/db/document.rs new file mode 100644 index 00000000..af1e97cb --- /dev/null +++ b/src/db/document.rs @@ -0,0 +1,92 @@ +use rowan::GreenNode; + +use crate::{ + parser::{parse_bibtex, parse_build_log, parse_latex}, + syntax::{latex, BuildError, BuildLog}, + Db, LineIndex, +}; + +use super::{FileId, Language}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum OpenedBy { + Client, + Server, +} + +#[salsa::input] +pub struct Document { + pub file: FileId, + #[return_ref] + pub text: String, + pub language: Language, + pub opened_by: OpenedBy, +} + +#[salsa::tracked] +impl Document { + #[salsa::tracked] + pub fn parse(self, db: &dyn Db) -> DocumentData { + let text = self.text(db); + match self.language(db) { + Language::Tex => { + let green = parse_latex(text); + DocumentData::Tex(TexDocumentData::new(db, green)) + } + Language::Bib => { + let green = parse_bibtex(text); + DocumentData::Bib(BibDocumentData::new(db, green)) + } + Language::Log => { + let BuildLog { errors } = parse_build_log(text); + DocumentData::Log(LogDocumentData::new(db, errors)) + } + } + } + + #[salsa::tracked(return_ref)] + pub fn line_index(self, db: &dyn Db) -> LineIndex { + LineIndex::new(self.text(db)) + } + + #[salsa::tracked] + pub fn can_be_root(self, db: &dyn Db) -> bool { + match self.parse(db) { + DocumentData::Tex(data) => data.extras(db).can_be_root, + DocumentData::Bib(_) | DocumentData::Log(_) => false, + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum DocumentData { + Tex(TexDocumentData), + Bib(BibDocumentData), + Log(LogDocumentData), +} + +#[salsa::tracked] +pub struct TexDocumentData { + pub green: GreenNode, +} + +#[salsa::tracked] +impl TexDocumentData { + #[salsa::tracked(return_ref)] + pub fn extras(self, db: &dyn Db) -> latex::Extras { + let extras = latex::Extras::default(); + latex::SyntaxNode::new_root(self.green(db)); + extras + } +} + +#[salsa::tracked] +pub struct BibDocumentData { + pub green: GreenNode, +} + +#[salsa::tracked] +pub struct LogDocumentData { + #[return_ref] + pub errors: Vec, +} diff --git a/src/db/file.rs b/src/db/file.rs new file mode 100644 index 00000000..dc0607f7 --- /dev/null +++ b/src/db/file.rs @@ -0,0 +1,59 @@ +use std::path::PathBuf; + +use lsp_types::Url; + +use crate::Db; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum Language { + Tex, + Bib, + Log, +} + +#[salsa::input] +pub struct FileId { + #[return_ref] + pub uri: Url, +} + +#[salsa::tracked] +impl FileId { + #[salsa::tracked(return_ref)] + pub fn path(self, db: &dyn Db) -> Option { + let uri = self.uri(db); + if uri.scheme() == "file" { + uri.to_file_path().ok() + } else { + None + } + } + + #[salsa::tracked(return_ref)] + pub fn stem(self, db: &dyn Db) -> Option { + let file_name = self.uri(db).path_segments()?.last()?; + let file_stem = file_name + .rfind('.') + .map(|i| &file_name[..i]) + .unwrap_or(file_name); + + Some(file_stem.to_string()) + } + + #[salsa::tracked] + pub fn language(self, db: &dyn Db) -> Option { + let uri = self.uri(db); + let (_, ext) = uri.path_segments()?.last()?.rsplit_once(".")?; + match ext.to_lowercase().as_str() { + "tex" | "sty" | "cls" | "def" | "lco" | "aux" | "rnw" => Some(Language::Tex), + "bib" | "bibtex" => Some(Language::Bib), + "log" => Some(Language::Log), + _ => None, + } + } + + pub fn join(self, db: &dyn Db, path: &str) -> Result { + let uri = self.uri(db); + Ok(FileId::new(db, uri.join(path)?)) + } +} diff --git a/src/db/project.rs b/src/db/project.rs new file mode 100644 index 00000000..3eb828cf --- /dev/null +++ b/src/db/project.rs @@ -0,0 +1,185 @@ +use itertools::Itertools; +use lsp_types::Url; +use rustc_hash::FxHashSet; + +use crate::{ + component_db::COMPONENT_DATABASE, + db::{DocumentData, FileId, Language, ServerContext}, + distro::Resolver, + Db, +}; + +use super::Document; + +#[salsa::interned] +pub struct Dependency { + pub document: Document, + pub base_dir: FileId, +} + +#[salsa::tracked(return_ref)] +pub fn implicit_dependencies_of(db: &dyn Db, parent: Dependency) -> Vec { + let context = ServerContext::get(db); + + let mut results = Vec::new(); + for extension in &["aux", "log"] { + let file_name = parent + .document(db) + .file(db) + .stem(db) + .as_deref() + .map(|stem| format!("{}.{}", stem, extension)); + + let child = file_name + .and_then(|name| { + parent + .base_dir(db) + .join(db, context.artifact_dir(db)) + .and_then(|file| file.join(db, &name)) + .ok() + }) + .and_then(|file| db.workspace().load(db, file)); + + if let Some(child) = child { + let artifact = Dependency::new(db, child, parent.base_dir(db)); + results.push(artifact); + } + } + + results +} + +#[salsa::tracked(return_ref)] +pub fn explicit_dependencies_of(db: &dyn Db, parent: Dependency) -> Vec { + let resolver = Resolver::default(); + let mut results = Vec::new(); + let data = match parent.document(db).parse(db) { + DocumentData::Tex(data) => data, + _ => return results, + }; + + let extras = data.extras(db); + for link in &extras.explicit_links { + if link + .as_component_name() + .and_then(|name| COMPONENT_DATABASE.find(&name)) + .is_some() + { + continue; + } + + let mut targets = link + .targets(parent.base_dir(db).uri(db), &resolver) + .map(|uri| FileId::new(db, uri)); + + if let Some(child) = targets.find_map(|file| db.workspace().load(db, file)) { + let base_dir = link + .working_dir + .as_ref() + .and_then(|path| parent.base_dir(db).join(db, path).ok()) + .unwrap_or_else(|| parent.base_dir(db)); + + results.push(Dependency::new(db, child, base_dir)); + } + } + + results +} + +#[salsa::interned] +pub struct Project { + pub root: Document, + + #[return_ref] + pub dependencies: Vec, +} + +impl Project { + pub fn documents<'db>(self, db: &'db dyn Db) -> impl Iterator + 'db { + self.dependencies(db) + .iter() + .map(|dependency| dependency.document(db)) + } +} + +#[salsa::tracked] +pub fn project_of(db: &dyn Db, root: Document) -> Project { + let mut results = Vec::new(); + let mut visited = FxHashSet::default(); + let mut stack = vec![Dependency::new(db, root, root.file(db))]; + while let Some(dependency) = stack.pop() { + if !visited.insert(dependency.document(db)) { + break; + } + + results.push(dependency); + stack.extend(explicit_dependencies_of(db, dependency)); + stack.extend(implicit_dependencies_of(db, dependency)); + } + + Project::new(db, root, results) +} + +#[salsa::tracked] +pub struct ProjectGroup { + #[return_ref] + pub projects: Vec, +} + +#[salsa::tracked] +impl ProjectGroup { + #[salsa::tracked(return_ref)] + pub fn union(self, db: &dyn Db) -> Vec { + self.projects(db) + .iter() + .flat_map(|project| project.documents(db)) + .unique() + .collect() + } +} + +#[salsa::tracked] +pub fn project_group_of(db: &dyn Db, child: Document) -> ProjectGroup { + let ancestors = child + .file(db) + .path(db) + .into_iter() + .flat_map(|path| path.ancestors()); + + fn find(db: &dyn Db, child: Document) -> Vec { + db.workspace() + .iter() + .filter(|doc| doc.can_be_root(db)) + .map(|root| project_of(db, root)) + .filter(|project| project.documents(db).contains(&child)) + .collect() + } + + for path in ancestors { + let projects = find(db, child); + + if !projects.is_empty() { + return ProjectGroup::new(db, projects); + } + + let files = std::fs::read_dir(path) + .into_iter() + .flatten() + .filter_map(Result::ok) + .filter(|entry| entry.file_type().map_or(false, |ty| ty.is_file())) + .filter_map(|entry| Url::from_file_path(entry.path()).ok()) + .map(|uri| FileId::new(db, uri)) + .filter(|file| file.language(db) == Some(Language::Tex)); + + for file in files { + db.workspace().load(db, file); + } + } + + let mut projects = find(db, child); + if projects.is_empty() { + projects = vec![project_of(db, child)]; + } + + ProjectGroup::new(db, projects) +} diff --git a/src/db/workspace.rs b/src/db/workspace.rs new file mode 100644 index 00000000..525f9405 --- /dev/null +++ b/src/db/workspace.rs @@ -0,0 +1,90 @@ +use std::borrow::Cow; + +use dashmap::DashMap; + +use crate::{db::Document, Db}; + +use super::{FileId, Language, OpenedBy}; + +#[derive(Debug, Clone, Default)] +pub struct Workspace { + documents: DashMap>, +} + +impl Workspace { + pub fn open( + &self, + db: &mut dyn Db, + file: FileId, + text: String, + language: Language, + ) -> Document { + match self.get(file) { + Some(document) => { + document + .set_text(db) + .with_durability(salsa::Durability::LOW) + .to(text); + + document + .set_language(db) + .with_durability(salsa::Durability::HIGH) + .to(language); + + document + .set_opened_by(db) + .with_durability(salsa::Durability::LOW) + .to(OpenedBy::Client); + + document + } + None => { + let document = Document::new(db, file, text, language, OpenedBy::Client); + self.documents.insert(file, Some(document)); + document + } + } + } + + pub fn load(&self, db: &dyn Db, file: FileId) -> Option { + *self.documents.entry(file).or_insert_with(|| { + let path = file.path(db).as_deref()?; + let data = std::fs::read(path).ok()?; + let text = match String::from_utf8_lossy(&data) { + Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(data) }, + Cow::Owned(text) => text, + }; + + let language = file.language(db)?; + Some(Document::new(db, file, text, language, OpenedBy::Server)) + }) + } + + pub fn close(&self, db: &mut dyn Db, file: FileId) { + self.documents.alter(&file, |_, document| { + if let Some(doc) = document { + doc.set_opened_by(db) + .with_durability(salsa::Durability::LOW) + .to(OpenedBy::Server); + } + + document + }); + } + + pub fn remove_if(&self, file: FileId, predicate: F) + where + F: FnOnce(Document) -> bool, + { + self.documents + .remove_if(&file, |_, doc| doc.map_or(false, predicate)); + } + + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { + self.documents.iter().filter_map(|entry| *entry) + } + + fn get(&self, file: FileId) -> Option { + *self.documents.get(&file)? + } +} diff --git a/src/lib.rs b/src/lib.rs index e12232e2..132a9629 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ mod capabilities; pub mod citation; mod client; pub mod component_db; +pub mod db; mod debouncer; mod diagnostics; mod dispatch; @@ -21,6 +22,8 @@ mod server; pub mod syntax; mod workspace; +use std::sync::Arc; + pub use self::{ capabilities::ClientCapabilitiesExt, document::*, @@ -54,3 +57,65 @@ pub(crate) fn normalize_uri(uri: &mut lsp_types::Url) { uri.set_fragment(None); } + +#[salsa::jar(db = Db)] +pub struct Jar( + db::ServerContext, + db::FileId, + db::FileId_path, + db::FileId_stem, + db::FileId_language, + db::Document, + db::Document_parse, + db::Document_line_index, + db::Document_can_be_root, + db::TexDocumentData, + db::TexDocumentData_extras, + db::BibDocumentData, + db::LogDocumentData, + db::Dependency, + db::implicit_dependencies_of, + db::explicit_dependencies_of, + db::Project, + db::project_of, + db::ProjectGroup, + db::ProjectGroup_union, + db::project_group_of, +); + +pub trait Db: salsa::DbWithJar { + fn workspace(&self) -> Arc; +} + +#[salsa::db(crate::Jar)] +pub(crate) struct Database { + storage: salsa::Storage, + workspace: Arc, +} + +impl Default for Database { + fn default() -> Self { + let storage = salsa::Storage::default(); + let workspace = Arc::default(); + let db = Self { storage, workspace }; + db::ServerContext::new(&db, distro::Resolver::default(), ".".to_string()); + db + } +} + +impl salsa::Database for Database {} + +impl salsa::ParallelDatabase for Database { + fn snapshot(&self) -> salsa::Snapshot { + salsa::Snapshot::new(Self { + storage: self.storage.snapshot(), + workspace: self.workspace(), + }) + } +} + +impl Db for Database { + fn workspace(&self) -> Arc { + Arc::clone(&self.workspace) + } +} diff --git a/src/syntax/latex/analysis/types.rs b/src/syntax/latex/analysis/types.rs index f2dfc172..92e62923 100644 --- a/src/syntax/latex/analysis/types.rs +++ b/src/syntax/latex/analysis/types.rs @@ -15,7 +15,7 @@ pub struct LatexAnalyzerContext<'a> { pub extras: Extras, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct Extras { pub implicit_links: ImplicitLinks, pub explicit_links: Vec, @@ -45,7 +45,7 @@ pub enum ExplicitLinkKind { Bibtex, } -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct ExplicitLink { pub stem: SmolStr, pub stem_range: TextRange, diff --git a/src/workspace.rs b/src/workspace.rs index 6de2601e..0a0c4dd7 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -298,3 +298,44 @@ fn change_extension(uri: &Url, extension: &str) -> Option { Some(format!("{}.{}", file_stem, extension)) } + +// fn explore_project( +// root: &Document, +// working_dir: &Url, +// resolver: &Resolver, +// visited: &mut FxHashSet>, +// results: &mut Vec, +// ) { +// if !visited.insert(Arc::clone(root.uri())) { +// return; +// } + +// results.push(root.clone()); +// if let Some(data) = root.data().as_latex() { +// for link in &data.extras.explicit_links { +// if link +// .as_component_name() +// .and_then(|name| COMPONENT_DATABASE.find(&name)) +// .is_some() +// { +// continue; +// } + +// if let Some(child) = link +// .targets(&working_dir, resolver) +// .find_map(|uri| self.get(&uri)) +// { +// explore_project(&child, &working_dir, visited, results); +// } +// } + +// for extension in &["aux", "log"] { +// if let Some(child) = change_extension(root.uri(), extension) +// .and_then(|file_name| working_dir.join(&file_name).ok()) +// .and_then(|uri| self.get(&uri)) +// { +// explore_project(&child, &working_dir, visited, results); +// } +// } +// } +// }