use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use rowan::TextRange;
use salsa::{Durability, Setter};
use crate::parser::{ParseDiagnostic, diff_edit, map_range_through_edit, parse, reparse};
use crate::project::{DefKind, SourceEdgeKey, project_defs, project_reads, workspace_project};
use crate::rindex::provider::IndexedProvider;
use crate::semantic::{BindingKind, ScopeKind, SemanticModel};
use crate::syntax::{NodePtr, SyntaxNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FileId(pub u32);
#[salsa::input]
pub struct SourceFile {
pub id: FileId,
#[returns(ref)]
pub path: Option<PathBuf>,
#[returns(ref)]
pub text: String,
}
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
}
#[derive(Default)]
struct FileSourceMap {
by_path: HashMap<PathBuf, SourceFile>,
next_id: u32,
}
impl FileSourceMap {
fn alloc_id(&mut self) -> FileId {
let id = FileId(self.next_id);
self.next_id += 1;
id
}
}
#[salsa::input(singleton)]
pub struct LibraryIndex {
#[returns(ref)]
pub data: Arc<IndexedProvider>,
}
#[salsa::input(singleton)]
pub struct Workspace {
#[returns(ref)]
pub members: Vec<SourceFile>,
#[returns(ref)]
pub roots: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QueryKind {
ParsedDocument,
SemanticModel,
FileExports,
FileFreeReads,
FileDefSites,
SourceEdges,
ReverseSourceEdges,
WorkspaceProject,
ProjectGraph,
ProjectDefs,
ProjectReads,
VisibleSymbols,
LoadedNames,
ExternalResolution,
}
#[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 PrevParse {
pub text: String,
pub green: rowan::GreenNode,
pub diagnostics: Vec<ParseDiagnostic>,
}
#[salsa::db]
pub trait IncrementalDb: salsa::Database {
fn record_query(&self, entry: QueryLogEntry);
fn reparse_prev(&self, file: SourceFile) -> Option<Arc<PrevParse>>;
fn reparse_store(&self, file: SourceFile, prev: PrevParse, incremental: bool);
}
#[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 text = file.text(db);
let reparsed = db
.reparse_prev(file)
.filter(|prev| prev.text != *text)
.and_then(|prev| {
let edit = diff_edit(&prev.text, text);
let old_root = SyntaxNode::new_root(prev.green.clone());
reparse(&old_root, &prev.text, &prev.diagnostics, &edit)
});
let incremental = reparsed.is_some();
let (green, diagnostics): (rowan::GreenNode, Vec<ParseDiagnostic>) = match reparsed {
Some(r) => (r.green, r.diagnostics),
None => {
let parsed = parse(text.as_str());
(parsed.cst.green().into_owned(), parsed.diagnostics)
}
};
db.reparse_store(
file,
PrevParse {
text: text.clone(),
green: green.clone(),
diagnostics: diagnostics.clone(),
},
incremental,
);
let diagnostics = diagnostics
.into_iter()
.map(|diagnostic| ParseDiagnosticData {
message: diagnostic.message,
start: diagnostic.start,
end: diagnostic.end,
})
.collect();
ParsedDocument { 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 file_exports(db: &dyn IncrementalDb, file: SourceFile) -> BTreeSet<String> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileExports,
file: Some(file),
});
crate::project::file_exports(semantic_model(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_free_reads(db: &dyn IncrementalDb, file: SourceFile) -> BTreeSet<String> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileFreeReads,
file: Some(file),
});
crate::project::file_free_reads(semantic_model(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_def_sites(db: &dyn IncrementalDb, file: SourceFile) -> BTreeMap<String, DefKind> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileDefSites,
file: Some(file),
});
crate::project::file_def_sites(semantic_model(db, file), &parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn loaded_names(db: &dyn IncrementalDb, file: SourceFile) -> BTreeSet<String> {
db.record_query(QueryLogEntry {
kind: QueryKind::LoadedNames,
file: Some(file),
});
semantic_model(db, file)
.loaded_packages()
.iter()
.map(|pkg| pkg.name.to_string())
.collect()
}
#[salsa::tracked(returns(ref))]
pub fn source_edges(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SourceEdgeKey> {
db.record_query(QueryLogEntry {
kind: QueryKind::SourceEdges,
file: Some(file),
});
let root = parsed_tree_root(db, file);
let base_dir = file.path(db).as_deref().and_then(Path::parent);
crate::project::collect_source_edge_keys(&root, base_dir)
}
#[salsa::db]
pub struct IncrementalDatabase {
storage: salsa::Storage<Self>,
query_log: Arc<Mutex<Vec<QueryLogEntry>>>,
source_map: Arc<Mutex<FileSourceMap>>,
reparse_cache: Arc<Mutex<HashMap<SourceFile, Arc<PrevParse>>>>,
reparse_hits: Arc<AtomicU64>,
}
impl Default for IncrementalDatabase {
fn default() -> Self {
Self {
storage: salsa::Storage::new(None),
query_log: Arc::new(Mutex::new(Vec::new())),
source_map: Arc::new(Mutex::new(FileSourceMap::default())),
reparse_cache: Arc::new(Mutex::new(HashMap::new())),
reparse_hits: Arc::new(AtomicU64::new(0)),
}
}
}
impl Clone for IncrementalDatabase {
fn clone(&self) -> Self {
Self {
storage: self.storage.clone(),
query_log: Arc::clone(&self.query_log),
source_map: Arc::clone(&self.source_map),
reparse_cache: Arc::clone(&self.reparse_cache),
reparse_hits: Arc::clone(&self.reparse_hits),
}
}
}
impl std::fmt::Debug for IncrementalDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IncrementalDatabase")
.finish_non_exhaustive()
}
}
impl IncrementalDatabase {
pub fn add_file(&self, text: impl Into<String>) -> SourceFile {
let id = self
.source_map
.lock()
.expect("file source map mutex poisoned")
.alloc_id();
SourceFile::new(self, id, None, text.into())
}
pub fn set_file_text(&mut self, file: SourceFile, text: impl Into<String>) {
file.set_text(self).to(text.into());
}
pub fn set_library_index(&mut self, indexed: IndexedProvider) -> LibraryIndex {
let data = Arc::new(indexed);
match LibraryIndex::try_get(self) {
Some(index) => {
index
.set_data(self)
.with_durability(Durability::HIGH)
.to(data);
index
}
None => {
let index = LibraryIndex::new(self, Arc::clone(&data));
index
.set_data(self)
.with_durability(Durability::HIGH)
.to(data);
index
}
}
}
pub fn library_index(&self) -> Option<LibraryIndex> {
LibraryIndex::try_get(self)
}
pub fn set_workspace_members(
&mut self,
mut members: Vec<SourceFile>,
roots: Vec<PathBuf>,
) -> Workspace {
members.sort_by_key(|file| file.id(self));
members.dedup();
match Workspace::try_get(self) {
Some(ws) => {
if ws.members(self) != &members {
ws.set_members(self)
.with_durability(Durability::MEDIUM)
.to(members);
}
if ws.roots(self) != &roots {
ws.set_roots(self)
.with_durability(Durability::MEDIUM)
.to(roots);
}
ws
}
None => {
let ws = Workspace::new(self, members.clone(), roots.clone());
ws.set_members(self)
.with_durability(Durability::MEDIUM)
.to(members);
ws.set_roots(self)
.with_durability(Durability::MEDIUM)
.to(roots);
ws
}
}
}
pub fn workspace(&self) -> Option<Workspace> {
Workspace::try_get(self)
}
pub fn library_data(&self) -> Option<Arc<IndexedProvider>> {
LibraryIndex::try_get(self).map(|index| index.data(self).clone())
}
pub fn upsert_file(&mut self, path: &Path, text: String) -> SourceFile {
let key = normalize_path(path);
let existing = self
.source_map
.lock()
.expect("file source map mutex poisoned")
.by_path
.get(&key)
.copied();
match existing {
Some(file) => {
if file.text(self) != &text {
file.set_text(self).to(text);
}
file
}
None => {
let id = self
.source_map
.lock()
.expect("file source map mutex poisoned")
.alloc_id();
let file = SourceFile::new(self, id, Some(path.to_path_buf()), text);
self.source_map
.lock()
.expect("file source map mutex poisoned")
.by_path
.insert(key, file);
file
}
}
}
pub fn lookup_file(&self, path: &Path) -> Option<SourceFile> {
self.source_map
.lock()
.expect("file source map mutex poisoned")
.by_path
.get(&normalize_path(path))
.copied()
}
pub fn file_text(&self, file: SourceFile) -> &str {
file.text(self)
}
pub fn file_path(&self, file: SourceFile) -> Option<&Path> {
file.path(self).as_deref()
}
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 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 reparse_hits(&self) -> u64 {
self.reparse_hits.load(Ordering::Relaxed)
}
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) -> Option<&Path> {
self.0.file_path(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)
}
pub fn semantic_model(&self, file: SourceFile) -> &SemanticModel {
self.0.semantic_model(file)
}
pub fn def_range_in(&self, file: SourceFile, name: &str) -> Option<TextRange> {
let model = self.0.semantic_model(file);
model
.bindings()
.iter()
.find(|binding| {
matches!(binding.kind, BindingKind::Local | BindingKind::Implicit)
&& model.scope(binding.scope).kind == ScopeKind::File
&& binding.name.as_str() == name
})
.map(|binding| binding.def_range)
}
pub fn workspace_def_sites(&self, name: &str) -> Vec<(PathBuf, TextRange)> {
if self.0.workspace().is_none() {
return Vec::new();
}
let project = workspace_project(&self.0);
let index = project_defs(&self.0, project);
let Some(sites) = index.by_name.get(name) else {
return Vec::new();
};
sites
.iter()
.filter_map(|(path, _kind)| {
let file = self.0.lookup_file(path)?;
Some((path.clone(), self.def_range_in(file, name)?))
})
.collect()
}
pub fn read_ranges_in(&self, file: SourceFile, name: &str) -> Vec<TextRange> {
let model = self.0.semantic_model(file);
model
.idents()
.iter()
.filter(|ident| ident.name.as_str() == name && model.resolve_local(ident).is_none())
.map(|ident| ident.range)
.collect()
}
pub fn workspace_read_sites(&self, name: &str) -> Vec<(PathBuf, TextRange)> {
if self.0.workspace().is_none() {
return Vec::new();
}
let project = workspace_project(&self.0);
let index = project_reads(&self.0, project);
let Some(paths) = index.by_name.get(name) else {
return Vec::new();
};
paths
.iter()
.filter_map(|path| {
let file = self.0.lookup_file(path)?;
Some((path.clone(), file))
})
.flat_map(|(path, file)| {
self.read_ranges_in(file, name)
.into_iter()
.map(move |range| (path.clone(), range))
})
.collect()
}
pub fn resolve_ptr(
&self,
file: SourceFile,
ptr: NodePtr,
taken_at_text: &str,
) -> Option<SyntaxNode> {
let root = self.parsed_tree(file);
if self.file_text(file) == taken_at_text {
return ptr.try_to_node(&root);
}
let edit = diff_edit(taken_at_text, self.file_text(file));
let mapped = map_range_through_edit(ptr.text_range(), &edit)?;
ptr.with_range(mapped).try_to_node(&root)
}
pub fn library_index(&self) -> Option<LibraryIndex> {
self.0.library_index()
}
pub fn library_data(&self) -> Option<Arc<IndexedProvider>> {
self.0
.library_index()
.map(|index| index.data(&self.0).clone())
}
pub(crate) fn as_db(&self) -> &dyn IncrementalDb {
&self.0
}
}
#[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);
}
fn reparse_prev(&self, file: SourceFile) -> Option<Arc<PrevParse>> {
self.reparse_cache
.lock()
.expect("reparse cache mutex poisoned")
.get(&file)
.cloned()
}
fn reparse_store(&self, file: SourceFile, prev: PrevParse, incremental: bool) {
if incremental {
self.reparse_hits.fetch_add(1, Ordering::Relaxed);
}
self.reparse_cache
.lock()
.expect("reparse cache mutex poisoned")
.insert(file, Arc::new(prev));
}
}