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, PackageInfo, ReadBinding, ReadSite, SourceEdgeKey, TopLevelEvent,
collect_source_literal_edges, collect_top_level_events, collect_top_level_events_spanned,
discover_packages, project_defs, project_graph, project_reads, relative_path,
reverse_source_edges, workspace_project,
};
use crate::rindex::provider::IndexedProvider;
use crate::rindex::remote::RemoteExports;
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,
}
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
}
fn to_forward_slash(path: &Path) -> String {
let s = path.to_string_lossy();
if std::path::MAIN_SEPARATOR == '/' {
s.into_owned()
} else {
s.replace(std::path::MAIN_SEPARATOR, "/")
}
}
#[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>,
#[returns(ref)]
pub remote: Arc<RemoteExports>,
}
#[salsa::input(singleton)]
pub struct Workspace {
#[returns(ref)]
pub members: Vec<SourceFile>,
#[returns(ref)]
pub roots: Vec<PathBuf>,
}
#[salsa::input(singleton)]
pub struct PackageGraph {
#[returns(ref)]
pub packages: Vec<PackageInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QueryKind {
ParsedDocument,
SemanticModel,
FileExports,
FileFreeReads,
FileDefSites,
SourceEdges,
TopLevelEvents,
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::tracked(returns(ref))]
pub fn top_level_events(db: &dyn IncrementalDb, file: SourceFile) -> Vec<TopLevelEvent> {
db.record_query(QueryLogEntry {
kind: QueryKind::TopLevelEvents,
file: Some(file),
});
let root = parsed_tree_root(db, file);
let base_dir = file.path(db).as_deref().and_then(Path::parent);
collect_top_level_events(&root, base_dir, semantic_model(db, file))
}
#[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());
}
fn library_index_or_empty(&mut self) -> LibraryIndex {
match LibraryIndex::try_get(self) {
Some(index) => index,
None => {
let index = LibraryIndex::new(
self,
Arc::new(IndexedProvider::empty()),
Arc::new(RemoteExports::new()),
);
index
.set_data(self)
.with_durability(Durability::HIGH)
.to(Arc::new(IndexedProvider::empty()));
index
.set_remote(self)
.with_durability(Durability::HIGH)
.to(Arc::new(RemoteExports::new()));
index
}
}
}
pub fn set_library_index(&mut self, indexed: IndexedProvider) -> LibraryIndex {
let index = self.library_index_or_empty();
index
.set_data(self)
.with_durability(Durability::HIGH)
.to(Arc::new(indexed));
index
}
pub fn set_remote_exports(&mut self, remote: RemoteExports) -> LibraryIndex {
let index = self.library_index_or_empty();
index
.set_remote(self)
.with_durability(Durability::HIGH)
.to(Arc::new(remote));
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();
let ws = 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
}
};
self.refresh_package_graph();
ws
}
pub fn refresh_package_graph(&mut self) -> PackageGraph {
let member_paths: Vec<PathBuf> = Workspace::try_get(self)
.map(|ws| {
ws.members(self)
.iter()
.filter_map(|file| file.path(self).clone())
.collect()
})
.unwrap_or_default();
let packages = discover_packages(&member_paths);
match PackageGraph::try_get(self) {
Some(graph) => {
if graph.packages(self) != &packages {
graph
.set_packages(self)
.with_durability(Durability::MEDIUM)
.to(packages);
}
graph
}
None => {
let graph = PackageGraph::new(self, packages.clone());
graph
.set_packages(self)
.with_durability(Durability::MEDIUM)
.to(packages);
graph
}
}
}
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 remote_exports(&self) -> Option<Arc<RemoteExports>> {
LibraryIndex::try_get(self).map(|index| index.remote(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())
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct CrossFileBinding {
pub cohort: Vec<PathBuf>,
pub readers: Vec<PathBuf>,
pub conflict: bool,
pub cohort_incomplete: bool,
pub dynamic_source_risk: bool,
}
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 workspace_symbols(
&self,
matches: impl Fn(&str) -> bool,
) -> Vec<(String, DefKind, PathBuf, TextRange)> {
if self.0.workspace().is_none() {
return Vec::new();
}
let project = workspace_project(&self.0);
let index = project_defs(&self.0, project);
index
.by_name
.iter()
.filter(|(name, _)| matches(name))
.flat_map(|(name, sites)| sites.iter().map(move |site| (name, site)))
.filter_map(|(name, (path, kind))| {
let file = self.0.lookup_file(path)?;
Some((
name.clone(),
*kind,
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 cross_file_binding(&self, def_file: &Path, name: &str) -> CrossFileBinding {
if self.0.workspace().is_none() {
return CrossFileBinding::default();
}
let project = workspace_project(&self.0);
let graph = project_graph(&self.0, project);
let defs = project_defs(&self.0, project);
let reads = project_reads(&self.0, project);
let def_paths: BTreeSet<PathBuf> = defs
.by_name
.get(name)
.map(|sites| sites.iter().map(|(path, _kind)| path.clone()).collect())
.unwrap_or_default();
let siblings = graph.package_siblings(def_file);
let cohort: Vec<PathBuf> = def_paths
.iter()
.filter(|d| d.as_path() == def_file || siblings.contains(d.as_path()))
.cloned()
.collect();
let conflict = cohort.len() > 1;
debug_assert!(
cohort.len() <= 1
|| cohort.iter().all(|d| d.as_path() == def_file
|| graph.package_siblings(def_file).contains(d.as_path())),
"multi-def cohort must be pure package siblings of def_file"
);
let cohort_incomplete = conflict && !graph.package_complete(def_file);
let seen_by = graph.seen_by(def_file);
let readers: Vec<PathBuf> = reads
.by_name
.get(name)
.map(|paths| {
paths
.iter()
.filter(|r| seen_by.contains(r.as_path()) && !def_paths.contains(r.as_path()))
.cloned()
.collect()
})
.unwrap_or_default();
let rev = reverse_source_edges(&self.0, project);
let dynamic_source_risk = !rev.dynamic_sources.is_empty()
&& reads.by_name.get(name).is_some_and(|name_readers| {
name_readers.iter().any(|r| {
rev.dynamic_sources
.iter()
.any(|d| r == d || graph.seen_by(d).contains(r.as_path()))
})
});
CrossFileBinding {
cohort,
readers,
conflict,
cohort_incomplete,
dynamic_source_risk,
}
}
pub fn reader_rename_ranges(
&self,
reader: &Path,
name: &str,
cohort: &[PathBuf],
) -> Option<Vec<TextRange>> {
let file = self.lookup_file(reader)?;
let all_reads = self.read_ranges_in(file, name);
if self.0.workspace().is_none() {
return Some(all_reads);
}
let project = workspace_project(&self.0);
let graph = project_graph(&self.0, project);
let final_site = graph.final_scope_binding(reader, name);
if matches!(final_site, ReadSite::Unknown) {
return None;
}
let skip_body = matches!(&final_site, ReadSite::Bound(def) if !cohort.contains(def));
if !skip_body {
match graph.top_level_read_binding(reader, name) {
ReadBinding::NoTopLevelRead => return Some(all_reads),
ReadBinding::Resolved(def) if cohort.contains(&def) => return Some(all_reads),
_ => {}
}
}
let root = self.parsed_tree(file);
let model = self.semantic_model(file);
let base_dir = reader.parent();
let spanned = collect_top_level_events_spanned(&root, base_dir, model);
let provenance = graph.top_level_read_provenance(reader, name, &spanned);
let mut skip: Vec<TextRange> = Vec::new();
let mut top_level_ranges: Vec<TextRange> = Vec::new();
for (range, site) in provenance {
top_level_ranges.push(range);
match site {
ReadSite::Bound(def) if cohort.contains(&def) => {}
ReadSite::Bound(_) | ReadSite::Unbound => skip.push(range),
ReadSite::Unknown => return None,
}
}
if skip_body {
for range in &all_reads {
if !top_level_ranges.contains(range) {
skip.push(*range);
}
}
}
Some(
all_reads
.into_iter()
.filter(|range| !skip.contains(range))
.collect(),
)
}
pub fn visible_def_files(&self, from_file: &Path, name: &str) -> Vec<PathBuf> {
if self.0.workspace().is_none() {
return Vec::new();
}
let project = workspace_project(&self.0);
let graph = project_graph(&self.0, project);
let defs = project_defs(&self.0, project);
let seen = graph.sees(from_file);
defs.by_name
.get(name)
.map(|sites| {
sites
.iter()
.map(|(path, _kind)| path.clone())
.filter(|path| seen.contains(path.as_path()))
.collect()
})
.unwrap_or_default()
}
pub fn source_rename_edits(
&self,
renames: &[(PathBuf, PathBuf)],
) -> Vec<(PathBuf, TextRange, String)> {
if self.0.workspace().is_none() {
return Vec::new();
}
let targets: Vec<(PathBuf, PathBuf)> = renames
.iter()
.map(|(old, new)| (normalize_path(old), normalize_path(new)))
.filter(|(old, new)| old != new)
.collect();
if targets.is_empty() {
return Vec::new();
}
let project = workspace_project(&self.0);
let rev = reverse_source_edges(&self.0, project);
let mut sourcers: BTreeSet<PathBuf> = BTreeSet::new();
for (key, members) in &rev.sourced_by {
if targets.iter().any(|(old, _)| normalize_path(key) == *old) {
sourcers.extend(members.iter().cloned());
}
}
let mut edits = Vec::new();
for sourcer in sourcers {
let Some(file) = self.lookup_file(&sourcer) else {
continue;
};
let text = self.file_text(file);
let root = parse(text).cst;
let base_dir = sourcer.parent();
for edge in collect_source_literal_edges(&root, base_dir) {
let edge_norm = normalize_path(&edge.target);
let Some((_, new)) = targets.iter().find(|(old, _)| *old == edge_norm) else {
continue;
};
let new_spelling = if edge.was_relative {
base_dir
.map(normalize_path)
.and_then(|dir| relative_path(&dir, new))
.map(|rel| to_forward_slash(&rel))
.unwrap_or_else(|| to_forward_slash(new))
} else {
to_forward_slash(new)
};
let quote = edge.quote as char;
if new_spelling.as_bytes().contains(&edge.quote) {
continue;
}
edits.push((
sourcer.clone(),
edge.literal_range,
format!("{quote}{new_spelling}{quote}"),
));
}
}
edits
}
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 fn remote_exports(&self) -> Option<Arc<RemoteExports>> {
self.0
.library_index()
.map(|index| index.remote(&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));
}
}