use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use salsa::Setter;
use crate::parser::{ParseDiagnostic, parse};
use crate::syntax::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,
}
#[derive(Debug, Clone)]
pub struct ParsedDocument {
pub green: rowan::GreenNode,
pub diagnostics: Vec<ParseDiagnostic>,
}
#[salsa::db]
pub trait IncrementalDb: salsa::Database {}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_update_types))]
pub fn parsed_document(db: &dyn IncrementalDb, file: SourceFile) -> ParsedDocument {
let text = file.text(db);
let parsed = parse(text.as_str());
ParsedDocument {
green: parsed.cst.green().into_owned(),
diagnostics: parsed.diagnostics,
}
}
pub fn parse_diagnostics(db: &dyn IncrementalDb, file: SourceFile) -> &[ParseDiagnostic] {
&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())
}
pub 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::db]
pub struct IncrementalDatabase {
storage: salsa::Storage<Self>,
source_map: Arc<Mutex<FileSourceMap>>,
}
impl Default for IncrementalDatabase {
fn default() -> Self {
Self {
storage: salsa::Storage::new(None),
source_map: Arc::new(Mutex::new(FileSourceMap::default())),
}
}
}
impl std::fmt::Debug for IncrementalDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IncrementalDatabase")
.finish_non_exhaustive()
}
}
#[salsa::db]
impl salsa::Database for IncrementalDatabase {}
#[salsa::db]
impl IncrementalDb for IncrementalDatabase {}
impl IncrementalDatabase {
pub fn new() -> Self {
Self::default()
}
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 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(key.clone()), 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> {
let key = normalize_path(path);
self.source_map
.lock()
.expect("file source map mutex poisoned")
.by_path
.get(&key)
.copied()
}
pub fn set_file_text(&mut self, file: SourceFile, text: impl Into<String>) {
file.set_text(self).to(text.into());
}
pub fn parsed_tree(&self, file: SourceFile) -> SyntaxNode {
parsed_tree_root(self, file)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_and_reparses_on_edit() {
let mut db = IncrementalDatabase::new();
let file = db.add_file("x = 1\n");
assert_eq!(parsed_tree_root(&db, file).to_string(), "x = 1\n");
db.set_file_text(file, "x = 2 + 3\n");
let root = parsed_tree_root(&db, file);
assert_eq!(root.to_string(), "x = 2 + 3\n");
assert!(parse_diagnostics(&db, file).is_empty());
}
#[test]
fn upsert_dedups_by_normalized_path() {
let mut db = IncrementalDatabase::new();
let a = db.upsert_file(Path::new("/tmp/a.jl"), "x = 1\n".into());
let b = db.upsert_file(Path::new("/tmp/./a.jl"), "x = 2\n".into());
assert!(a == b, "equivalent path spellings should reuse one input");
assert_eq!(parsed_tree_root(&db, a).to_string(), "x = 2\n");
}
}