diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 3a0df7a..67134b0 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -7,6 +7,8 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +#[cfg(test)] +use std::sync::Mutex; use dashmap::DashMap; use djls_templates::db::Db as TemplateDb; @@ -31,15 +33,49 @@ pub struct DjangoDatabase { files: Arc>, storage: salsa::Storage, + + // The logs are only used for testing and demonstrating reuse: + #[cfg(test)] + #[allow(dead_code)] + logs: Arc>>>, +} + +#[cfg(test)] +impl Default for DjangoDatabase { + fn default() -> Self { + use djls_workspace::InMemoryFileSystem; + + let logs = >>>>::default(); + Self { + fs: Arc::new(InMemoryFileSystem::new()), + files: Arc::new(DashMap::new()), + 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, + } + } } impl DjangoDatabase { /// Create a new [`DjangoDatabase`] with the given file system and file map. pub fn new(file_system: Arc, files: Arc>) -> Self { Self { - storage: salsa::Storage::new(None), fs: file_system, files, + storage: salsa::Storage::new(None), + #[cfg(test)] + logs: Arc::new(Mutex::new(None)), } } diff --git a/crates/djls-workspace/src/db.rs b/crates/djls-workspace/src/db.rs index 743a327..846ab4e 100644 --- a/crates/djls-workspace/src/db.rs +++ b/crates/djls-workspace/src/db.rs @@ -21,13 +21,7 @@ //! ``` use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; -#[cfg(test)] -use std::sync::Mutex; - -use dashmap::DashMap; -use salsa::Setter; use crate::FileKind; use crate::FileSystem; @@ -45,156 +39,6 @@ pub trait Db: salsa::Database { fn read_file_content(&self, path: &Path) -> std::io::Result; } -/// Temporary concrete database for workspace. -/// -/// This will be moved to the server crate in the refactoring. -/// For now, it's kept here to avoid breaking existing code. -#[salsa::db] -#[derive(Clone)] -pub struct Database { - storage: salsa::Storage, - - /// File system for reading file content (checks buffers first, then disk). - fs: Arc, - - /// Maps paths to [`SourceFile`] entities for O(1) lookup. - files: Arc>, - - // The logs are only used for testing and demonstrating reuse: - #[cfg(test)] - #[allow(dead_code)] - logs: Arc>>>, -} - -#[cfg(test)] -impl Default for Database { - fn default() -> Self { - use crate::fs::InMemoryFileSystem; - - let logs = >>>>::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:?}")); - } - } - } - }))), - fs: Arc::new(InMemoryFileSystem::new()), - files: Arc::new(DashMap::new()), - logs, - } - } -} - -impl Database { - pub fn new(file_system: Arc, files: Arc>) -> Self { - Self { - storage: salsa::Storage::new(None), - fs: file_system, - files, - #[cfg(test)] - logs: Arc::new(Mutex::new(None)), - } - } - - /// Read file content through the file system. - pub fn read_file_content(&self, path: &Path) -> std::io::Result { - self.fs.read_to_string(path) - } - - /// Get an existing [`SourceFile`] for the given path without creating it. - /// - /// Returns `Some(SourceFile)` if the file is already tracked, `None` otherwise. - /// This method uses an immutable reference and doesn't modify the database. - pub fn get_file(&self, path: &Path) -> Option { - self.files.get(path).map(|file_ref| *file_ref) - } - - /// Get or create a [`SourceFile`] for the given path. - /// - /// Files are created with an initial revision of 0 and tracked in the [`Database`]'s - /// `DashMap`. The `Arc` ensures cheap cloning while maintaining thread safety. - /// - /// ## Thread Safety - /// - /// This method is inherently thread-safe despite the check-then-create pattern because - /// it requires `&mut self`, ensuring exclusive access to the Database. Only one thread - /// can call this method at a time due to Rust's ownership rules. - pub fn get_or_create_file(&mut self, path: &PathBuf) -> SourceFile { - if let Some(file_ref) = self.files.get(path) { - // Copy the value (SourceFile is Copy) - // The guard drops automatically, no need for explicit drop - return *file_ref; - } - - // File doesn't exist, so we need to create it - let kind = FileKind::from_path(path); - let file = SourceFile::new(self, kind, Arc::from(path.to_string_lossy().as_ref()), 0); - - self.files.insert(path.clone(), file); - file - } - - /// Check if a file is being tracked without creating it. - /// - /// This is primarily used for testing to verify that files have been - /// created without affecting the database state. - pub fn has_file(&self, path: &Path) -> bool { - 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 - ); - } -} - -#[salsa::db] -impl salsa::Database for Database {} - -#[salsa::db] -impl Db for Database { - fn fs(&self) -> Arc { - self.fs.clone() - } - - fn read_file_content(&self, path: &Path) -> std::io::Result { - self.fs.read_to_string(path) - } -} - /// Represents a single file without storing its content. /// /// [`SourceFile`] is a Salsa input entity that tracks a file's path, revision, and diff --git a/crates/djls-workspace/src/fs.rs b/crates/djls-workspace/src/fs.rs index 1c4bdda..6737405 100644 --- a/crates/djls-workspace/src/fs.rs +++ b/crates/djls-workspace/src/fs.rs @@ -3,11 +3,9 @@ //! This module provides the [`FileSystem`] trait that abstracts file I/O operations. //! This allows the LSP to work with both real files and in-memory overlays. -#[cfg(test)] use std::collections::HashMap; use std::io; use std::path::Path; -#[cfg(test)] use std::path::PathBuf; use std::sync::Arc; @@ -19,13 +17,12 @@ pub trait FileSystem: Send + Sync { fn exists(&self, path: &Path) -> bool; } -#[cfg(test)] pub struct InMemoryFileSystem { files: HashMap, } -#[cfg(test)] impl InMemoryFileSystem { + #[must_use] pub fn new() -> Self { Self { files: HashMap::new(), @@ -37,7 +34,12 @@ impl InMemoryFileSystem { } } -#[cfg(test)] +impl Default for InMemoryFileSystem { + fn default() -> Self { + Self::new() + } +} + impl FileSystem for InMemoryFileSystem { fn read_to_string(&self, path: &Path) -> io::Result { self.files @@ -81,7 +83,7 @@ impl FileSystem for OsFileSystem { /// This ensures consistent behavior across all filesystem operations for /// buffered files that may not yet be saved to disk. /// -/// This type is used by the [`Database`](crate::db::Database) to ensure all file reads go +/// This type is used by the database implementations to ensure all file reads go /// through the buffer system first. pub struct WorkspaceFileSystem { /// In-memory buffers that take precedence over disk files diff --git a/crates/djls-workspace/src/lib.rs b/crates/djls-workspace/src/lib.rs index 151277a..08a2424 100644 --- a/crates/djls-workspace/src/lib.rs +++ b/crates/djls-workspace/src/lib.rs @@ -7,7 +7,7 @@ //! # Key Components //! //! - [`Buffers`] - Thread-safe storage for open documents -//! - [`Database`] - Salsa database for incremental computation +//! - [`Db`] - Database trait for file system access (concrete impl in server crate) //! - [`TextDocument`] - LSP document representation with efficient indexing //! - [`FileSystem`] - Abstraction layer for file operations with overlay support //! - [`paths`] - Consistent URL/path conversion utilities @@ -24,12 +24,12 @@ mod workspace; use std::path::Path; pub use buffers::Buffers; -pub use db::Database; pub use db::Db; pub use db::SourceFile; pub use document::TextDocument; pub use encoding::PositionEncoding; pub use fs::FileSystem; +pub use fs::InMemoryFileSystem; pub use fs::OsFileSystem; pub use fs::WorkspaceFileSystem; pub use language::LanguageId;