#![cfg_attr(docsrs, feature(doc_cfg))]
use std::sync::Arc;
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use gdscript_api::EngineApi;
use gdscript_base::FileId;
use gdscript_syntax::Parse;
use rustc_hash::FxBuildHasher;
use salsa::{Durability, Setter};
#[salsa::db]
pub trait Db: salsa::Database {
fn file_text(&self, file: FileId) -> Option<FileText>;
fn engine(&self) -> Option<&'static EngineApi>;
fn source_root(&self) -> Option<SourceRoot>;
fn project_config(&self) -> Option<ProjectConfig>;
}
#[salsa::input(debug)]
pub struct FileText {
#[returns(ref)]
pub text: Arc<str>,
pub file_id: FileId,
pub res_path: Option<smol_str::SmolStr>,
}
#[salsa::input]
pub struct SourceRoot {
#[returns(ref)]
pub files: Vec<FileText>,
}
#[salsa::input]
pub struct ProjectConfig {
#[returns(ref)]
pub project_godot_text: Arc<str>,
}
#[salsa::input]
pub struct EngineGeneration {
pub generation: u32,
}
#[derive(Debug, Default, Clone)]
pub struct Files {
inner: Arc<DashMap<FileId, FileText, FxBuildHasher>>,
}
impl Files {
#[must_use]
pub fn file_text(&self, file: FileId) -> Option<FileText> {
self.inner.get(&file).map(|r| *r)
}
pub fn set_file_text(&self, db: &mut dyn Db, file: FileId, text: &str, durability: Durability) {
match self.inner.entry(file) {
Entry::Occupied(occ) => {
occ.get()
.set_text(db)
.with_durability(durability)
.to(Arc::from(text));
}
Entry::Vacant(vac) => {
let ft = FileText::builder(Arc::from(text), file, None)
.durability(durability)
.new(db);
vac.insert(ft);
}
}
}
pub fn set_file_path(&self, db: &mut dyn Db, file: FileId, path: &str) {
let Some(ft) = self.inner.get(&file).map(|r| *r) else {
return;
};
if ft.res_path(&*db).as_deref() == Some(path) {
return;
}
ft.set_res_path(db)
.with_durability(Durability::MEDIUM)
.to(Some(smol_str::SmolStr::new(path)));
}
pub fn remove(&self, file: FileId) {
self.inner.remove(&file);
}
fn all(&self) -> Vec<FileText> {
let mut v: Vec<(FileId, FileText)> =
self.inner.iter().map(|r| (*r.key(), *r.value())).collect();
v.sort_by_key(|(id, _)| *id);
v.into_iter().map(|(_, ft)| ft).collect()
}
}
#[salsa::tracked]
pub fn parse(db: &dyn Db, file: FileText) -> Parse {
gdscript_syntax::parse(file.text(db))
}
#[salsa::db]
#[derive(Clone, Default)]
pub struct RootDatabase {
storage: salsa::Storage<Self>,
files: Files,
root: Option<SourceRoot>,
config: Option<ProjectConfig>,
engine: Option<&'static EngineApi>,
#[cfg(target_arch = "wasm32")]
engine_gen: Option<EngineGeneration>,
}
impl std::fmt::Debug for RootDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RootDatabase").finish_non_exhaustive()
}
}
impl RootDatabase {
pub fn set_file_text(&mut self, file: FileId, text: &str, durability: Durability) {
let files = self.files.clone();
files.set_file_text(self, file, text, durability);
}
pub fn set_file_path(&mut self, file: FileId, path: &str) {
let files = self.files.clone();
files.set_file_path(self, file, path);
}
pub fn remove_file(&mut self, file: FileId) {
self.files.remove(file);
}
pub fn set_project_config(&mut self, text: &str) {
if let Some(cfg) = self.config {
if cfg.project_godot_text(self).as_ref() == text {
return;
}
cfg.set_project_godot_text(self)
.with_durability(Durability::MEDIUM)
.to(Arc::from(text));
} else {
self.config = Some(
ProjectConfig::builder(Arc::from(text))
.durability(Durability::MEDIUM)
.new(self),
);
}
}
pub fn set_engine_api(&mut self, api: EngineApi) {
if self.engine.is_none() {
self.engine = Some(Box::leak(Box::new(api)));
#[cfg(target_arch = "wasm32")]
self.bump_engine_generation();
}
}
#[cfg(target_arch = "wasm32")]
fn bump_engine_generation(&mut self) {
if let Some(eg) = self.engine_gen {
let next = eg.generation(self).wrapping_add(1);
eg.set_generation(self)
.with_durability(Durability::MEDIUM)
.to(next);
} else {
self.engine_gen = Some(
EngineGeneration::builder(0)
.durability(Durability::MEDIUM)
.new(self),
);
}
}
pub fn sync_source_root(&mut self) {
#[cfg(target_arch = "wasm32")]
if self.engine_gen.is_none() {
self.engine_gen = Some(
EngineGeneration::builder(0)
.durability(Durability::MEDIUM)
.new(self),
);
}
let files = self.files.all();
if let Some(root) = self.root {
root.set_files(self)
.with_durability(Durability::MEDIUM)
.to(files);
} else {
let root = SourceRoot::builder(files)
.durability(Durability::MEDIUM)
.new(self);
self.root = Some(root);
}
}
}
#[salsa::db]
impl salsa::Database for RootDatabase {}
#[salsa::db]
impl Db for RootDatabase {
fn file_text(&self, file: FileId) -> Option<FileText> {
self.files.file_text(file)
}
#[allow(clippy::unnecessary_wraps)]
fn engine(&self) -> Option<&'static EngineApi> {
#[cfg(target_arch = "wasm32")]
if let Some(eg) = self.engine_gen {
let _ = eg.generation(self);
}
if let Some(api) = self.engine {
return Some(api);
}
#[cfg(not(target_arch = "wasm32"))]
{
Some(gdscript_api::bundled())
}
#[cfg(target_arch = "wasm32")]
{
None
}
}
fn source_root(&self) -> Option<SourceRoot> {
self.root
}
fn project_config(&self) -> Option<ProjectConfig> {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_query_returns_a_cst() {
let mut db = RootDatabase::default();
db.set_file_text(FileId(0), "func f():\n\tpass\n", Durability::LOW);
let ft = db.file_text(FileId(0)).unwrap();
let p = parse(&db, ft);
assert!(p.errors().is_empty());
assert_eq!(parse(&db, ft).debug_tree(), p.debug_tree());
}
#[test]
fn set_get_remove_round_trips() {
let mut db = RootDatabase::default();
let id = FileId(7);
db.set_file_text(id, "var x = 1\n", Durability::LOW);
assert_eq!(db.file_text(id).unwrap().text(&db).as_ref(), "var x = 1\n");
db.set_file_text(id, "var y = 2\n", Durability::LOW);
assert_eq!(db.file_text(id).unwrap().text(&db).as_ref(), "var y = 2\n");
db.remove_file(id);
assert!(db.file_text(id).is_none());
}
#[test]
fn res_path_round_trips_and_guards_no_op_sets() {
let mut db = RootDatabase::default();
let id = FileId(3);
db.set_file_text(id, "class_name A\n", Durability::LOW);
assert_eq!(db.file_text(id).unwrap().res_path(&db), None);
db.set_file_path(id, "res://a.gd");
assert_eq!(
db.file_text(id).unwrap().res_path(&db).as_deref(),
Some("res://a.gd")
);
db.set_file_path(id, "res://a.gd");
db.set_file_path(id, "res://b.gd");
assert_eq!(
db.file_text(id).unwrap().res_path(&db).as_deref(),
Some("res://b.gd")
);
db.set_file_path(FileId(999), "res://ghost.gd");
assert!(db.file_text(FileId(999)).is_none());
}
}