Skip to content

Commit 01e52a4

Browse files
Simplify file mgmt by plugging workspace file abstraction leak (#235)
1 parent 9b98366 commit 01e52a4

File tree

7 files changed

+210
-163
lines changed

7 files changed

+210
-163
lines changed

crates/djls-server/src/db.rs

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,16 @@ use std::sync::Mutex;
99

1010
use camino::Utf8Path;
1111
use camino::Utf8PathBuf;
12-
use dashmap::DashMap;
1312
use djls_project::Db as ProjectDb;
1413
use djls_project::InspectorPool;
1514
use djls_project::Interpreter;
1615
use djls_project::Project;
1716
use djls_semantic::Db as SemanticDb;
1817
use djls_semantic::TagSpecs;
1918
use djls_source::Db as SourceDb;
20-
use djls_source::File;
2119
use djls_templates::db::Db as TemplateDb;
2220
use djls_workspace::db::Db as WorkspaceDb;
2321
use djls_workspace::FileSystem;
24-
use salsa::Setter;
2522

2623
/// Concrete Salsa database for the Django Language Server.
2724
///
@@ -35,9 +32,6 @@ pub struct DjangoDatabase {
3532
/// File system for reading file content (checks buffers first, then disk).
3633
fs: Arc<dyn FileSystem>,
3734

38-
/// Maps paths to [`SourceFile`] entities for O(1) lookup.
39-
files: Arc<DashMap<Utf8PathBuf, File>>,
40-
4135
/// The single project for this database instance
4236
project: Arc<Mutex<Option<Project>>>,
4337

@@ -60,7 +54,6 @@ impl Default for DjangoDatabase {
6054
let logs = <Arc<Mutex<Option<Vec<String>>>>>::default();
6155
Self {
6256
fs: Arc::new(InMemoryFileSystem::new()),
63-
files: Arc::new(DashMap::new()),
6457
project: Arc::new(Mutex::new(None)),
6558
inspector_pool: Arc::new(InspectorPool::new()),
6659
storage: salsa::Storage::new(Some(Box::new({
@@ -95,64 +88,17 @@ impl DjangoDatabase {
9588

9689
*self.project.lock().unwrap() = Some(project);
9790
}
98-
/// Create a new [`DjangoDatabase`] with the given file system and file map.
99-
pub fn new(file_system: Arc<dyn FileSystem>, files: Arc<DashMap<Utf8PathBuf, File>>) -> Self {
91+
/// Create a new [`DjangoDatabase`] with the given file system handle.
92+
pub fn new(file_system: Arc<dyn FileSystem>) -> Self {
10093
Self {
10194
fs: file_system,
102-
files,
10395
project: Arc::new(Mutex::new(None)),
10496
inspector_pool: Arc::new(InspectorPool::new()),
10597
storage: salsa::Storage::new(None),
10698
#[cfg(test)]
10799
logs: Arc::new(Mutex::new(None)),
108100
}
109101
}
110-
111-
/// Get an existing [`SourceFile`] for the given path without creating it.
112-
///
113-
/// Returns `Some(SourceFile)` if the file is already tracked, `None` otherwise.
114-
pub fn get_file(&self, path: &Utf8Path) -> Option<File> {
115-
self.files.get(path).map(|file_ref| *file_ref)
116-
}
117-
118-
/// Get or create a [`SourceFile`] for the given path.
119-
///
120-
/// Files are created with an initial revision of 0 and tracked in the database's
121-
/// `DashMap`. The `Arc` ensures cheap cloning while maintaining thread safety.
122-
pub fn get_or_create_file(&mut self, path: &Utf8PathBuf) -> File {
123-
if let Some(file_ref) = self.files.get(path) {
124-
return *file_ref;
125-
}
126-
127-
let file = File::new(self, path.clone(), 0);
128-
129-
self.files.insert(path.clone(), file);
130-
file
131-
}
132-
133-
/// Check if a file is being tracked without creating it.
134-
pub fn has_file(&self, path: &Utf8Path) -> bool {
135-
self.files.contains_key(path)
136-
}
137-
138-
/// Touch a file to mark it as modified, triggering re-evaluation of dependent queries.
139-
///
140-
/// Updates the file's revision number to signal that cached query results
141-
/// depending on this file should be invalidated.
142-
pub fn touch_file(&mut self, path: &Utf8Path) {
143-
let Some(file_ref) = self.files.get(path) else {
144-
tracing::debug!("File {} not tracked, skipping touch", path);
145-
return;
146-
};
147-
let file = *file_ref;
148-
drop(file_ref); // Explicitly drop to release the lock
149-
150-
let current_rev = file.revision(self);
151-
let new_rev = current_rev + 1;
152-
file.set_revision(self).to(new_rev);
153-
154-
tracing::debug!("Touched {}: revision {} -> {}", path, current_rev, new_rev);
155-
}
156102
}
157103

158104
#[salsa::db]

crates/djls-server/src/server.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::future::Future;
22
use std::sync::Arc;
33

4+
use camino::Utf8PathBuf;
45
use djls_project::Db as ProjectDb;
56
use djls_semantic::Db as SemanticDb;
67
use djls_source::FileKind;
@@ -406,13 +407,11 @@ impl LanguageServer for DjangoLanguageServer {
406407
}
407408

408409
// Get diagnostics from the database
410+
let path: Utf8PathBuf = url.path().into();
409411
let diagnostics: Vec<lsp_types::Diagnostic> = self
410-
.with_session(|session| {
412+
.with_session_mut(|session| {
413+
let file = session.get_or_create_file(&path);
411414
session.with_db(|db| {
412-
let Some(file) = db.get_file(url.path().into()) else {
413-
return vec![];
414-
};
415-
416415
let nodelist = djls_templates::parse_template(db, file);
417416
djls_ide::collect_diagnostics(db, file, nodelist)
418417
})

crates/djls-server/src/session.rs

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
//! This module implements the LSP session abstraction that manages project-specific
44
//! state and the Salsa database for incremental computation.
55
6-
use std::sync::Arc;
7-
86
use camino::Utf8PathBuf;
9-
use dashmap::DashMap;
107
use djls_conf::Settings;
118
use djls_project::Db as ProjectDb;
129
use djls_project::Interpreter;
@@ -16,6 +13,7 @@ use djls_workspace::paths;
1613
use djls_workspace::PositionEncoding;
1714
use djls_workspace::TextDocument;
1815
use djls_workspace::Workspace;
16+
use djls_workspace::WorkspaceFileEvent;
1917
use salsa::Setter;
2018
use tower_lsp_server::lsp_types;
2119
use url::Url;
@@ -74,9 +72,7 @@ impl Session {
7472
};
7573

7674
let workspace = Workspace::new();
77-
78-
let files = Arc::new(DashMap::new());
79-
let mut db = DjangoDatabase::new(workspace.file_system(), files);
75+
let mut db = DjangoDatabase::new(workspace.file_system());
8076

8177
if let Some(root_path) = &project_path {
8278
db.set_project(root_path);
@@ -160,24 +156,8 @@ impl Session {
160156
/// the database or invalidates it if it already exists.
161157
/// For template files, immediately triggers parsing and validation.
162158
pub fn open_document(&mut self, url: &Url, document: TextDocument) {
163-
// Add to workspace buffers
164-
self.workspace.open_document(url, document);
165-
166-
if let Some(path) = paths::url_to_path(url) {
167-
let already_exists = self.db.has_file(&path);
168-
let file = self.db.get_or_create_file(&path);
169-
170-
if already_exists {
171-
// File was already read - touch to invalidate cache
172-
self.db.touch_file(&path);
173-
}
174-
175-
if FileKind::from_path(&path) == FileKind::Template {
176-
let nodelist = djls_templates::parse_template(&self.db, file);
177-
if let Some(nodelist) = nodelist {
178-
djls_semantic::validate_nodelist(&self.db, nodelist);
179-
}
180-
}
159+
if let Some(event) = self.workspace.open_document(&mut self.db, url, document) {
160+
self.handle_file_event(&event);
181161
}
182162
}
183163

@@ -191,39 +171,20 @@ impl Session {
191171
changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
192172
version: i32,
193173
) {
194-
self.workspace
195-
.update_document(url, changes, version, self.position_encoding);
196-
197-
if let Some(path) = paths::url_to_path(url) {
198-
if self.db.has_file(&path) {
199-
// Touch file in database to trigger invalidation
200-
self.db.touch_file(&path);
201-
202-
if FileKind::from_path(&path) == FileKind::Template {
203-
let file = self.db.get_or_create_file(&path);
204-
let nodelist = djls_templates::parse_template(&self.db, file);
205-
if let Some(nodelist) = nodelist {
206-
djls_semantic::validate_nodelist(&self.db, nodelist);
207-
}
208-
}
209-
}
174+
if let Some(event) = self.workspace.update_document(
175+
&mut self.db,
176+
url,
177+
changes,
178+
version,
179+
self.position_encoding,
180+
) {
181+
self.handle_file_event(&event);
210182
}
211183
}
212184

213185
pub fn save_document(&mut self, url: &Url) {
214-
if let Some(path) = paths::url_to_path(url) {
215-
if self.db.has_file(&path) {
216-
// Touch file in database to trigger invalidation
217-
self.db.touch_file(&path);
218-
219-
if FileKind::from_path(&path) == FileKind::Template {
220-
let file = self.db.get_or_create_file(&path);
221-
let nodelist = djls_templates::parse_template(&self.db, file);
222-
if let Some(nodelist) = nodelist {
223-
djls_semantic::validate_nodelist(&self.db, nodelist);
224-
}
225-
}
226-
}
186+
if let Some(event) = self.workspace.save_document(&mut self.db, url) {
187+
self.handle_file_event(&event);
227188
}
228189
}
229190

@@ -232,16 +193,7 @@ impl Session {
232193
/// Removes from workspace buffers and triggers database invalidation to fall back to disk.
233194
/// For template files, immediately re-parses from disk.
234195
pub fn close_document(&mut self, url: &Url) -> Option<TextDocument> {
235-
let document = self.workspace.close_document(url);
236-
237-
if let Some(path) = paths::url_to_path(url) {
238-
if self.db.has_file(&path) {
239-
// Touch file in database to trigger re-read from disk
240-
self.db.touch_file(&path);
241-
}
242-
}
243-
244-
document
196+
self.workspace.close_document(&mut self.db, url)
245197
}
246198

247199
/// Get a document from the buffer if it's open.
@@ -252,7 +204,18 @@ impl Session {
252204

253205
/// Get or create a file in the database.
254206
pub fn get_or_create_file(&mut self, path: &Utf8PathBuf) -> File {
255-
self.db.get_or_create_file(path)
207+
self.workspace
208+
.track_file(&mut self.db, path.as_path())
209+
.file()
210+
}
211+
212+
fn handle_file_event(&self, event: &WorkspaceFileEvent) {
213+
if FileKind::from_path(event.path()) == FileKind::Template {
214+
let nodelist = djls_templates::parse_template(&self.db, event.file());
215+
if let Some(nodelist) = nodelist {
216+
djls_semantic::validate_nodelist(&self.db, nodelist);
217+
}
218+
}
256219
}
257220

258221
/// Check if the client supports pull diagnostics.

crates/djls-workspace/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.0.0"
44
edition = "2021"
55

66
[dependencies]
7-
djls-source = { workspace = true}
7+
djls-source = { workspace = true }
88

99
anyhow = { workspace = true }
1010
camino = { workspace = true }

crates/djls-workspace/src/db.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,29 @@
2222
2323
use std::sync::Arc;
2424

25+
use djls_source::Db as SourceDb;
26+
use djls_source::File;
27+
use salsa::Setter;
28+
2529
use crate::FileSystem;
2630

2731
/// Base database trait that provides file system access for Salsa queries
2832
#[salsa::db]
29-
pub trait Db: salsa::Database {
33+
pub trait Db: SourceDb {
3034
/// Get the file system for reading files.
3135
fn fs(&self) -> Arc<dyn FileSystem>;
36+
37+
/// Bump the revision for a tracked file to invalidate dependent queries.
38+
fn touch_file(&mut self, file: File) {
39+
let current_rev = file.revision(self);
40+
let new_rev = current_rev + 1;
41+
file.set_revision(self).to(new_rev);
42+
43+
tracing::debug!(
44+
"Touched {}: revision {} -> {}",
45+
file.path(self),
46+
current_rev,
47+
new_rev
48+
);
49+
}
3250
}

crates/djls-workspace/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ pub use fs::OsFileSystem;
3131
pub use fs::WorkspaceFileSystem;
3232
pub use language::LanguageId;
3333
pub use workspace::Workspace;
34+
pub use workspace::WorkspaceFileEvent;

0 commit comments

Comments
 (0)