use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use salsa::Setter;
use crate::parser::parse;
use crate::project::{IncludeEdgeKey, collect_include_edge_keys};
use crate::semantic::SemanticModel;
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,
IncludeEdges,
ProjectGraph,
}
#[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>,
}
#[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 parsed = 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();
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 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::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()
}
}
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 existing = self
.files
.lock()
.expect("file cache mutex poisoned")
.get(path)
.copied();
match existing {
Some(file) => {
if file.text(self) != &text {
file.set_text(self).to(text);
}
file
}
None => {
let file = SourceFile::new(self, path.to_path_buf(), text);
self.files
.lock()
.expect("file cache mutex poisoned")
.insert(path.to_path_buf(), file);
file
}
}
}
pub fn lookup_file(&self, path: &Path) -> Option<SourceFile> {
self.files
.lock()
.expect("file cache mutex poisoned")
.get(path)
.copied()
}
pub fn remove_file(&mut self, path: &Path) -> Option<SourceFile> {
self.files
.lock()
.expect("file cache mutex poisoned")
.remove(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 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 parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
self.0.parse_diagnostics(file)
}
pub fn parsed_tree(&self, file: SourceFile) -> SyntaxNode {
self.0.parsed_tree(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);
}
}