diff --git a/Cargo.lock b/Cargo.lock index 88449d4..2cc7ff2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,6 +459,7 @@ name = "djls-project" version = "0.0.0" dependencies = [ "djls-dev", + "djls-workspace", "pyo3", "salsa", "tempfile", @@ -513,7 +514,6 @@ dependencies = [ "anyhow", "camino", "dashmap", - "djls-project", "notify", "percent-encoding", "salsa", diff --git a/crates/djls-project/Cargo.toml b/crates/djls-project/Cargo.toml index bdde816..c5f9485 100644 --- a/crates/djls-project/Cargo.toml +++ b/crates/djls-project/Cargo.toml @@ -8,6 +8,8 @@ extension-module = [] default = [] [dependencies] +djls-workspace = { workspace = true } + pyo3 = { workspace = true } salsa = { workspace = true } which = { workspace = true} diff --git a/crates/djls-project/src/db.rs b/crates/djls-project/src/db.rs index 94fc21c..fbfc417 100644 --- a/crates/djls-project/src/db.rs +++ b/crates/djls-project/src/db.rs @@ -1,31 +1,31 @@ -use crate::meta::ProjectMetadata; +//! Project-specific database trait and queries. +//! +//! This module extends the workspace database trait with project-specific +//! functionality including metadata access and Python environment discovery. +use djls_workspace::Db as WorkspaceDb; + +use crate::meta::ProjectMetadata; +use crate::python::PythonEnvironment; + +/// Project-specific database trait extending the workspace database #[salsa::db] -pub trait Db: salsa::Database { +pub trait Db: WorkspaceDb { + /// Get the project metadata containing root path and venv configuration fn metadata(&self) -> &ProjectMetadata; } -#[salsa::db] -#[derive(Clone)] -pub struct ProjectDatabase { - storage: salsa::Storage, - metadata: ProjectMetadata, +/// Find the Python environment for the project. +/// +/// This Salsa tracked function discovers the Python environment based on: +/// 1. Explicit venv path from metadata +/// 2. VIRTUAL_ENV environment variable +/// 3. Common venv directories in project root (.venv, venv, env, .env) +/// 4. System Python as fallback +#[salsa::tracked] +pub fn find_python_environment(db: &dyn Db) -> Option { + let project_path = db.metadata().root().as_path(); + let venv_path = db.metadata().venv().and_then(|p| p.to_str()); + + PythonEnvironment::new(project_path, venv_path) } - -impl ProjectDatabase { - pub fn new(metadata: ProjectMetadata) -> Self { - let storage = salsa::Storage::new(None); - - Self { storage, metadata } - } -} - -#[salsa::db] -impl Db for ProjectDatabase { - fn metadata(&self) -> &ProjectMetadata { - &self.metadata - } -} - -#[salsa::db] -impl salsa::Database for ProjectDatabase {} diff --git a/crates/djls-project/src/lib.rs b/crates/djls-project/src/lib.rs index 1fb0ff4..685cd10 100644 --- a/crates/djls-project/src/lib.rs +++ b/crates/djls-project/src/lib.rs @@ -8,11 +8,11 @@ use std::fmt; use std::path::Path; use std::path::PathBuf; -use db::ProjectDatabase; -use meta::ProjectMetadata; +pub use db::find_python_environment; +pub use db::Db; +pub use meta::ProjectMetadata; use pyo3::prelude::*; -use python::find_python_environment; -use python::PythonEnvironment; +pub use python::PythonEnvironment; pub use templatetags::TemplateTags; #[derive(Debug)] @@ -32,11 +32,9 @@ impl DjangoProject { } } - pub fn initialize(&mut self, venv_path: Option<&str>) -> PyResult<()> { - let venv_pathbuf = venv_path.map(PathBuf::from); - let metadata = ProjectMetadata::new(self.path.clone(), venv_pathbuf); - let db = ProjectDatabase::new(metadata); - self.env = find_python_environment(&db); + pub fn initialize(&mut self, db: &dyn Db) -> PyResult<()> { + // Use the database to find the Python environment + self.env = find_python_environment(db); if self.env.is_none() { return Err(PyErr::new::( "Could not find Python environment", diff --git a/crates/djls-project/src/meta.rs b/crates/djls-project/src/meta.rs index 14636d1..c041809 100644 --- a/crates/djls-project/src/meta.rs +++ b/crates/djls-project/src/meta.rs @@ -7,14 +7,17 @@ pub struct ProjectMetadata { } impl ProjectMetadata { + #[must_use] pub fn new(root: PathBuf, venv: Option) -> Self { ProjectMetadata { root, venv } } + #[must_use] pub fn root(&self) -> &PathBuf { &self.root } + #[must_use] pub fn venv(&self) -> Option<&PathBuf> { self.venv.as_ref() } diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 6935845..66ec683 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -4,17 +4,8 @@ use std::path::PathBuf; use pyo3::prelude::*; -use crate::db::Db; use crate::system; -#[salsa::tracked] -pub fn find_python_environment(db: &dyn Db) -> Option { - let project_path = db.metadata().root().as_path(); - let venv_path = db.metadata().venv().and_then(|p| p.to_str()); - - PythonEnvironment::new(project_path, venv_path) -} - #[derive(Clone, Debug, PartialEq)] pub struct PythonEnvironment { python_path: PathBuf, @@ -23,7 +14,8 @@ pub struct PythonEnvironment { } impl PythonEnvironment { - fn new(project_path: &Path, venv_path: Option<&str>) -> Option { + #[must_use] + pub fn new(project_path: &Path, venv_path: Option<&str>) -> Option { if let Some(path) = venv_path { let prefix = PathBuf::from(path); if let Some(env) = Self::from_venv_prefix(&prefix) { @@ -703,12 +695,57 @@ mod tests { } } - // Add tests for the salsa tracked function mod salsa_integration { + use std::sync::Arc; + + use djls_workspace::FileSystem; + use djls_workspace::InMemoryFileSystem; + use super::*; - use crate::db::ProjectDatabase; + use crate::db::find_python_environment; + use crate::db::Db as ProjectDb; use crate::meta::ProjectMetadata; + /// Test implementation of ProjectDb for unit tests + #[salsa::db] + #[derive(Clone)] + struct TestDatabase { + storage: salsa::Storage, + metadata: ProjectMetadata, + fs: Arc, + } + + impl TestDatabase { + fn new(metadata: ProjectMetadata) -> Self { + Self { + storage: salsa::Storage::new(None), + metadata, + fs: Arc::new(InMemoryFileSystem::new()), + } + } + } + + #[salsa::db] + impl salsa::Database for TestDatabase {} + + #[salsa::db] + impl djls_workspace::Db for TestDatabase { + fn fs(&self) -> Arc { + self.fs.clone() + } + + fn read_file_content(&self, path: &std::path::Path) -> std::io::Result { + self.fs.read_to_string(path) + } + } + + #[salsa::db] + impl ProjectDb for TestDatabase { + fn metadata(&self) -> &ProjectMetadata { + &self.metadata + } + } + #[test] fn test_find_python_environment_with_salsa_db() { let project_dir = tempdir().unwrap(); @@ -721,8 +758,8 @@ mod tests { let metadata = ProjectMetadata::new(project_dir.path().to_path_buf(), Some(venv_prefix.clone())); - // Create a ProjectDatabase with the metadata - let db = ProjectDatabase::new(metadata); + // Create a TestDatabase with the metadata + let db = TestDatabase::new(metadata); // Call the tracked function let env = find_python_environment(&db); @@ -756,8 +793,8 @@ mod tests { // Create a metadata instance with project path but no explicit venv path let metadata = ProjectMetadata::new(project_dir.path().to_path_buf(), None); - // Create a ProjectDatabase with the metadata - let db = ProjectDatabase::new(metadata); + // Create a TestDatabase with the metadata + let db = TestDatabase::new(metadata); // Mock to ensure VIRTUAL_ENV is not set let _guard = system::mock::MockGuard; diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 67134b0..0690325 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -1,8 +1,8 @@ //! Concrete Salsa database implementation for the Django Language Server. //! //! This module provides the concrete [`DjangoDatabase`] that implements all -//! the database traits from workspace and template crates. This follows Ruff's -//! architecture pattern where the concrete database lives at the top level. +//! the database traits from workspace, template, and project crates. This follows +//! Ruff's architecture pattern where the concrete database lives at the top level. use std::path::Path; use std::path::PathBuf; @@ -11,6 +11,8 @@ use std::sync::Arc; use std::sync::Mutex; use dashmap::DashMap; +use djls_project::Db as ProjectDb; +use djls_project::ProjectMetadata; use djls_templates::db::Db as TemplateDb; use djls_workspace::db::Db as WorkspaceDb; use djls_workspace::db::SourceFile; @@ -23,6 +25,7 @@ use salsa::Setter; /// This database implements all the traits from various crates: /// - [`WorkspaceDb`] for file system access and core operations /// - [`TemplateDb`] for template parsing and diagnostics +/// - [`ProjectDb`] for project metadata and Python environment #[salsa::db] #[derive(Clone)] pub struct DjangoDatabase { @@ -32,6 +35,9 @@ pub struct DjangoDatabase { /// Maps paths to [`SourceFile`] entities for O(1) lookup. files: Arc>, + /// Project metadata containing root path and venv configuration. + metadata: ProjectMetadata, + storage: salsa::Storage, // The logs are only used for testing and demonstrating reuse: @@ -49,6 +55,7 @@ impl Default for DjangoDatabase { Self { fs: Arc::new(InMemoryFileSystem::new()), files: Arc::new(DashMap::new()), + metadata: ProjectMetadata::new(PathBuf::from("/test"), None), storage: salsa::Storage::new(Some(Box::new({ let logs = logs.clone(); move |event| { @@ -68,11 +75,16 @@ impl Default for DjangoDatabase { } impl DjangoDatabase { - /// Create a new [`DjangoDatabase`] with the given file system and file map. - pub fn new(file_system: Arc, files: Arc>) -> Self { + /// Create a new [`DjangoDatabase`] with the given file system, file map, and project metadata. + pub fn new( + file_system: Arc, + files: Arc>, + metadata: ProjectMetadata, + ) -> Self { Self { fs: file_system, files, + metadata, storage: salsa::Storage::new(None), #[cfg(test)] logs: Arc::new(Mutex::new(None)), @@ -149,3 +161,10 @@ impl WorkspaceDb for DjangoDatabase { #[salsa::db] impl TemplateDb for DjangoDatabase {} + +#[salsa::db] +impl ProjectDb for DjangoDatabase { + fn metadata(&self) -> &ProjectMetadata { + &self.metadata + } +} diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 8be4dd0..6c22ccd 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -149,12 +149,7 @@ impl LanguageServer for DjangoLanguageServer { let init_result = { let mut session_lock = session_arc.lock().await; - if let Some(project) = session_lock.project_mut().as_mut() { - project.initialize(venv_path.as_deref()) - } else { - // Project was removed between read and write locks - Ok(()) - } + session_lock.initialize_project() }; match init_result { diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index abbbdb3..3d84b0f 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -9,11 +9,13 @@ use std::sync::Arc; use dashmap::DashMap; use djls_conf::Settings; use djls_project::DjangoProject; +use djls_project::ProjectMetadata; use djls_workspace::db::SourceFile; use djls_workspace::paths; use djls_workspace::PositionEncoding; use djls_workspace::TextDocument; use djls_workspace::Workspace; +use pyo3::PyResult; use tower_lsp_server::lsp_types; use url::Url; @@ -63,23 +65,29 @@ impl Session { std::env::current_dir().ok() }); - let (project, settings) = if let Some(path) = &project_path { + let (project, settings, metadata) = if let Some(path) = &project_path { let settings = djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default()); let project = Some(djls_project::DjangoProject::new(path.clone())); - (project, settings) + // Create metadata for the project with venv path from settings + let venv_path = settings.venv_path().map(PathBuf::from); + let metadata = ProjectMetadata::new(path.clone(), venv_path); + + (project, settings, metadata) } else { - (None, Settings::default()) + // Default metadata for when there's no project path + let metadata = ProjectMetadata::new(PathBuf::from("."), None); + (None, Settings::default(), metadata) }; // Create workspace for buffer management let workspace = Workspace::new(); - // Create the concrete database with the workspace's file system + // Create the concrete database with the workspace's file system and metadata let files = Arc::new(DashMap::new()); - let db = DjangoDatabase::new(workspace.file_system(), files); + let db = DjangoDatabase::new(workspace.file_system(), files, metadata); Self { db, @@ -130,6 +138,20 @@ impl Session { f(&mut self.db) } + /// Get a reference to the database for project operations. + pub fn database(&self) -> &DjangoDatabase { + &self.db + } + + /// Initialize the project with the database. + pub fn initialize_project(&mut self) -> PyResult<()> { + if let Some(project) = self.project.as_mut() { + project.initialize(&self.db) + } else { + Ok(()) + } + } + /// Open a document in the session. /// /// Updates both the workspace buffers and database. Creates the file in diff --git a/crates/djls-workspace/Cargo.toml b/crates/djls-workspace/Cargo.toml index 235118c..a9c8bfe 100644 --- a/crates/djls-workspace/Cargo.toml +++ b/crates/djls-workspace/Cargo.toml @@ -4,8 +4,6 @@ version = "0.0.0" edition = "2021" [dependencies] -djls-project = { workspace = true } - anyhow = { workspace = true } camino = { workspace = true } dashmap = { workspace = true }