diff --git a/Cargo.lock b/Cargo.lock index 2722022..79834e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,6 +518,7 @@ dependencies = [ "tempfile", "tokio", "tower-lsp-server", + "tracing", "url", ] diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index efb3bc8..ae472a9 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -216,7 +216,7 @@ impl LanguageServer for DjangoLanguageServer { language_id, ); - session.open_document(url, document); + session.open_document(&url, document); }) .await; } diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 2be2abd..8f4d9ea 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -41,7 +41,7 @@ use djls_workspace::{ db::{Database, SourceFile}, paths, Buffers, FileSystem, OsFileSystem, TextDocument, WorkspaceFileSystem, }; -use salsa::{Setter, StorageHandle}; +use salsa::StorageHandle; use tower_lsp_server::lsp_types; use url::Url; @@ -286,13 +286,13 @@ impl Session { /// This method coordinates both layers: /// - Layer 1: Stores the document content in buffers /// - Layer 2: Creates the SourceFile in Salsa (if path is resolvable) - pub fn open_document(&mut self, url: Url, document: TextDocument) { + pub fn open_document(&mut self, url: &Url, document: TextDocument) { tracing::debug!("Opening document: {}", url); // Layer 1: Set buffer self.buffers.open(url.clone(), document); - // Layer 2: Create file and bump revision if it already exists + // Layer 2: Create file and touch if it already exists // This is crucial: if the file was already read from disk, we need to // invalidate Salsa's cache so it re-reads through the buffer system if let Some(path) = paths::url_to_path(&url) { @@ -302,16 +302,8 @@ impl Session { let file = db.get_or_create_file(path.clone()); if already_exists { - // File was already read - bump revision to invalidate cache - let current_rev = file.revision(db); - let new_rev = current_rev + 1; - file.set_revision(db).to(new_rev); - tracing::debug!( - "Bumped revision for {} on open: {} -> {}", - path.display(), - current_rev, - new_rev - ); + // File was already read - touch to invalidate cache + db.touch_file(&path); } else { // New file - starts at revision 0 tracing::debug!( @@ -336,9 +328,9 @@ impl Session { // Layer 1: Update buffer self.buffers.update(url.clone(), document); - // Layer 2: Bump revision to trigger invalidation + // Layer 2: Touch file to trigger invalidation if let Some(path) = paths::url_to_path(url) { - self.notify_file_changed(&path); + self.with_db_mut(|db| db.touch_file(&path)); } } @@ -383,43 +375,15 @@ impl Session { ); } - // Layer 2: Bump revision to trigger re-read from disk + // Layer 2: Touch file to trigger re-read from disk // We keep the file alive for potential re-opening if let Some(path) = paths::url_to_path(url) { - self.notify_file_changed(&path); + self.with_db_mut(|db| db.touch_file(&path)); } removed } - /// Internal: Notify that a file's content has changed. - /// - /// This bumps the file's revision number in Salsa, which triggers - /// invalidation of any queries that depend on the file's content. - fn notify_file_changed(&mut self, path: &Path) { - self.with_db_mut(|db| { - // Only bump revision if file is already being tracked - // We don't create files just for notifications - if db.has_file(path) { - let file = db.get_or_create_file(path.to_path_buf()); - let current_rev = file.revision(db); - let new_rev = current_rev + 1; - file.set_revision(db).to(new_rev); - tracing::debug!( - "Bumped revision for {}: {} -> {}", - path.display(), - current_rev, - new_rev - ); - } else { - tracing::debug!( - "File {} not tracked, skipping revision bump", - path.display() - ); - } - }); - } - // ===== Safe Query API ===== // These methods encapsulate all Salsa interactions, preventing the // "mixed database instance" bug by never exposing SourceFile or Database. @@ -503,7 +467,7 @@ mod tests { 1, LanguageId::Other, ); - session.open_document(url.clone(), document); + session.open_document(&url, document); // Try to read content - this might be where it hangs println!("**[test]** try to read content - this might be where it hangs"); diff --git a/crates/djls-workspace/Cargo.toml b/crates/djls-workspace/Cargo.toml index b1fa2e0..e2fb358 100644 --- a/crates/djls-workspace/Cargo.toml +++ b/crates/djls-workspace/Cargo.toml @@ -15,6 +15,7 @@ percent-encoding = { workspace = true } salsa = { workspace = true } tokio = { workspace = true } tower-lsp-server = { workspace = true } +tracing = { workspace = true } url = { workspace = true } [dev-dependencies] diff --git a/crates/djls-workspace/src/db.rs b/crates/djls-workspace/src/db.rs index 36c52ce..3bcef5f 100644 --- a/crates/djls-workspace/src/db.rs +++ b/crates/djls-workspace/src/db.rs @@ -32,6 +32,7 @@ use std::sync::Arc; use std::sync::Mutex; use dashmap::DashMap; +use salsa::Setter; use crate::{FileKind, FileSystem}; @@ -160,6 +161,36 @@ impl Database { self.files.contains_key(path) } + /// Touch a file to mark it as modified, triggering re-evaluation of dependent queries. + /// + /// Similar to Unix `touch`, this updates the file's revision number to signal + /// that cached query results depending on this file should be invalidated. + /// + /// This is typically called when: + /// - A file is opened in the editor (if it was previously cached from disk) + /// - A file's content is modified + /// - A file's buffer is closed (reverting to disk content) + pub fn touch_file(&mut self, path: &Path) { + // Get the file if it exists + let Some(file_ref) = self.files.get(path) else { + tracing::debug!("File {} not tracked, skipping touch", path.display()); + return; + }; + let file = *file_ref; + drop(file_ref); // Explicitly drop to release the lock + + let current_rev = file.revision(self); + let new_rev = current_rev + 1; + file.set_revision(self).to(new_rev); + + tracing::debug!( + "Touched {}: revision {} -> {}", + path.display(), + current_rev, + new_rev + ); + } + /// Get a reference to the storage for handle extraction. /// /// This is used by `Session` to extract the [`StorageHandle`](salsa::StorageHandle) after mutations.