Add basic salsa DB

This commit is contained in:
Patrick Förster 2022-10-10 20:01:11 +02:00
parent cfeead5c24
commit 2f64ce6bbc
11 changed files with 666 additions and 2 deletions

110
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

7
src/db.rs Normal file
View file

@ -0,0 +1,7 @@
mod context;
mod document;
mod file;
mod project;
mod workspace;
pub use self::{context::*, document::*, file::*, project::*, workspace::*};

10
src/db/context.rs Normal file
View file

@ -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,
}

92
src/db/document.rs Normal file
View file

@ -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<BuildError>,
}

59
src/db/file.rs Normal file
View file

@ -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<PathBuf> {
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<String> {
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<Language> {
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<FileId, url::ParseError> {
let uri = self.uri(db);
Ok(FileId::new(db, uri.join(path)?))
}
}

185
src/db/project.rs Normal file
View file

@ -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<Dependency> {
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<Dependency> {
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<Dependency>,
}
impl Project {
pub fn documents<'db>(self, db: &'db dyn Db) -> impl Iterator<Item = Document> + '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<Project>,
}
#[salsa::tracked]
impl ProjectGroup {
#[salsa::tracked(return_ref)]
pub fn union(self, db: &dyn Db) -> Vec<Document> {
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<Project> {
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)
}

90
src/db/workspace.rs Normal file
View file

@ -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<FileId, Option<Document>>,
}
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<Document> {
*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<F>(&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<Item = Document> + 'a {
self.documents.iter().filter_map(|entry| *entry)
}
fn get(&self, file: FileId) -> Option<Document> {
*self.documents.get(&file)?
}
}

View file

@ -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<Jar> {
fn workspace(&self) -> Arc<db::Workspace>;
}
#[salsa::db(crate::Jar)]
pub(crate) struct Database {
storage: salsa::Storage<Self>,
workspace: Arc<db::Workspace>,
}
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<Self> {
salsa::Snapshot::new(Self {
storage: self.storage.snapshot(),
workspace: self.workspace(),
})
}
}
impl Db for Database {
fn workspace(&self) -> Arc<db::Workspace> {
Arc::clone(&self.workspace)
}
}

View file

@ -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<ExplicitLink>,
@ -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,

View file

@ -298,3 +298,44 @@ fn change_extension(uri: &Url, extension: &str) -> Option<String> {
Some(format!("{}.{}", file_stem, extension))
}
// fn explore_project(
// root: &Document,
// working_dir: &Url,
// resolver: &Resolver,
// visited: &mut FxHashSet<Arc<Url>>,
// results: &mut Vec<Document>,
// ) {
// 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);
// }
// }
// }
// }