This commit is contained in:
Josh Thomas 2025-08-29 22:09:56 -05:00
parent 361d7e2598
commit 00fef522ad
5 changed files with 44 additions and 47 deletions

1
Cargo.lock generated
View file

@ -518,6 +518,7 @@ dependencies = [
"tempfile", "tempfile",
"tokio", "tokio",
"tower-lsp-server", "tower-lsp-server",
"tracing",
"url", "url",
] ]

View file

@ -216,7 +216,7 @@ impl LanguageServer for DjangoLanguageServer {
language_id, language_id,
); );
session.open_document(url, document); session.open_document(&url, document);
}) })
.await; .await;
} }

View file

@ -41,7 +41,7 @@ use djls_workspace::{
db::{Database, SourceFile}, db::{Database, SourceFile},
paths, Buffers, FileSystem, OsFileSystem, TextDocument, WorkspaceFileSystem, paths, Buffers, FileSystem, OsFileSystem, TextDocument, WorkspaceFileSystem,
}; };
use salsa::{Setter, StorageHandle}; use salsa::StorageHandle;
use tower_lsp_server::lsp_types; use tower_lsp_server::lsp_types;
use url::Url; use url::Url;
@ -286,13 +286,13 @@ impl Session {
/// This method coordinates both layers: /// This method coordinates both layers:
/// - Layer 1: Stores the document content in buffers /// - Layer 1: Stores the document content in buffers
/// - Layer 2: Creates the SourceFile in Salsa (if path is resolvable) /// - 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); tracing::debug!("Opening document: {}", url);
// Layer 1: Set buffer // Layer 1: Set buffer
self.buffers.open(url.clone(), document); 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 // 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 // invalidate Salsa's cache so it re-reads through the buffer system
if let Some(path) = paths::url_to_path(&url) { if let Some(path) = paths::url_to_path(&url) {
@ -302,16 +302,8 @@ impl Session {
let file = db.get_or_create_file(path.clone()); let file = db.get_or_create_file(path.clone());
if already_exists { if already_exists {
// File was already read - bump revision to invalidate cache // File was already read - touch to invalidate cache
let current_rev = file.revision(db); db.touch_file(&path);
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
);
} else { } else {
// New file - starts at revision 0 // New file - starts at revision 0
tracing::debug!( tracing::debug!(
@ -336,9 +328,9 @@ impl Session {
// Layer 1: Update buffer // Layer 1: Update buffer
self.buffers.update(url.clone(), document); 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) { 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 // We keep the file alive for potential re-opening
if let Some(path) = paths::url_to_path(url) { if let Some(path) = paths::url_to_path(url) {
self.notify_file_changed(&path); self.with_db_mut(|db| db.touch_file(&path));
} }
removed 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 ===== // ===== Safe Query API =====
// These methods encapsulate all Salsa interactions, preventing the // These methods encapsulate all Salsa interactions, preventing the
// "mixed database instance" bug by never exposing SourceFile or Database. // "mixed database instance" bug by never exposing SourceFile or Database.
@ -503,7 +467,7 @@ mod tests {
1, 1,
LanguageId::Other, LanguageId::Other,
); );
session.open_document(url.clone(), document); session.open_document(&url, document);
// Try to read content - this might be where it hangs // Try to read content - this might be where it hangs
println!("**[test]** try to read content - this might be where it hangs"); println!("**[test]** try to read content - this might be where it hangs");

View file

@ -15,6 +15,7 @@ percent-encoding = { workspace = true }
salsa = { workspace = true } salsa = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tower-lsp-server = { workspace = true } tower-lsp-server = { workspace = true }
tracing = { workspace = true }
url = { workspace = true } url = { workspace = true }
[dev-dependencies] [dev-dependencies]

View file

@ -32,6 +32,7 @@ use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use dashmap::DashMap; use dashmap::DashMap;
use salsa::Setter;
use crate::{FileKind, FileSystem}; use crate::{FileKind, FileSystem};
@ -160,6 +161,36 @@ impl Database {
self.files.contains_key(path) 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. /// Get a reference to the storage for handle extraction.
/// ///
/// This is used by `Session` to extract the [`StorageHandle`](salsa::StorageHandle) after mutations. /// This is used by `Session` to extract the [`StorageHandle`](salsa::StorageHandle) after mutations.