use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use salsa::Setter;
use smol_str::SmolStr;
use crate::bib::semantic::Model as BibModel;
use crate::bib::syntax::SyntaxNode as BibSyntaxNode;
use crate::file_discovery::file_kind_or_tex;
use crate::parser::parse_with_flavor;
use crate::project::citations::document_cite_names;
use crate::project::labels::{document_label_names, is_document_root};
use crate::project::{
BibTarget, IncludeEdgeKey, PackageEdgeKey, Project, ProjectMember, ResolvedCitations,
ResolvedLabels, collect_bib_resource_targets, collect_include_edge_keys,
collect_package_edge_keys, package_graph, resolved_citations, resolved_labels,
};
use crate::semantic::{
DocAssociation, SemanticModel, SignatureDb, doc_associations as build_doc_associations,
scan_definitions,
};
use crate::syntax::SyntaxNode;
#[salsa::input]
pub struct SourceFile {
#[returns(ref)]
pub path: PathBuf,
#[returns(ref)]
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QueryKind {
ParsedDocument,
SemanticModel,
DocumentSignatures,
DocAssociations,
IncludeEdges,
PackageEdges,
FileLabels,
FileIsDocumentRoot,
ProjectGraph,
PackageGraph,
ScopeSignatures,
ResolvedLabels,
ParsedBibDocument,
BibSemanticModel,
FileCiteNames,
FileCiteFacts,
ResolvedCitations,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct QueryLogEntry {
pub kind: QueryKind,
pub file: Option<SourceFile>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseDiagnosticData {
pub message: String,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone)]
pub struct ParsedDocument {
pub green: rowan::GreenNode,
pub diagnostics: Vec<ParseDiagnosticData>,
}
#[derive(Debug, Clone)]
pub struct ParsedBibDocument {
pub green: rowan::GreenNode,
pub diagnostics: Vec<ParseDiagnosticData>,
}
#[salsa::db]
pub trait IncrementalDb: salsa::Database {
fn record_query(&self, entry: QueryLogEntry);
}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_update_types))]
pub fn parsed_document(db: &dyn IncrementalDb, file: SourceFile) -> ParsedDocument {
db.record_query(QueryLogEntry {
kind: QueryKind::ParsedDocument,
file: Some(file),
});
let config = file_kind_or_tex(file.path(db)).lex_config();
let parsed = parse_with_flavor(file.text(db).as_str(), config);
let diagnostics = parsed
.errors
.into_iter()
.map(|error| ParseDiagnosticData {
message: error.message,
start: error.start,
end: error.end,
})
.collect();
ParsedDocument {
green: parsed.green,
diagnostics,
}
}
pub fn parse_diagnostics(db: &dyn IncrementalDb, file: SourceFile) -> &[ParseDiagnosticData] {
&parsed_document(db, file).diagnostics
}
pub fn parsed_tree_root(db: &dyn IncrementalDb, file: SourceFile) -> SyntaxNode {
SyntaxNode::new_root(parsed_document(db, file).green.clone())
}
#[salsa::tracked(returns(ref))]
pub fn semantic_model(db: &dyn IncrementalDb, file: SourceFile) -> SemanticModel {
db.record_query(QueryLogEntry {
kind: QueryKind::SemanticModel,
file: Some(file),
});
SemanticModel::build(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn document_signatures(db: &dyn IncrementalDb, file: SourceFile) -> SignatureDb {
db.record_query(QueryLogEntry {
kind: QueryKind::DocumentSignatures,
file: Some(file),
});
scan_definitions(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn scope_signatures<'db>(
db: &'db dyn IncrementalDb,
project: Project<'db>,
file: SourceFile,
) -> SignatureDb {
db.record_query(QueryLogEntry {
kind: QueryKind::ScopeSignatures,
file: Some(file),
});
let graph = package_graph(db, project);
let by_path: HashMap<&Path, SourceFile> = project
.members(db)
.iter()
.map(|member| (member.path.as_path(), member.file))
.collect();
let mut merged = SignatureDb::default();
for loaded in graph.transitively_loaded(file.path(db)) {
if let Some(&member) = by_path.get(loaded.as_path()) {
merged.merge_from(document_signatures(db, member));
}
}
merged.merge_from(document_signatures(db, file));
merged
}
#[salsa::tracked(returns(ref))]
pub fn doc_associations(db: &dyn IncrementalDb, file: SourceFile) -> Vec<DocAssociation> {
db.record_query(QueryLogEntry {
kind: QueryKind::DocAssociations,
file: Some(file),
});
build_doc_associations(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn include_edges(db: &dyn IncrementalDb, file: SourceFile) -> Vec<IncludeEdgeKey> {
db.record_query(QueryLogEntry {
kind: QueryKind::IncludeEdges,
file: Some(file),
});
let root = parsed_tree_root(db, file);
collect_include_edge_keys(&root, file.path(db).parent())
}
#[salsa::tracked(returns(ref))]
pub fn package_edges(db: &dyn IncrementalDb, file: SourceFile) -> Vec<PackageEdgeKey> {
db.record_query(QueryLogEntry {
kind: QueryKind::PackageEdges,
file: Some(file),
});
let root = parsed_tree_root(db, file);
collect_package_edge_keys(&root, file.path(db).parent())
}
#[salsa::tracked(returns(ref))]
pub fn file_labels(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SmolStr> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileLabels,
file: Some(file),
});
document_label_names(semantic_model(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_is_document_root(db: &dyn IncrementalDb, file: SourceFile) -> bool {
db.record_query(QueryLogEntry {
kind: QueryKind::FileIsDocumentRoot,
file: Some(file),
});
is_document_root(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_update_types))]
pub fn parsed_bib_document(db: &dyn IncrementalDb, file: SourceFile) -> ParsedBibDocument {
db.record_query(QueryLogEntry {
kind: QueryKind::ParsedBibDocument,
file: Some(file),
});
let parsed = crate::bib::parse(file.text(db).as_str());
let diagnostics = parsed
.errors
.into_iter()
.map(|error| ParseDiagnosticData {
message: error.message,
start: error.start,
end: error.end,
})
.collect();
ParsedBibDocument {
green: parsed.green,
diagnostics,
}
}
pub fn bib_parse_diagnostics(db: &dyn IncrementalDb, file: SourceFile) -> &[ParseDiagnosticData] {
&parsed_bib_document(db, file).diagnostics
}
pub fn parsed_bib_tree_root(db: &dyn IncrementalDb, file: SourceFile) -> BibSyntaxNode {
BibSyntaxNode::new_root(parsed_bib_document(db, file).green.clone())
}
#[salsa::tracked(returns(ref))]
pub fn bib_semantic_model(db: &dyn IncrementalDb, file: SourceFile) -> BibModel {
db.record_query(QueryLogEntry {
kind: QueryKind::BibSemanticModel,
file: Some(file),
});
BibModel::build(&parsed_bib_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_cite_names(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SmolStr> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileCiteNames,
file: Some(file),
});
document_cite_names(bib_semantic_model(db, file))
}
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub struct FileCiteFacts {
pub bib_targets: Vec<BibTarget>,
pub nocite_all: bool,
}
#[salsa::tracked(returns(ref))]
pub fn file_cite_facts(db: &dyn IncrementalDb, file: SourceFile) -> FileCiteFacts {
db.record_query(QueryLogEntry {
kind: QueryKind::FileCiteFacts,
file: Some(file),
});
let root = parsed_tree_root(db, file);
FileCiteFacts {
bib_targets: collect_bib_resource_targets(&root, file.path(db).parent()),
nocite_all: semantic_model(db, file).has_wildcard_nocite(),
}
}
#[salsa::db]
pub struct IncrementalDatabase {
storage: salsa::Storage<Self>,
query_log: Arc<Mutex<Vec<QueryLogEntry>>>,
files: Arc<Mutex<HashMap<PathBuf, SourceFile>>>,
}
impl Default for IncrementalDatabase {
fn default() -> Self {
Self {
storage: salsa::Storage::new(None),
query_log: Arc::new(Mutex::new(Vec::new())),
files: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Clone for IncrementalDatabase {
fn clone(&self) -> Self {
Self {
storage: self.storage.clone(),
query_log: Arc::clone(&self.query_log),
files: Arc::clone(&self.files),
}
}
}
impl std::fmt::Debug for IncrementalDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IncrementalDatabase")
.finish_non_exhaustive()
}
}
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let mut out = PathBuf::new();
for component in absolute.components() {
match component {
Component::CurDir => {}
Component::ParentDir
if matches!(out.components().next_back(), Some(Component::Normal(_))) =>
{
out.pop();
}
other => out.push(other.as_os_str()),
}
}
out
}
static MEM_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
impl IncrementalDatabase {
pub fn add_file(&self, text: impl Into<String>) -> SourceFile {
let n = MEM_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = PathBuf::from(format!("<mem>/{n}.tex"));
SourceFile::new(self, path, text.into())
}
pub fn set_file_text(&mut self, file: SourceFile, text: impl Into<String>) {
file.set_text(self).to(text.into());
}
pub fn upsert_file(&mut self, path: &Path, text: String) -> SourceFile {
let key = normalize_path(path);
let existing = self
.files
.lock()
.expect("file cache mutex poisoned")
.get(&key)
.copied();
match existing {
Some(file) => {
if file.text(self) != &text {
file.set_text(self).to(text);
}
file
}
None => {
let file = SourceFile::new(self, key.clone(), text);
self.files
.lock()
.expect("file cache mutex poisoned")
.insert(key, file);
file
}
}
}
pub fn tracked_files(&self) -> Vec<(PathBuf, SourceFile)> {
let mut files: Vec<(PathBuf, SourceFile)> = self
.files
.lock()
.expect("file cache mutex poisoned")
.iter()
.map(|(path, &file)| (path.clone(), file))
.collect();
files.sort_by(|a, b| a.0.cmp(&b.0));
files
}
pub fn lookup_file(&self, path: &Path) -> Option<SourceFile> {
self.files
.lock()
.expect("file cache mutex poisoned")
.get(&normalize_path(path))
.copied()
}
pub fn remove_file(&mut self, path: &Path) -> Option<SourceFile> {
self.files
.lock()
.expect("file cache mutex poisoned")
.remove(&normalize_path(path))
}
pub fn file_text(&self, file: SourceFile) -> &str {
file.text(self)
}
pub fn file_path(&self, file: SourceFile) -> &Path {
file.path(self)
}
pub fn parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
parse_diagnostics(self, file)
}
pub fn parsed_tree(&self, file: SourceFile) -> SyntaxNode {
parsed_tree_root(self, file)
}
pub fn include_edges(&self, file: SourceFile) -> &[IncludeEdgeKey] {
include_edges(self, file)
}
pub fn semantic_model(&self, file: SourceFile) -> &SemanticModel {
semantic_model(self, file)
}
pub fn document_signatures(&self, file: SourceFile) -> &SignatureDb {
document_signatures(self, file)
}
pub fn doc_associations(&self, file: SourceFile) -> &[DocAssociation] {
doc_associations(self, file)
}
pub fn file_labels(&self, file: SourceFile) -> &[SmolStr] {
file_labels(self, file)
}
pub fn file_is_document_root(&self, file: SourceFile) -> bool {
*file_is_document_root(self, file)
}
pub fn bib_parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
bib_parse_diagnostics(self, file)
}
pub fn parsed_bib_tree(&self, file: SourceFile) -> BibSyntaxNode {
parsed_bib_tree_root(self, file)
}
pub fn bib_semantic_model(&self, file: SourceFile) -> &BibModel {
bib_semantic_model(self, file)
}
pub fn clear_query_log(&self) {
self.query_log
.lock()
.expect("query log mutex poisoned")
.clear();
}
pub fn query_log(&self) -> Vec<QueryLogEntry> {
self.query_log
.lock()
.expect("query log mutex poisoned")
.clone()
}
pub fn snapshot(&self) -> Analysis {
Analysis(self.clone())
}
}
pub struct Analysis(IncrementalDatabase);
impl Analysis {
pub fn lookup_file(&self, path: &Path) -> Option<SourceFile> {
self.0.lookup_file(path)
}
pub fn file_text(&self, file: SourceFile) -> &str {
self.0.file_text(file)
}
pub fn file_path(&self, file: SourceFile) -> &Path {
self.0.file_path(file)
}
pub fn tracked_files(&self) -> Vec<(PathBuf, SourceFile)> {
self.0.tracked_files()
}
pub fn parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
self.0.parse_diagnostics(file)
}
pub fn parsed_tree(&self, file: SourceFile) -> SyntaxNode {
self.0.parsed_tree(file)
}
pub fn semantic_model(&self, file: SourceFile) -> &SemanticModel {
self.0.semantic_model(file)
}
pub fn document_signatures(&self, file: SourceFile) -> &SignatureDb {
self.0.document_signatures(file)
}
pub fn bib_parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
self.0.bib_parse_diagnostics(file)
}
pub fn parsed_bib_tree(&self, file: SourceFile) -> BibSyntaxNode {
self.0.parsed_bib_tree(file)
}
pub fn bib_semantic_model(&self, file: SourceFile) -> &BibModel {
self.0.bib_semantic_model(file)
}
pub fn resolve_project(
&self,
members: Vec<ProjectMember>,
) -> (&ResolvedLabels, &ResolvedCitations) {
let project = Project::new(&self.0, members);
(
resolved_labels(&self.0, project),
resolved_citations(&self.0, project),
)
}
pub fn scope_signatures(&self, members: Vec<ProjectMember>, file: SourceFile) -> &SignatureDb {
let project = Project::new(&self.0, members);
scope_signatures(&self.0, project, file)
}
}
#[salsa::db]
impl salsa::Database for IncrementalDatabase {}
#[salsa::db]
impl IncrementalDb for IncrementalDatabase {
fn record_query(&self, entry: QueryLogEntry) {
self.query_log
.lock()
.expect("query log mutex poisoned")
.push(entry);
}
}