mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-04 17:30:37 +00:00
workspace
This commit is contained in:
parent
5749b7df98
commit
75385e0254
8 changed files with 776 additions and 0 deletions
279
Cargo.lock
generated
279
Cargo.lock
generated
|
@ -168,6 +168,12 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.1"
|
||||
|
@ -403,6 +409,17 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "djls"
|
||||
version = "5.2.0-alpha"
|
||||
|
@ -453,10 +470,13 @@ name = "djls-server"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"dashmap",
|
||||
"djls-conf",
|
||||
"djls-dev",
|
||||
"djls-project",
|
||||
"djls-templates",
|
||||
"djls-workspace",
|
||||
"percent-encoding",
|
||||
"pyo3",
|
||||
"salsa",
|
||||
|
@ -467,6 +487,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -481,6 +502,16 @@ dependencies = [
|
|||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "djls-workspace"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"dashmap",
|
||||
"salsa",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.5.2"
|
||||
|
@ -564,6 +595,15 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
|
@ -718,6 +758,113 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.10.0"
|
||||
|
@ -817,6 +964,12 @@ version = "0.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
|
@ -1056,6 +1209,15 @@ version = "1.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
@ -1465,6 +1627,12 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
@ -1613,6 +1781,16 @@ dependencies = [
|
|||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
|
@ -1859,6 +2037,23 @@ version = "0.2.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
|
@ -2111,6 +2306,12 @@ dependencies = [
|
|||
"bitflags 2.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.3"
|
||||
|
@ -2121,3 +2322,81 @@ dependencies = [
|
|||
"encoding_rs",
|
||||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ djls-dev = { path = "crates/djls-dev" }
|
|||
djls-project = { path = "crates/djls-project" }
|
||||
djls-server = { path = "crates/djls-server" }
|
||||
djls-templates = { path = "crates/djls-templates" }
|
||||
djls-workspace = { path = "crates/djls-workspace" }
|
||||
|
||||
# core deps, pin exact versions
|
||||
pyo3 = "0.25.0"
|
||||
|
@ -17,8 +18,10 @@ salsa = "0.23.0"
|
|||
tower-lsp-server = { version = "0.22.0", features = ["proposed"] }
|
||||
|
||||
anyhow = "1.0"
|
||||
camino = "1.1"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
config = { version ="0.15", features = ["toml"] }
|
||||
dashmap = "6.1"
|
||||
directories = "6.0"
|
||||
percent-encoding = "2.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -29,6 +32,7 @@ toml = "0.9"
|
|||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] }
|
||||
url = "2.5"
|
||||
which = "8.0"
|
||||
|
||||
# testing
|
||||
|
|
|
@ -11,8 +11,11 @@ default = []
|
|||
djls-conf = { workspace = true }
|
||||
djls-project = { workspace = true }
|
||||
djls-templates = { workspace = true }
|
||||
djls-workspace = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
percent-encoding = { workspace = true }
|
||||
pyo3 = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
|
@ -23,6 +26,7 @@ tower-lsp-server = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
djls-dev = { workspace = true }
|
||||
|
|
15
crates/djls-workspace/Cargo.toml
Normal file
15
crates/djls-workspace/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "djls-workspace"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
camino = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
108
crates/djls-workspace/src/bridge.rs
Normal file
108
crates/djls-workspace/src/bridge.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
//! Bridge between VFS snapshots and Salsa inputs.
|
||||
//!
|
||||
//! The bridge module isolates Salsa input mutation behind a single, idempotent API.
|
||||
//! It ensures we only touch Salsa when content or classification changes, maximizing
|
||||
//! incremental performance.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use salsa::Setter;
|
||||
|
||||
use super::{
|
||||
db::{Database, FileKindMini, SourceFile, TemplateLoaderOrder},
|
||||
vfs::{FileKind, VfsSnapshot},
|
||||
FileId,
|
||||
};
|
||||
|
||||
/// Owner of the Salsa [`Database`] plus the handles for updating inputs.
|
||||
///
|
||||
/// [`FileStore`] serves as the bridge between the VFS (with [`FileId`]s) and Salsa (with entities).
|
||||
/// It maintains a mapping from [`FileId`]s to [`SourceFile`] entities and manages the global
|
||||
/// [`TemplateLoaderOrder`] input. The [`FileStore`] ensures that Salsa inputs are only mutated
|
||||
/// when actual changes occur, preserving incremental computation efficiency.
|
||||
pub struct FileStore {
|
||||
/// The Salsa DB instance
|
||||
pub db: Database,
|
||||
/// Map from [`FileId`] to its Salsa input entity
|
||||
files: HashMap<FileId, SourceFile>,
|
||||
/// Handle to the global template loader configuration input
|
||||
template_loader: Option<TemplateLoaderOrder>,
|
||||
}
|
||||
|
||||
impl FileStore {
|
||||
/// Construct an empty store and DB.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
db: Database::default(),
|
||||
files: HashMap::new(),
|
||||
template_loader: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create or update the global template loader order input.
|
||||
///
|
||||
/// Sets the ordered list of template root directories that Django will search
|
||||
/// when resolving template names. If the input already exists, it updates the
|
||||
/// existing value; otherwise, it creates a new [`TemplateLoaderOrder`] input.
|
||||
pub fn set_template_loader_order(&mut self, ordered_roots: Vec<String>) {
|
||||
let roots = Arc::from(ordered_roots.into_boxed_slice());
|
||||
if let Some(tl) = self.template_loader {
|
||||
tl.set_roots(&mut self.db).to(roots);
|
||||
} else {
|
||||
self.template_loader = Some(TemplateLoaderOrder::new(&self.db, roots));
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror a VFS snapshot into Salsa inputs.
|
||||
///
|
||||
/// This method is the core synchronization point between the VFS and Salsa.
|
||||
/// It iterates through all files in the snapshot and:
|
||||
/// - Creates [`SourceFile`] inputs for new files
|
||||
/// - Updates `.text` and `.kind` only when changed to preserve incremental reuse
|
||||
///
|
||||
/// The method is idempotent and minimizes Salsa invalidations by checking for
|
||||
/// actual changes before updating inputs.
|
||||
pub fn apply_vfs_snapshot(&mut self, snap: &VfsSnapshot) {
|
||||
for (id, rec) in &snap.files {
|
||||
let new_text = snap.get_text(*id).unwrap_or_else(|| Arc::<str>::from(""));
|
||||
let new_kind = match rec.meta.kind {
|
||||
FileKind::Python => FileKindMini::Python,
|
||||
FileKind::Template => FileKindMini::Template,
|
||||
FileKind::Other => FileKindMini::Other,
|
||||
};
|
||||
|
||||
if let Some(sf) = self.files.get(id) {
|
||||
// Update if changed — avoid touching Salsa when not needed
|
||||
if sf.kind(&self.db) != new_kind {
|
||||
sf.set_kind(&mut self.db).to(new_kind.clone());
|
||||
}
|
||||
if sf.text(&self.db).as_ref() != &*new_text {
|
||||
sf.set_text(&mut self.db).to(new_text.clone());
|
||||
}
|
||||
} else {
|
||||
let sf = SourceFile::new(&self.db, new_kind, new_text);
|
||||
self.files.insert(*id, sf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the text content of a file by its [`FileId`].
|
||||
///
|
||||
/// Returns `None` if the file is not tracked in the [`FileStore`].
|
||||
pub fn file_text(&self, id: FileId) -> Option<Arc<str>> {
|
||||
self.files.get(&id).map(|sf| sf.text(&self.db).clone())
|
||||
}
|
||||
|
||||
/// Get the file kind classification by its [`FileId`].
|
||||
///
|
||||
/// Returns `None` if the file is not tracked in the [`FileStore`].
|
||||
pub fn file_kind(&self, id: FileId) -> Option<FileKindMini> {
|
||||
self.files.get(&id).map(|sf| sf.kind(&self.db))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
91
crates/djls-workspace/src/db.rs
Normal file
91
crates/djls-workspace/src/db.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
//! Salsa database and input entities for workspace.
|
||||
//!
|
||||
//! This module defines the Salsa world—what can be set and tracked incrementally.
|
||||
//! Inputs are kept minimal to avoid unnecessary recomputation.
|
||||
|
||||
use std::sync::Arc;
|
||||
#[cfg(test)]
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Salsa database root for workspace
|
||||
///
|
||||
/// The [`Database`] provides default storage and, in tests, captures Salsa events for
|
||||
/// reuse/diagnostics. It serves as the core incremental computation engine, tracking
|
||||
/// dependencies and invalidations across all inputs and derived queries.
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(not(test), derive(Default))]
|
||||
pub struct Database {
|
||||
storage: salsa::Storage<Self>,
|
||||
|
||||
// The logs are only used for testing and demonstrating reuse:
|
||||
#[cfg(test)]
|
||||
logs: Arc<Mutex<Option<Vec<String>>>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Default for Database {
|
||||
fn default() -> Self {
|
||||
let logs = <Arc<Mutex<Option<Vec<String>>>>>::default();
|
||||
Self {
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let logs = logs.clone();
|
||||
move |event| {
|
||||
eprintln!("Event: {event:?}");
|
||||
// Log interesting events, if logging is enabled
|
||||
if let Some(logs) = &mut *logs.lock().unwrap() {
|
||||
// only log interesting events
|
||||
if let salsa::EventKind::WillExecute { .. } = event.kind {
|
||||
logs.push(format!("Event: {event:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}))),
|
||||
logs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for Database {}
|
||||
|
||||
/// Minimal classification for analysis routing.
|
||||
///
|
||||
/// [`FileKindMini`] provides a lightweight categorization of files to determine which
|
||||
/// analysis pipelines should process them. This is the Salsa-side representation
|
||||
/// of file types, mapped from the VFS layer's `vfs::FileKind`.
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum FileKindMini {
|
||||
/// Python source file (.py)
|
||||
Python,
|
||||
/// Django template file (.html, .jinja, etc.)
|
||||
Template,
|
||||
/// Other file types not requiring specialized analysis
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Represents a single file's classification and current content.
|
||||
///
|
||||
/// [`SourceFile`] is a Salsa input entity that tracks both the file's type (for routing
|
||||
/// to appropriate analyzers) and its current text content. The text is stored as
|
||||
/// `Arc<str>` for efficient sharing across the incremental computation graph.
|
||||
#[salsa::input]
|
||||
pub struct SourceFile {
|
||||
/// The file's classification for analysis routing
|
||||
pub kind: FileKindMini,
|
||||
/// The current text content of the file
|
||||
#[returns(ref)]
|
||||
pub text: Arc<str>,
|
||||
}
|
||||
|
||||
/// Global input configuring ordered template loader roots.
|
||||
///
|
||||
/// [`TemplateLoaderOrder`] represents the Django `TEMPLATES[n]['DIRS']` configuration,
|
||||
/// defining the search order for template resolution. This is a global input that
|
||||
/// affects template name resolution across the entire project.
|
||||
#[salsa::input]
|
||||
pub struct TemplateLoaderOrder {
|
||||
/// Ordered list of template root directories
|
||||
#[returns(ref)]
|
||||
pub roots: Arc<[String]>,
|
||||
}
|
30
crates/djls-workspace/src/lib.rs
Normal file
30
crates/djls-workspace/src/lib.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
mod bridge;
|
||||
mod db;
|
||||
mod vfs;
|
||||
|
||||
// Re-export public API
|
||||
pub use bridge::FileStore;
|
||||
pub use db::{Database, FileKindMini, SourceFile, TemplateLoaderOrder};
|
||||
pub use vfs::{FileKind, FileMeta, FileRecord, Revision, TextSource, Vfs, VfsSnapshot};
|
||||
|
||||
/// Stable, compact identifier for files across the subsystem.
|
||||
///
|
||||
/// [`FileId`] decouples file identity from paths/URIs, providing efficient keys for maps and
|
||||
/// Salsa inputs. Once assigned to a file (via its URI), a [`FileId`] remains stable for the
|
||||
/// lifetime of the VFS, even if the file's content or metadata changes.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||
pub struct FileId(u32);
|
||||
|
||||
impl FileId {
|
||||
/// Create a [`FileId`] from a raw u32 value.
|
||||
#[must_use]
|
||||
pub fn from_raw(raw: u32) -> Self {
|
||||
FileId(raw)
|
||||
}
|
||||
|
||||
/// Get the underlying u32 index value.
|
||||
#[must_use]
|
||||
pub fn index(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
245
crates/djls-workspace/src/vfs.rs
Normal file
245
crates/djls-workspace/src/vfs.rs
Normal file
|
@ -0,0 +1,245 @@
|
|||
//! Change-tracked, concurrent virtual file system keyed by [`FileId`].
|
||||
//!
|
||||
//! The VFS provides thread-safe, identity-stable storage with cheap change detection
|
||||
//! and snapshotting. Downstream systems consume snapshots to avoid locking and to
|
||||
//! batch updates.
|
||||
|
||||
use camino::Utf8PathBuf;
|
||||
use dashmap::DashMap;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicU32, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use super::FileId;
|
||||
|
||||
/// Monotonic counter representing global VFS state.
|
||||
///
|
||||
/// [`Revision`] increments whenever file content changes occur in the VFS.
|
||||
/// This provides a cheap way to detect if any changes have occurred since
|
||||
/// a previous snapshot was taken.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default, PartialOrd, Ord)]
|
||||
pub struct Revision(u64);
|
||||
|
||||
impl Revision {
|
||||
/// Create a [`Revision`] from a raw u64 value.
|
||||
pub fn from_raw(raw: u64) -> Self {
|
||||
Revision(raw)
|
||||
}
|
||||
|
||||
/// Get the underlying u64 value.
|
||||
pub fn value(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// File classification at the VFS layer.
|
||||
///
|
||||
/// [`FileKind`] determines how a file should be processed by downstream analyzers.
|
||||
/// This classification is performed when files are first ingested into the VFS.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum FileKind {
|
||||
/// Python source file
|
||||
Python,
|
||||
/// Django template file
|
||||
Template,
|
||||
/// Other file type
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Metadata associated with a file in the VFS.
|
||||
///
|
||||
/// [`FileMeta`] contains all non-content information about a file, including its
|
||||
/// identity (URI), filesystem path, classification, and optional version number
|
||||
/// from the LSP client.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileMeta {
|
||||
/// The file's URI (typically file:// scheme)
|
||||
pub uri: Url,
|
||||
/// The file's path in the filesystem
|
||||
pub path: Utf8PathBuf,
|
||||
/// Classification for routing to analyzers
|
||||
pub kind: FileKind,
|
||||
/// Optional LSP document version
|
||||
pub version: Option<i64>,
|
||||
}
|
||||
|
||||
/// Source of text content in the VFS.
|
||||
///
|
||||
/// [`TextSource`] tracks where file content originated from, which is useful for
|
||||
/// debugging and understanding the current state of the VFS. All variants hold
|
||||
/// `Arc<str>` for efficient sharing.
|
||||
#[derive(Clone)]
|
||||
pub enum TextSource {
|
||||
/// Content loaded from disk
|
||||
Disk(Arc<str>),
|
||||
/// Content from LSP client overlay (in-memory edits)
|
||||
Overlay(Arc<str>),
|
||||
/// Content generated programmatically
|
||||
Generated(Arc<str>),
|
||||
}
|
||||
|
||||
/// Complete record of a file in the VFS.
|
||||
///
|
||||
/// [`FileRecord`] combines metadata, current text content, and a content hash
|
||||
/// for efficient change detection.
|
||||
#[derive(Clone)]
|
||||
pub struct FileRecord {
|
||||
/// File metadata (URI, path, kind, version)
|
||||
pub meta: FileMeta,
|
||||
/// Current text content and its source
|
||||
pub text: TextSource,
|
||||
/// Hash of current content for change detection
|
||||
pub hash: u64,
|
||||
}
|
||||
|
||||
/// Thread-safe virtual file system with change tracking.
|
||||
///
|
||||
/// [`Vfs`] provides concurrent access to file content with stable [`FileId`] assignment,
|
||||
/// content hashing for change detection, and atomic snapshot generation. It uses
|
||||
/// `DashMap` for lock-free concurrent access and atomic counters for revision tracking.
|
||||
pub struct Vfs {
|
||||
/// Atomic counter for generating unique [`FileId`]s
|
||||
next_file_id: AtomicU32,
|
||||
/// Map from URI to [`FileId`] for deduplication
|
||||
by_uri: DashMap<Url, FileId>,
|
||||
/// Map from [`FileId`] to [`FileRecord`] for content storage
|
||||
files: DashMap<FileId, FileRecord>,
|
||||
/// Global revision counter, incremented on content changes
|
||||
head: AtomicU64,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
/// Construct an empty VFS.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_file_id: AtomicU32::new(0),
|
||||
by_uri: DashMap::new(),
|
||||
files: DashMap::new(),
|
||||
head: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create a [`FileId`] for the given URI.
|
||||
///
|
||||
/// Returns the existing [`FileId`] if the URI is already known, or creates a new
|
||||
/// [`FileRecord`] with the provided metadata and text. This method computes and
|
||||
/// stores a content hash for change detection.
|
||||
pub fn intern_file(
|
||||
&self,
|
||||
uri: Url,
|
||||
path: Utf8PathBuf,
|
||||
kind: FileKind,
|
||||
text: TextSource,
|
||||
) -> FileId {
|
||||
if let Some(id) = self.by_uri.get(&uri).map(|entry| *entry) {
|
||||
return id;
|
||||
}
|
||||
let id = FileId(self.next_file_id.fetch_add(1, Ordering::SeqCst));
|
||||
let meta = FileMeta {
|
||||
uri: uri.clone(),
|
||||
path,
|
||||
kind,
|
||||
version: None,
|
||||
};
|
||||
let hash = content_hash(&text);
|
||||
self.by_uri.insert(uri, id);
|
||||
self.files.insert(id, FileRecord { meta, text, hash });
|
||||
id
|
||||
}
|
||||
|
||||
/// Set overlay text for a file, typically from LSP didChange events.
|
||||
///
|
||||
/// Updates the file's text to an Overlay variant with the new content.
|
||||
/// Only increments the global revision if the content actually changed
|
||||
/// (detected via hash comparison).
|
||||
///
|
||||
/// Returns a tuple of (new global revision, whether content changed).
|
||||
pub fn set_overlay(
|
||||
&self,
|
||||
id: FileId,
|
||||
version: Option<i64>,
|
||||
new_text: Arc<str>,
|
||||
) -> (Revision, bool) {
|
||||
let mut rec = self.files.get_mut(&id).expect("unknown file");
|
||||
rec.meta.version = version;
|
||||
let next = TextSource::Overlay(new_text);
|
||||
let new_hash = content_hash(&next);
|
||||
let changed = new_hash != rec.hash;
|
||||
if changed {
|
||||
rec.text = next;
|
||||
rec.hash = new_hash;
|
||||
self.head.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
(
|
||||
Revision::from_raw(self.head.load(Ordering::SeqCst)),
|
||||
changed,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create an immutable snapshot of the current VFS state.
|
||||
///
|
||||
/// Materializes a consistent view of all files for downstream consumers.
|
||||
/// The snapshot includes the current revision and a clone of all file records.
|
||||
/// This operation is relatively cheap due to `Arc` sharing of text content.
|
||||
pub fn snapshot(&self) -> VfsSnapshot {
|
||||
VfsSnapshot {
|
||||
revision: Revision::from_raw(self.head.load(Ordering::SeqCst)),
|
||||
files: self
|
||||
.files
|
||||
.iter()
|
||||
.map(|entry| (*entry.key(), entry.value().clone()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a stable hash over file content.
|
||||
///
|
||||
/// Used for efficient change detection - if the hash hasn't changed,
|
||||
/// the content hasn't changed, avoiding unnecessary Salsa invalidations.
|
||||
fn content_hash(src: &TextSource) -> u64 {
|
||||
let s: &str = match src {
|
||||
TextSource::Disk(s) | TextSource::Overlay(s) | TextSource::Generated(s) => s,
|
||||
};
|
||||
let mut h = DefaultHasher::new();
|
||||
s.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Immutable snapshot view of the VFS at a specific revision.
|
||||
///
|
||||
/// [`VfsSnapshot`] provides a consistent view of all files for downstream consumers,
|
||||
/// avoiding the need for locking during processing. Snapshots are created atomically
|
||||
/// and can be safely shared across threads.
|
||||
#[derive(Clone)]
|
||||
pub struct VfsSnapshot {
|
||||
/// The global revision at the time of snapshot
|
||||
pub revision: Revision,
|
||||
/// All files in the VFS at snapshot time
|
||||
pub files: HashMap<FileId, FileRecord>,
|
||||
}
|
||||
|
||||
impl VfsSnapshot {
|
||||
/// Get the text content of a file in this snapshot.
|
||||
///
|
||||
/// Returns `None` if the [`FileId`] is not present in the snapshot.
|
||||
pub fn get_text(&self, id: FileId) -> Option<Arc<str>> {
|
||||
self.files.get(&id).map(|r| match &r.text {
|
||||
TextSource::Disk(s) | TextSource::Overlay(s) | TextSource::Generated(s) => s.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the metadata for a file in this snapshot.
|
||||
///
|
||||
/// Returns `None` if the [`FileId`] is not present in the snapshot.
|
||||
pub fn meta(&self, id: FileId) -> Option<&FileMeta> {
|
||||
self.files.get(&id).map(|r| &r.meta)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue