From 6d6d47862b030376cc803fdc59aff59e8c634738 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 15:02:47 -0500 Subject: [PATCH 01/17] wip --- crates/djls-conf/src/lib.rs | 2 +- crates/djls-project/src/db.rs | 31 +-- crates/djls-project/src/django.rs | 74 +++++++ .../src/{ => django}/templatetags.rs | 51 ++++- crates/djls-project/src/inspector.rs | 62 +++++- crates/djls-project/src/inspector/pool.rs | 6 - crates/djls-project/src/inspector/queries.rs | 26 +++ crates/djls-project/src/lib.rs | 151 +------------ crates/djls-project/src/meta.rs | 22 ++ crates/djls-project/src/python.rs | 146 ++++++++++++- crates/djls-project/src/system.rs | 4 +- crates/djls-server/src/db.rs | 39 ++++ crates/djls-server/src/server.rs | 33 +-- crates/djls-server/src/session.rs | 205 ++++++++++++++++-- 14 files changed, 638 insertions(+), 214 deletions(-) create mode 100644 crates/djls-project/src/django.rs rename crates/djls-project/src/{ => django}/templatetags.rs (56%) diff --git a/crates/djls-conf/src/lib.rs b/crates/djls-conf/src/lib.rs index 2b909ea2..65e2b691 100644 --- a/crates/djls-conf/src/lib.rs +++ b/crates/djls-conf/src/lib.rs @@ -21,7 +21,7 @@ pub enum ConfigError { PyprojectSerialize(#[from] toml::ser::Error), } -#[derive(Debug, Deserialize, Default, PartialEq)] +#[derive(Debug, Deserialize, Default, PartialEq, Clone)] pub struct Settings { #[serde(default)] debug: bool, diff --git a/crates/djls-project/src/db.rs b/crates/djls-project/src/db.rs index fbfc4170..6a7cd916 100644 --- a/crates/djls-project/src/db.rs +++ b/crates/djls-project/src/db.rs @@ -2,30 +2,33 @@ //! //! This module extends the workspace database trait with project-specific //! functionality including metadata access and Python environment discovery. +//! +//! ## Architecture +//! +//! Following the Salsa pattern established in workspace and templates crates: +//! - `DjangoProject` is a Salsa input representing external project state +//! - Tracked functions compute derived values (Python env, Django config) +//! - Database trait provides stable configuration (metadata, template tags) + +use std::path::Path; +use std::sync::Arc; use djls_workspace::Db as WorkspaceDb; +use crate::django::TemplateTags; +use crate::meta::Project; use crate::meta::ProjectMetadata; -use crate::python::PythonEnvironment; /// Project-specific database trait extending the workspace database #[salsa::db] pub trait Db: WorkspaceDb { /// Get the project metadata containing root path and venv configuration fn metadata(&self) -> &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()); + /// Get discovered template tags for the project (if available). + /// This is populated by the LSP server after querying Django. + fn template_tags(&self) -> Option>; - PythonEnvironment::new(project_path, venv_path) + /// Get or create a Project input for a given path + fn project(&self, root: &Path) -> Project; } diff --git a/crates/djls-project/src/django.rs b/crates/djls-project/src/django.rs new file mode 100644 index 00000000..61093016 --- /dev/null +++ b/crates/djls-project/src/django.rs @@ -0,0 +1,74 @@ +mod templatetags; + +use std::path::Path; + +use crate::db::Db as ProjectDb; +use crate::inspector::inspector_run; +use crate::inspector::queries::InspectorQueryKind; +use crate::meta::Project; +use crate::python::python_environment; + +pub use templatetags::template_tags; +pub use templatetags::TemplateTags; + +/// Check if Django is available for a project. +/// +/// This determines if Django is installed and configured in the Python environment. +/// First consults the inspector, then falls back to environment detection. +#[salsa::tracked] +pub fn django_available(db: &dyn ProjectDb, project: Project) -> bool { + // First try to get Django availability from inspector + if let Some(json_data) = inspector_run(db, project, InspectorQueryKind::DjangoAvailable) { + // Parse the JSON response - expect a boolean + if let Ok(available) = serde_json::from_str::(&json_data) { + return available; + } + } + + // Fallback to environment detection + python_environment(db, project).is_some() +} + +/// Get the Django settings module name for a project. +/// +/// Returns the settings_module_override from project, or inspector result, +/// or DJANGO_SETTINGS_MODULE env var, or attempts to detect it. +#[salsa::tracked] +pub fn django_settings_module(db: &dyn ProjectDb, project: Project) -> Option { + let _ = project.revision(db); + let project_path = Path::new(project.root(db)); + + // Check project override first + if let Some(settings) = project.settings_module(db) { + return Some(settings.clone()); + } + + // Try to get settings module from inspector + if let Some(json_data) = inspector_run(db, project, InspectorQueryKind::SettingsModule) { + // Parse the JSON response - expect a string + if let Ok(settings) = serde_json::from_str::(&json_data) { + return Some(settings); + } + } + + // Try to detect settings module + if project_path.join("manage.py").exists() { + // Look for common settings modules + for candidate in &["settings", "config.settings", "project.settings"] { + let parts: Vec<&str> = candidate.split('.').collect(); + let mut path = project_path.to_path_buf(); + for part in &parts[..parts.len() - 1] { + path = path.join(part); + } + if let Some(last) = parts.last() { + path = path.join(format!("{last}.py")); + } + + if path.exists() { + return Some((*candidate).to_string()); + } + } + } + + None +} diff --git a/crates/djls-project/src/templatetags.rs b/crates/djls-project/src/django/templatetags.rs similarity index 56% rename from crates/djls-project/src/templatetags.rs rename to crates/djls-project/src/django/templatetags.rs index 237a4544..a39bcd1a 100644 --- a/crates/djls-project/src/templatetags.rs +++ b/crates/djls-project/src/django/templatetags.rs @@ -4,7 +4,30 @@ use anyhow::Context; use anyhow::Result; use serde_json::Value; -#[derive(Debug, Default, Clone)] +use crate::db::Db as ProjectDb; +use crate::inspector::inspector_run; +use crate::inspector::queries::InspectorQueryKind; +use crate::meta::Project; + +/// Get template tags for a project by querying the inspector. +/// +/// This tracked function calls the inspector to retrieve Django template tags +/// and parses the JSON response into a TemplateTags struct. +#[salsa::tracked] +pub fn template_tags(db: &dyn ProjectDb, project: Project) -> Option { + let json_str = inspector_run(db, project, InspectorQueryKind::TemplateTags)?; + + // Parse the JSON string into a Value first + let json_value: serde_json::Value = match serde_json::from_str(&json_str) { + Ok(value) => value, + Err(_) => return None, + }; + + // Parse the JSON data into TemplateTags + TemplateTags::from_json(&json_value).ok() +} + +#[derive(Debug, Default, Clone, PartialEq)] pub struct TemplateTags(Vec); impl Deref for TemplateTags { @@ -83,3 +106,29 @@ impl TemplateTag { self.doc.as_ref() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_template_tags_parsing() { + // Test that TemplateTags can parse valid JSON + let json_data = r#"{ + "templatetags": [ + { + "name": "test_tag", + "module": "test_module", + "doc": "Test documentation" + } + ] + }"#; + + let value: serde_json::Value = serde_json::from_str(json_data).unwrap(); + let tags = TemplateTags::from_json(&value).unwrap(); + assert_eq!(tags.len(), 1); + assert_eq!(tags[0].name(), "test_tag"); + assert_eq!(tags[0].library(), "test_module"); + assert_eq!(tags[0].doc(), Some(&"Test documentation".to_string())); + } +} diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index 2497453f..36b74ed9 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -3,10 +3,18 @@ pub mod pool; pub mod queries; mod zipapp; -pub use queries::Query; +use std::path::Path; + use serde::Deserialize; use serde::Serialize; +use crate::db::Db as ProjectDb; +use crate::meta::Project; +use crate::python::python_environment; +use crate::python::resolve_interpreter; +use queries::InspectorQueryKind; +pub use queries::Query; + #[derive(Serialize)] pub struct DjlsRequest { #[serde(flatten)] @@ -19,3 +27,55 @@ pub struct DjlsResponse { pub data: Option, pub error: Option, } + +/// Run an inspector query and return the JSON result as a string. +/// +/// This tracked function executes inspector queries through a temporary pool +/// and caches the results based on project state and query kind. +#[allow(clippy::drop_non_drop)] +#[salsa::tracked] +pub fn inspector_run( + db: &dyn ProjectDb, + project: Project, + kind: InspectorQueryKind, +) -> Option { + // Create dependency on project revision + let _ = project.revision(db); + + // Get interpreter path - required for inspector + let _interpreter_path = resolve_interpreter(db, project)?; + let project_path = Path::new(project.root(db)); + + // Get Python environment for inspector + let python_env = python_environment(db, project)?; + + // Create the appropriate query based on kind + let query = match kind { + InspectorQueryKind::TemplateTags => crate::inspector::Query::Templatetags, + InspectorQueryKind::DjangoAvailable | InspectorQueryKind::SettingsModule => { + crate::inspector::Query::DjangoInit + } + }; + + let request = crate::inspector::DjlsRequest { query }; + + // Create a temporary inspector pool for this query + // Note: In production, this could be optimized with a shared pool + let pool = crate::inspector::pool::InspectorPool::new(); + + match pool.query(&python_env, project_path, &request) { + Ok(response) => { + if response.ok { + if let Some(data) = response.data { + // Convert to JSON string + serde_json::to_string(&data).ok() + } else { + None + } + } else { + None + } + } + Err(_) => None, + } +} diff --git a/crates/djls-project/src/inspector/pool.rs b/crates/djls-project/src/inspector/pool.rs index 1db929aa..b5a153d2 100644 --- a/crates/djls-project/src/inspector/pool.rs +++ b/crates/djls-project/src/inspector/pool.rs @@ -11,12 +11,6 @@ use super::DjlsRequest; use super::DjlsResponse; use crate::python::PythonEnvironment; -/// Global singleton pool for convenience -static GLOBAL_POOL: std::sync::OnceLock = std::sync::OnceLock::new(); - -pub fn global_pool() -> &'static InspectorPool { - GLOBAL_POOL.get_or_init(InspectorPool::new) -} const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60); /// Manages a pool of inspector processes with automatic cleanup diff --git a/crates/djls-project/src/inspector/queries.rs b/crates/djls-project/src/inspector/queries.rs index 12b68aef..6fb38420 100644 --- a/crates/djls-project/src/inspector/queries.rs +++ b/crates/djls-project/src/inspector/queries.rs @@ -12,6 +12,14 @@ pub enum Query { DjangoInit, } +/// Enum representing different kinds of inspector queries for Salsa tracking +#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] +pub enum InspectorQueryKind { + TemplateTags, + DjangoAvailable, + SettingsModule, +} + #[derive(Serialize, Deserialize)] pub struct PythonEnvironmentQueryData { pub sys_base_prefix: PathBuf, @@ -42,3 +50,21 @@ pub struct TemplateTag { pub module: String, pub doc: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inspector_query_kind_enum() { + // Test that InspectorQueryKind variants exist and are copyable + let template_tags = InspectorQueryKind::TemplateTags; + let django_available = InspectorQueryKind::DjangoAvailable; + let settings_module = InspectorQueryKind::SettingsModule; + + // Test that they can be copied + assert_eq!(template_tags, InspectorQueryKind::TemplateTags); + assert_eq!(django_available, InspectorQueryKind::DjangoAvailable); + assert_eq!(settings_module, InspectorQueryKind::SettingsModule); + } +} diff --git a/crates/djls-project/src/lib.rs b/crates/djls-project/src/lib.rs index 6474d179..483cc734 100644 --- a/crates/djls-project/src/lib.rs +++ b/crates/djls-project/src/lib.rs @@ -1,149 +1,20 @@ mod db; +mod django; pub mod inspector; mod meta; pub mod python; mod system; -mod templatetags; -use std::fmt; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Context; -use anyhow::Result; -pub use db::find_python_environment; pub use db::Db; -use inspector::pool::InspectorPool; -use inspector::DjlsRequest; -use inspector::Query; +pub use django::django_available; +pub use django::django_settings_module; +pub use django::template_tags; +pub use django::TemplateTags; +pub use inspector::inspector_run; +pub use inspector::queries::InspectorQueryKind; +pub use meta::Project; pub use meta::ProjectMetadata; +pub use python::python_environment; +pub use python::resolve_interpreter; +pub use python::Interpreter; pub use python::PythonEnvironment; -pub use templatetags::TemplateTags; - -#[derive(Debug)] -pub struct DjangoProject { - path: PathBuf, - env: Option, - template_tags: Option, - inspector: Arc, -} - -impl DjangoProject { - #[must_use] - pub fn new(path: PathBuf) -> Self { - Self { - path, - env: None, - template_tags: None, - inspector: Arc::new(InspectorPool::new()), - } - } - - pub fn initialize(&mut self, db: &dyn Db) -> Result<()> { - // Use the database to find the Python environment - self.env = find_python_environment(db); - let env = self - .env - .as_ref() - .context("Could not find Python environment")?; - - // Initialize Django - let request = DjlsRequest { - query: Query::DjangoInit, - }; - let response = self.inspector.query(env, &self.path, &request)?; - - if !response.ok { - anyhow::bail!("Failed to initialize Django: {:?}", response.error); - } - - // Get template tags - let request = DjlsRequest { - query: Query::Templatetags, - }; - let response = self.inspector.query(env, &self.path, &request)?; - - if let Some(data) = response.data { - self.template_tags = Some(TemplateTags::from_json(&data)?); - } - - Ok(()) - } - - #[must_use] - pub fn template_tags(&self) -> Option<&TemplateTags> { - self.template_tags.as_ref() - } - - #[must_use] - pub fn path(&self) -> &Path { - &self.path - } -} - -impl fmt::Display for DjangoProject { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Project path: {}", self.path.display())?; - if let Some(py_env) = &self.env { - write!(f, "{py_env}")?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::fs; - - use tempfile::tempdir; - - use super::*; - - fn create_mock_django_project(dir: &Path) -> PathBuf { - let project_path = dir.to_path_buf(); - fs::create_dir_all(&project_path).unwrap(); - - // Create a mock Django project structure - fs::create_dir_all(project_path.join("myapp")).unwrap(); - fs::create_dir_all(project_path.join("myapp/templates")).unwrap(); - fs::write(project_path.join("manage.py"), "#!/usr/bin/env python").unwrap(); - - project_path - } - - #[test] - fn test_django_project_initialization() { - // This test needs to be run in an environment with Django installed - // For this test to pass, you would need a real Python environment with Django - // Here we're just testing the creation of the DjangoProject object - let project_dir = tempdir().unwrap(); - let project_path = create_mock_django_project(project_dir.path()); - - let project = DjangoProject::new(project_path); - - assert!(project.env.is_none()); // Environment not initialized yet - assert!(project.template_tags.is_none()); // Template tags not loaded yet - } - - #[test] - fn test_django_project_path() { - let project_dir = tempdir().unwrap(); - let project_path = create_mock_django_project(project_dir.path()); - - let project = DjangoProject::new(project_path.clone()); - - assert_eq!(project.path(), project_path.as_path()); - } - - #[test] - fn test_django_project_display() { - let project_dir = tempdir().unwrap(); - let project_path = create_mock_django_project(project_dir.path()); - - let project = DjangoProject::new(project_path.clone()); - - let display_str = format!("{project}"); - assert!(display_str.contains(&format!("Project path: {}", project_path.display()))); - } -} diff --git a/crates/djls-project/src/meta.rs b/crates/djls-project/src/meta.rs index c0418097..6208c09b 100644 --- a/crates/djls-project/src/meta.rs +++ b/crates/djls-project/src/meta.rs @@ -1,5 +1,27 @@ use std::path::PathBuf; +use crate::python::Interpreter; + +/// Complete project configuration as a Salsa input. +/// +/// Following Ruff's pattern, this contains all external project configuration +/// rather than minimal keys that everything derives from. This replaces both +/// Project input. +#[salsa::input] +#[derive(Debug)] +pub struct Project { + /// The project root path + #[returns(ref)] + pub root: String, + /// Interpreter specification for Python environment discovery + pub interpreter: Interpreter, + /// Optional Django settings module override from configuration + #[returns(ref)] + pub settings_module: Option, + /// Revision number for invalidation tracking + pub revision: u64, +} + #[derive(Clone, Debug)] pub struct ProjectMetadata { root: PathBuf, diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 2e2810b4..afcad8a0 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -1,9 +1,83 @@ use std::fmt; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; +use crate::db::Db as ProjectDb; +use crate::meta::Project; use crate::system; +/// Interpreter specification for Python environment discovery. +/// +/// This enum represents the different ways to specify which Python interpreter +/// to use for a project. +#[derive(Clone, Debug, PartialEq)] +pub enum Interpreter { + /// Automatically discover interpreter (`VIRTUAL_ENV`, project venv dirs, system) + Auto, + /// Use specific virtual environment path + VenvPath(String), + /// Use specific interpreter executable path + InterpreterPath(String), +} + +/// Resolve the Python interpreter path for a project. +/// +/// This tracked function determines the interpreter path based on the project's +/// interpreter specification. +#[salsa::tracked] +pub fn resolve_interpreter(db: &dyn ProjectDb, project: Project) -> Option { + // Create dependency on project revision + let _ = project.revision(db); + + let project_path = Path::new(project.root(db)); + + match &project.interpreter(db) { + Interpreter::InterpreterPath(path) => { + let path_buf = PathBuf::from(path.as_str()); + if path_buf.exists() { + Some(path_buf) + } else { + None + } + } + Interpreter::VenvPath(venv_path) => { + // Derive interpreter path from venv + #[cfg(unix)] + let interpreter_path = PathBuf::from(venv_path.as_str()).join("bin").join("python"); + #[cfg(windows)] + let interpreter_path = PathBuf::from(venv_path.as_str()) + .join("Scripts") + .join("python.exe"); + + if interpreter_path.exists() { + Some(interpreter_path) + } else { + None + } + } + Interpreter::Auto => { + // Try common venv directories + for venv_dir in &[".venv", "venv", "env", ".env"] { + let potential_venv = project_path.join(venv_dir); + if potential_venv.is_dir() { + #[cfg(unix)] + let interpreter_path = potential_venv.join("bin").join("python"); + #[cfg(windows)] + let interpreter_path = potential_venv.join("Scripts").join("python.exe"); + + if interpreter_path.exists() { + return Some(interpreter_path); + } + } + } + + // Fall back to system python + crate::system::find_executable("python").ok() + } + } +} + #[derive(Clone, Debug, PartialEq)] pub struct PythonEnvironment { pub python_path: PathBuf, @@ -128,6 +202,38 @@ impl fmt::Display for PythonEnvironment { Ok(()) } } +/// +/// Find the Python environment for a Django project. +/// +/// This Salsa tracked function discovers the Python environment based on: +/// 1. Explicit venv path from project config +/// 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 python_environment(db: &dyn ProjectDb, project: Project) -> Option> { + let interpreter_path = resolve_interpreter(db, project)?; + let project_path = Path::new(project.root(db)); + + // For venv paths, we need to determine the venv root + let interpreter_spec = project.interpreter(db); + let venv_path = match &interpreter_spec { + Interpreter::InterpreterPath(_) => { + // Try to determine venv from interpreter path + interpreter_path + .parent() + .and_then(|bin_dir| bin_dir.parent()) + .and_then(|venv_root| venv_root.to_str()) + } + Interpreter::VenvPath(path) => Some(path.as_str()), + Interpreter::Auto => { + // For auto-discovery, let PythonEnvironment::new handle it + None + } + }; + + PythonEnvironment::new(project_path, venv_path).map(Arc::new) +} #[cfg(test)] mod tests { @@ -171,9 +277,7 @@ mod tests { use super::*; use crate::system::mock::MockGuard; - use crate::system::mock::{ - self as sys_mock, - }; + use crate::system::mock::{self as sys_mock}; #[test] fn test_explicit_venv_path_found() { @@ -471,7 +575,6 @@ mod tests { use djls_workspace::InMemoryFileSystem; use super::*; - use crate::db::find_python_environment; use crate::db::Db as ProjectDb; use crate::meta::ProjectMetadata; @@ -513,10 +616,31 @@ mod tests { fn metadata(&self) -> &ProjectMetadata { &self.metadata } + + fn template_tags(&self) -> Option> { + None + } + + fn project(&self, root: &Path) -> crate::meta::Project { + let interpreter_spec = if let Some(venv_path) = self.metadata.venv() { + crate::python::Interpreter::VenvPath(venv_path.to_string_lossy().to_string()) + } else { + crate::python::Interpreter::Auto + }; + let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); + + crate::meta::Project::new( + self, + root.to_string_lossy().to_string(), + interpreter_spec, + django_settings, + 0, + ) + } } #[test] - fn test_find_python_environment_with_salsa_db() { + fn test_python_environment_with_salsa_db() { let project_dir = tempdir().unwrap(); let venv_dir = tempdir().unwrap(); @@ -530,8 +654,9 @@ mod tests { // Create a TestDatabase with the metadata let db = TestDatabase::new(metadata); - // Call the tracked function - let env = find_python_environment(&db); + // Call the new tracked function + let project = db.project(project_dir.path()); + let env = crate::python_environment(&db, project); // Verify we found the environment assert!(env.is_some(), "Should find environment via salsa db"); @@ -553,7 +678,7 @@ mod tests { } #[test] - fn test_find_python_environment_with_project_venv() { + fn test_python_environment_with_project_venv() { let project_dir = tempdir().unwrap(); // Create a .venv in the project directory @@ -569,8 +694,9 @@ mod tests { let _guard = system::mock::MockGuard; system::mock::remove_env_var("VIRTUAL_ENV"); - // Call the tracked function - let env = find_python_environment(&db); + // Call the new tracked function + let project = db.project(project_dir.path()); + let env = crate::python_environment(&db, project); // Verify we found the environment assert!( diff --git a/crates/djls-project/src/system.rs b/crates/djls-project/src/system.rs index 167e51c6..88269e38 100644 --- a/crates/djls-project/src/system.rs +++ b/crates/djls-project/src/system.rs @@ -103,9 +103,7 @@ mod tests { use which::Error as WhichError; use super::mock::MockGuard; - use super::mock::{ - self as sys_mock, - }; + use super::mock::{self as sys_mock}; use super::*; #[test] diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 94eb47f3..6d30ec80 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -12,7 +12,9 @@ use std::sync::Mutex; use dashmap::DashMap; use djls_project::Db as ProjectDb; +use djls_project::Project; use djls_project::ProjectMetadata; +use djls_project::TemplateTags; use djls_templates::db::Db as TemplateDb; use djls_templates::templatetags::TagSpecs; use djls_workspace::db::Db as WorkspaceDb; @@ -39,6 +41,9 @@ pub struct DjangoDatabase { /// Project metadata containing root path and venv configuration. metadata: ProjectMetadata, + /// Unified project inputs cache - maps root path to Project input + projects: Arc>, + storage: salsa::Storage, // The logs are only used for testing and demonstrating reuse: @@ -57,6 +62,7 @@ impl Default for DjangoDatabase { fs: Arc::new(InMemoryFileSystem::new()), files: Arc::new(DashMap::new()), metadata: ProjectMetadata::new(PathBuf::from("/test"), None), + projects: Arc::new(DashMap::new()), storage: salsa::Storage::new(Some(Box::new({ let logs = logs.clone(); move |event| { @@ -86,6 +92,7 @@ impl DjangoDatabase { fs: file_system, files, metadata, + projects: Arc::new(DashMap::new()), storage: salsa::Storage::new(None), #[cfg(test)] logs: Arc::new(Mutex::new(None)), @@ -185,4 +192,36 @@ impl ProjectDb for DjangoDatabase { fn metadata(&self) -> &ProjectMetadata { &self.metadata } + + fn template_tags(&self) -> Option> { + let project = self.project(self.metadata().root().as_path()); + djls_project::template_tags(self, project).map(Arc::new) + } + + fn project(&self, root: &Path) -> Project { + let root_buf = root.to_path_buf(); + + // Check if we already have this project + if let Some(project) = self.projects.get(&root_buf) { + return *project; + } + + // Create a new Project input with complete configuration + let interpreter_spec = if let Some(venv_path) = self.metadata.venv() { + djls_project::Interpreter::VenvPath(venv_path.to_string_lossy().to_string()) + } else { + djls_project::Interpreter::Auto + }; + let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); + + let project = Project::new( + self, + root.to_string_lossy().to_string(), + interpreter_spec, + django_settings, + 0, + ); + self.projects.insert(root_buf, project); + project + } } diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 204a4ade..059df554 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -1,6 +1,8 @@ use std::future::Future; +use std::path::PathBuf; use std::sync::Arc; +use djls_project::Db as ProjectDb; use djls_templates::analyze_template; use djls_templates::TemplateDiagnostic; use djls_workspace::paths; @@ -188,15 +190,14 @@ impl LanguageServer for DjangoLanguageServer { self.with_session_task(move |session_arc| async move { let project_path_and_venv = { let session_lock = session_arc.lock().await; - session_lock.project().map(|p| { - ( - p.path().display().to_string(), - session_lock - .settings() - .venv_path() - .map(std::string::ToString::to_string), - ) - }) + let metadata = session_lock.db().metadata(); + Some(( + metadata.root().display().to_string(), + session_lock + .settings() + .venv_path() + .map(std::string::ToString::to_string), + )) }; if let Some((path_display, venv_path)) = project_path_and_venv { @@ -225,9 +226,7 @@ impl LanguageServer for DjangoLanguageServer { e ); - // Clear project on error - let mut session_lock = session_arc.lock().await; - *session_lock.project_mut() = None; + // Django initialization is now handled lazily through Salsa } } } else { @@ -369,7 +368,8 @@ impl LanguageServer for DjangoLanguageServer { let position = params.text_document_position.position; let encoding = session.position_encoding(); let file_kind = FileKind::from_path(&path); - let template_tags = session.project().and_then(|p| p.template_tags()); + let template_tags_arc = session.with_db(djls_project::Db::template_tags); + let template_tags = template_tags_arc.as_ref().map(std::convert::AsRef::as_ref); let tag_specs = session.with_db(djls_templates::Db::tag_specs); let supports_snippets = session.supports_snippets(); @@ -475,13 +475,16 @@ impl LanguageServer for DjangoLanguageServer { tracing::info!("Configuration change detected. Reloading settings..."); let project_path = self - .with_session(|session| session.project().map(|p| p.path().to_path_buf())) + .with_session_mut(|session| { + let project = session.project(); + Some(PathBuf::from(project.root(session.database()).as_str())) + }) .await; if let Some(path) = project_path { self.with_session_mut(|session| match djls_conf::Settings::new(path.as_path()) { Ok(new_settings) => { - session.set_settings(new_settings); + session.update_session_state(new_settings); } Err(e) => { tracing::error!("Error loading settings: {}", e); diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 55a1ba09..24f78351 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -3,24 +3,46 @@ //! This module implements the LSP session abstraction that manages project-specific //! state and the Salsa database for incremental computation. +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use anyhow::Result; use dashmap::DashMap; use djls_conf::Settings; -use djls_project::DjangoProject; +use djls_project::Db as ProjectDb; use djls_project::ProjectMetadata; +use djls_project::TemplateTags; use djls_workspace::db::SourceFile; use djls_workspace::paths; use djls_workspace::PositionEncoding; use djls_workspace::TextDocument; use djls_workspace::Workspace; +use salsa::Setter; use tower_lsp_server::lsp_types; use url::Url; use crate::db::DjangoDatabase; +/// Complete LSP session configuration as a Salsa input. +/// +/// This contains all external session state including client capabilities, +/// workspace configuration, and server settings. +#[salsa::input] +pub struct SessionState { + /// The project root path + #[returns(ref)] + pub project_root: Option>, + /// Client capabilities negotiated during initialization + pub client_capabilities: lsp_types::ClientCapabilities, + /// Position encoding negotiated with client + pub position_encoding: djls_workspace::PositionEncoding, + /// Server settings from configuration + pub server_settings: djls_conf::Settings, + /// Revision number for invalidation tracking + pub revision: u64, +} + /// LSP Session managing project-specific state and database operations. /// /// The Session serves as the main entry point for LSP operations, managing: @@ -28,13 +50,11 @@ use crate::db::DjangoDatabase; /// - Project configuration and settings /// - Client capabilities and position encoding /// - Workspace operations (buffers and file system) +/// - All Salsa inputs (`SessionState`, Project) /// /// Following Ruff's architecture, the concrete database lives at this level /// and is passed down to operations that need it. pub struct Session { - /// The Django project configuration - project: Option, - /// LSP server settings settings: Settings, @@ -50,7 +70,14 @@ pub struct Session { /// Position encoding negotiated with client position_encoding: PositionEncoding, + /// The Salsa database for incremental computation db: DjangoDatabase, + + /// Session state input - complete LSP session configuration + state: Option, + + /// Cached template tags - Session is the Arc boundary + template_tags: Option>, } impl Session { @@ -65,21 +92,19 @@ impl Session { std::env::current_dir().ok() }); - let (project, settings, metadata) = if let Some(path) = &project_path { + let (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())); - // 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) + (settings, metadata) } else { // Default metadata for when there's no project path let metadata = ProjectMetadata::new(PathBuf::from("."), None); - (None, Settings::default(), metadata) + (Settings::default(), metadata) }; // Create workspace for buffer management @@ -87,25 +112,76 @@ impl Session { // 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, metadata); + let mut db = DjangoDatabase::new(workspace.file_system(), files, metadata); + + // Create the session state input + let project_root = project_path + .as_ref() + .and_then(|p| p.to_str()) + .map(Arc::from); + let session_state = SessionState::new( + &db, + project_root, + params.capabilities.clone(), + PositionEncoding::negotiate(params), + settings.clone(), + 0, + ); + + // Initialize the project input with correct interpreter spec from settings + if let Some(root_path) = &project_path { + let project = db.project(root_path); + + // Update interpreter spec based on VIRTUAL_ENV if available + if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") { + let interpreter = djls_project::Interpreter::VenvPath(virtual_env); + project.set_interpreter(&mut db).to(interpreter); + } + + // Update Django settings module override if available + if let Ok(settings_module) = std::env::var("DJANGO_SETTINGS_MODULE") { + project + .set_settings_module(&mut db) + .to(Some(settings_module)); + } + + // Bump revision to invalidate dependent queries + let current_rev = project.revision(&db); + project.set_revision(&mut db).to(current_rev + 1); + } Self { - db, - project, settings, workspace, client_capabilities: params.capabilities.clone(), position_encoding: PositionEncoding::negotiate(params), + db, + state: Some(session_state), + template_tags: None, } } - #[must_use] - pub fn project(&self) -> Option<&DjangoProject> { - self.project.as_ref() + /// Refresh Django data for the project (template tags, etc.) + /// + /// This method caches the template tags in the Session (Arc boundary) + /// and warms up other tracked functions. + pub fn refresh_django_data(&mut self) -> Result<()> { + // Get the unified project input + let project = self.project(); + + // Cache template tags in Session (Arc boundary) + self.template_tags = djls_project::template_tags(&self.db, project).map(Arc::new); + + // Warm up other tracked functions + let _ = djls_project::django_available(&self.db, project); + let _ = djls_project::django_settings_module(&self.db, project); + + Ok(()) } - pub fn project_mut(&mut self) -> &mut Option { - &mut self.project + #[must_use] + pub fn db(&self) -> &DjangoDatabase { + &self.db } #[must_use] @@ -157,11 +233,13 @@ impl Session { /// Initialize the project with the database. pub fn initialize_project(&mut self) -> Result<()> { - if let Some(project) = self.project.as_mut() { - project.initialize(&self.db) - } else { - Ok(()) - } + // Discover Python environment and update inputs + self.discover_python_environment()?; + + // Refresh Django data using the new inputs + self.refresh_django_data()?; + + Ok(()) } /// Open a document in the session. @@ -239,6 +317,76 @@ impl Session { self.workspace.get_document(url) } + /// Get the session state input + #[must_use] + pub fn session_state(&self) -> Option { + self.state + } + + /// Update the session state with new settings + pub fn update_session_state(&mut self, new_settings: Settings) { + if let Some(session_state) = self.state { + // Update the settings in the input + session_state + .set_server_settings(&mut self.db) + .to(new_settings.clone()); + // Bump revision to invalidate dependent queries + let current_rev = session_state.revision(&self.db); + session_state.set_revision(&mut self.db).to(current_rev + 1); + } + self.settings = new_settings; + } + + /// Get or create unified project input for the current project + pub fn project(&mut self) -> djls_project::Project { + let project_root = if let Some(state) = self.state { + if let Some(root) = state.project_root(&self.db) { + Path::new(root.as_ref()) + } else { + self.db.metadata().root().as_path() + } + } else { + self.db.metadata().root().as_path() + }; + self.db.project(project_root) + } + + /// Update project configuration when settings change + pub fn update_project_config( + &mut self, + new_venv_path: Option, + new_django_settings: Option, + ) { + let project = self.project(); + + // Update the interpreter spec if venv path is provided + if let Some(venv_path) = new_venv_path { + let interpreter_spec = + djls_project::Interpreter::VenvPath(venv_path.to_string_lossy().to_string()); + project.set_interpreter(&mut self.db).to(interpreter_spec); + } + + // Update Django settings override if provided + if let Some(settings) = new_django_settings { + project.set_settings_module(&mut self.db).to(Some(settings)); + } + + // Bump revision to invalidate dependent queries + let current_rev = project.revision(&self.db); + project.set_revision(&mut self.db).to(current_rev + 1); + } + + /// Discover and update Python environment state + pub fn discover_python_environment(&mut self) -> Result<()> { + let project = self.project(); + + // Use the new tracked functions to ensure environment discovery + let _interpreter_path = djls_project::resolve_interpreter(&self.db, project); + let _env = djls_project::python_environment(&self.db, project); + + Ok(()) + } + /// Get or create a file in the database. pub fn get_or_create_file(&mut self, path: &PathBuf) -> SourceFile { self.db.get_or_create_file(path) @@ -256,11 +404,22 @@ impl Session { .and_then(|td| td.diagnostic.as_ref()) .is_some() } + + /// Get template tags for the current project. + /// + /// Returns a reference to the cached template tags, or None if not available. + /// Session acts as the Arc boundary, so this returns a borrow. + #[must_use] + pub fn template_tags(&self) -> Option<&TemplateTags> { + self.template_tags.as_deref() + } } impl Default for Session { fn default() -> Self { - Self::new(&lsp_types::InitializeParams::default()) + let mut session = Self::new(&lsp_types::InitializeParams::default()); + session.state = None; // Default session has no state + session } } From aef0e6f809497512658490e180ceb4ad995f0106 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 15:29:06 -0500 Subject: [PATCH 02/17] wip --- crates/djls-project/src/db.rs | 11 +-- crates/djls-project/src/django.rs | 12 ++-- .../djls-project/src/django/templatetags.rs | 6 +- crates/djls-project/src/inspector.rs | 7 +- crates/djls-project/src/lib.rs | 1 - crates/djls-project/src/meta.rs | 28 ++------ crates/djls-project/src/python.rs | 15 ++-- crates/djls-server/src/db.rs | 71 +++++++++---------- crates/djls-server/src/server.rs | 18 ++--- crates/djls-server/src/session.rs | 38 ++++------ 10 files changed, 94 insertions(+), 113 deletions(-) diff --git a/crates/djls-project/src/db.rs b/crates/djls-project/src/db.rs index 6a7cd916..e6c18c65 100644 --- a/crates/djls-project/src/db.rs +++ b/crates/djls-project/src/db.rs @@ -16,19 +16,22 @@ use std::sync::Arc; use djls_workspace::Db as WorkspaceDb; use crate::django::TemplateTags; +use crate::inspector::pool::InspectorPool; use crate::meta::Project; -use crate::meta::ProjectMetadata; /// Project-specific database trait extending the workspace database #[salsa::db] pub trait Db: WorkspaceDb { - /// Get the project metadata containing root path and venv configuration - fn metadata(&self) -> &ProjectMetadata; - /// Get discovered template tags for the project (if available). /// This is populated by the LSP server after querying Django. fn template_tags(&self) -> Option>; /// Get or create a Project input for a given path fn project(&self, root: &Path) -> Project; + + /// Get the current project (typically the main project being worked on) + fn current_project(&self) -> Project; + + /// Get the shared inspector pool for executing Python queries + fn inspector_pool(&self) -> Arc; } diff --git a/crates/djls-project/src/django.rs b/crates/djls-project/src/django.rs index 61093016..53d17bfa 100644 --- a/crates/djls-project/src/django.rs +++ b/crates/djls-project/src/django.rs @@ -5,18 +5,19 @@ use std::path::Path; use crate::db::Db as ProjectDb; use crate::inspector::inspector_run; use crate::inspector::queries::InspectorQueryKind; -use crate::meta::Project; use crate::python::python_environment; pub use templatetags::template_tags; pub use templatetags::TemplateTags; -/// Check if Django is available for a project. +/// Check if Django is available for the current project. /// /// This determines if Django is installed and configured in the Python environment. /// First consults the inspector, then falls back to environment detection. #[salsa::tracked] -pub fn django_available(db: &dyn ProjectDb, project: Project) -> bool { +pub fn django_available(db: &dyn ProjectDb) -> bool { + let project = db.current_project(); + // First try to get Django availability from inspector if let Some(json_data) = inspector_run(db, project, InspectorQueryKind::DjangoAvailable) { // Parse the JSON response - expect a boolean @@ -29,12 +30,13 @@ pub fn django_available(db: &dyn ProjectDb, project: Project) -> bool { python_environment(db, project).is_some() } -/// Get the Django settings module name for a project. +/// Get the Django settings module name for the current project. /// /// Returns the settings_module_override from project, or inspector result, /// or DJANGO_SETTINGS_MODULE env var, or attempts to detect it. #[salsa::tracked] -pub fn django_settings_module(db: &dyn ProjectDb, project: Project) -> Option { +pub fn django_settings_module(db: &dyn ProjectDb) -> Option { + let project = db.current_project(); let _ = project.revision(db); let project_path = Path::new(project.root(db)); diff --git a/crates/djls-project/src/django/templatetags.rs b/crates/djls-project/src/django/templatetags.rs index a39bcd1a..4137a6b0 100644 --- a/crates/djls-project/src/django/templatetags.rs +++ b/crates/djls-project/src/django/templatetags.rs @@ -7,14 +7,14 @@ use serde_json::Value; use crate::db::Db as ProjectDb; use crate::inspector::inspector_run; use crate::inspector::queries::InspectorQueryKind; -use crate::meta::Project; -/// Get template tags for a project by querying the inspector. +/// Get template tags for the current project by querying the inspector. /// /// This tracked function calls the inspector to retrieve Django template tags /// and parses the JSON response into a TemplateTags struct. #[salsa::tracked] -pub fn template_tags(db: &dyn ProjectDb, project: Project) -> Option { +pub fn template_tags(db: &dyn ProjectDb) -> Option { + let project = db.current_project(); let json_str = inspector_run(db, project, InspectorQueryKind::TemplateTags)?; // Parse the JSON string into a Value first diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index 36b74ed9..6ece86a6 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -30,7 +30,7 @@ pub struct DjlsResponse { /// Run an inspector query and return the JSON result as a string. /// -/// This tracked function executes inspector queries through a temporary pool +/// This tracked function executes inspector queries through the shared pool /// and caches the results based on project state and query kind. #[allow(clippy::drop_non_drop)] #[salsa::tracked] @@ -59,9 +59,8 @@ pub fn inspector_run( let request = crate::inspector::DjlsRequest { query }; - // Create a temporary inspector pool for this query - // Note: In production, this could be optimized with a shared pool - let pool = crate::inspector::pool::InspectorPool::new(); + // Use the shared inspector pool from the database + let pool = db.inspector_pool(); match pool.query(&python_env, project_path, &request) { Ok(response) => { diff --git a/crates/djls-project/src/lib.rs b/crates/djls-project/src/lib.rs index 483cc734..f1f7c21c 100644 --- a/crates/djls-project/src/lib.rs +++ b/crates/djls-project/src/lib.rs @@ -13,7 +13,6 @@ pub use django::TemplateTags; pub use inspector::inspector_run; pub use inspector::queries::InspectorQueryKind; pub use meta::Project; -pub use meta::ProjectMetadata; pub use python::python_environment; pub use python::resolve_interpreter; pub use python::Interpreter; diff --git a/crates/djls-project/src/meta.rs b/crates/djls-project/src/meta.rs index 6208c09b..9d71de2f 100644 --- a/crates/djls-project/src/meta.rs +++ b/crates/djls-project/src/meta.rs @@ -1,18 +1,19 @@ -use std::path::PathBuf; - use crate::python::Interpreter; /// Complete project configuration as a Salsa input. /// /// Following Ruff's pattern, this contains all external project configuration /// rather than minimal keys that everything derives from. This replaces both -/// Project input. +/// Project input and ProjectMetadata. #[salsa::input] #[derive(Debug)] pub struct Project { /// The project root path #[returns(ref)] pub root: String, + /// Optional virtual environment path + #[returns(ref)] + pub venv: Option, /// Interpreter specification for Python environment discovery pub interpreter: Interpreter, /// Optional Django settings module override from configuration @@ -22,25 +23,4 @@ pub struct Project { pub revision: u64, } -#[derive(Clone, Debug)] -pub struct ProjectMetadata { - root: PathBuf, - venv: Option, -} - -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 afcad8a0..8ac1ecbf 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -613,10 +613,6 @@ mod tests { #[salsa::db] impl ProjectDb for TestDatabase { - fn metadata(&self) -> &ProjectMetadata { - &self.metadata - } - fn template_tags(&self) -> Option> { None } @@ -632,11 +628,22 @@ mod tests { crate::meta::Project::new( self, root.to_string_lossy().to_string(), + self.metadata.venv().map(|p| p.to_string_lossy().to_string()), interpreter_spec, django_settings, 0, ) } + + fn current_project(&self) -> crate::meta::Project { + // For tests, return a project for the current directory + let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + self.project(current_dir.as_path()) + } + + fn inspector_pool(&self) -> Arc { + Arc::new(crate::inspector::pool::InspectorPool::new()) + } } #[test] diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 6d30ec80..b6f00039 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -13,8 +13,8 @@ use std::sync::Mutex; use dashmap::DashMap; use djls_project::Db as ProjectDb; use djls_project::Project; -use djls_project::ProjectMetadata; use djls_project::TemplateTags; +use djls_project::inspector::pool::InspectorPool; use djls_templates::db::Db as TemplateDb; use djls_templates::templatetags::TagSpecs; use djls_workspace::db::Db as WorkspaceDb; @@ -38,12 +38,12 @@ pub struct DjangoDatabase { /// Maps paths to [`SourceFile`] entities for O(1) lookup. files: Arc>, - /// Project metadata containing root path and venv configuration. - metadata: ProjectMetadata, - /// Unified project inputs cache - maps root path to Project input projects: Arc>, + /// Shared inspector pool for executing Python queries + inspector_pool: Arc, + storage: salsa::Storage, // The logs are only used for testing and demonstrating reuse: @@ -61,8 +61,8 @@ impl Default for DjangoDatabase { Self { fs: Arc::new(InMemoryFileSystem::new()), files: Arc::new(DashMap::new()), - metadata: ProjectMetadata::new(PathBuf::from("/test"), None), projects: Arc::new(DashMap::new()), + inspector_pool: Arc::new(InspectorPool::new()), storage: salsa::Storage::new(Some(Box::new({ let logs = logs.clone(); move |event| { @@ -82,17 +82,16 @@ impl Default for DjangoDatabase { } impl DjangoDatabase { - /// Create a new [`DjangoDatabase`] with the given file system, file map, and project metadata. + /// Create a new [`DjangoDatabase`] with the given file system and file map. pub fn new( file_system: Arc, files: Arc>, - metadata: ProjectMetadata, ) -> Self { Self { fs: file_system, files, - metadata, projects: Arc::new(DashMap::new()), + inspector_pool: Arc::new(InspectorPool::new()), storage: salsa::Storage::new(None), #[cfg(test)] logs: Arc::new(Mutex::new(None)), @@ -170,7 +169,8 @@ impl WorkspaceDb for DjangoDatabase { #[salsa::db] impl TemplateDb for DjangoDatabase { fn tag_specs(&self) -> Arc { - let project_root = self.metadata.root(); + let project_root_buf = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + let project_root = project_root_buf.as_path(); if let Ok(user_specs) = TagSpecs::load_user_specs(project_root) { // If user specs exist and aren't empty, merge with built-in specs @@ -189,39 +189,38 @@ impl TemplateDb for DjangoDatabase { #[salsa::db] impl ProjectDb for DjangoDatabase { - fn metadata(&self) -> &ProjectMetadata { - &self.metadata - } - fn template_tags(&self) -> Option> { - let project = self.project(self.metadata().root().as_path()); - djls_project::template_tags(self, project).map(Arc::new) + djls_project::template_tags(self).map(Arc::new) } fn project(&self, root: &Path) -> Project { let root_buf = root.to_path_buf(); - // Check if we already have this project - if let Some(project) = self.projects.get(&root_buf) { - return *project; - } + // Use entry API for atomic check-and-insert to avoid race conditions + *self.projects.entry(root_buf.clone()).or_insert_with(|| { + // Create a new Project input with complete configuration + let interpreter_spec = djls_project::Interpreter::Auto; + let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); + + Project::new( + self, + root.to_string_lossy().to_string(), + None, // No venv by default + interpreter_spec, + django_settings, + 0, + ) + }) + } - // Create a new Project input with complete configuration - let interpreter_spec = if let Some(venv_path) = self.metadata.venv() { - djls_project::Interpreter::VenvPath(venv_path.to_string_lossy().to_string()) - } else { - djls_project::Interpreter::Auto - }; - let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); - - let project = Project::new( - self, - root.to_string_lossy().to_string(), - interpreter_spec, - django_settings, - 0, - ); - self.projects.insert(root_buf, project); - project + fn current_project(&self) -> Project { + // For now, return a project for the current directory + // In a real implementation, this might be configurable + let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + self.project(current_dir.as_path()) + } + + fn inspector_pool(&self) -> Arc { + self.inspector_pool.clone() } } diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 059df554..c93cfaa1 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -2,7 +2,6 @@ use std::future::Future; use std::path::PathBuf; use std::sync::Arc; -use djls_project::Db as ProjectDb; use djls_templates::analyze_template; use djls_templates::TemplateDiagnostic; use djls_workspace::paths; @@ -190,14 +189,15 @@ impl LanguageServer for DjangoLanguageServer { self.with_session_task(move |session_arc| async move { let project_path_and_venv = { let session_lock = session_arc.lock().await; - let metadata = session_lock.db().metadata(); - Some(( - metadata.root().display().to_string(), - session_lock - .settings() - .venv_path() - .map(std::string::ToString::to_string), - )) + std::env::current_dir().ok().map(|current_dir| { + ( + current_dir.display().to_string(), + session_lock + .settings() + .venv_path() + .map(std::string::ToString::to_string), + ) + }) }; if let Some((path_display, venv_path)) = project_path_and_venv { diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 24f78351..3e2dd246 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -3,7 +3,6 @@ //! This module implements the LSP session abstraction that manages project-specific //! state and the Salsa database for incremental computation. -use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -11,7 +10,6 @@ use anyhow::Result; use dashmap::DashMap; use djls_conf::Settings; use djls_project::Db as ProjectDb; -use djls_project::ProjectMetadata; use djls_project::TemplateTags; use djls_workspace::db::SourceFile; use djls_workspace::paths; @@ -92,27 +90,18 @@ impl Session { std::env::current_dir().ok() }); - let (settings, metadata) = if let Some(path) = &project_path { - let settings = - djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default()); - - // 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); - - (settings, metadata) + let settings = if let Some(path) = &project_path { + djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default()) } else { - // Default metadata for when there's no project path - let metadata = ProjectMetadata::new(PathBuf::from("."), None); - (Settings::default(), metadata) + Settings::default() }; // Create workspace for buffer management let workspace = Workspace::new(); - // Create the concrete database with the workspace's file system and metadata + // Create the concrete database with the workspace's file system let files = Arc::new(DashMap::new()); - let mut db = DjangoDatabase::new(workspace.file_system(), files, metadata); + let mut db = DjangoDatabase::new(workspace.file_system(), files); // Create the session state input let project_root = project_path @@ -170,11 +159,11 @@ impl Session { let project = self.project(); // Cache template tags in Session (Arc boundary) - self.template_tags = djls_project::template_tags(&self.db, project).map(Arc::new); + self.template_tags = djls_project::template_tags(&self.db).map(Arc::new); // Warm up other tracked functions - let _ = djls_project::django_available(&self.db, project); - let _ = djls_project::django_settings_module(&self.db, project); + let _ = djls_project::django_available(&self.db); + let _ = djls_project::django_settings_module(&self.db); Ok(()) } @@ -339,15 +328,18 @@ impl Session { /// Get or create unified project input for the current project pub fn project(&mut self) -> djls_project::Project { - let project_root = if let Some(state) = self.state { + let project_root_buf = if let Some(state) = &self.state { if let Some(root) = state.project_root(&self.db) { - Path::new(root.as_ref()) + PathBuf::from(root.as_ref()) } else { - self.db.metadata().root().as_path() + // Fall back to current directory + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) } } else { - self.db.metadata().root().as_path() + // Fall back to current directory + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) }; + let project_root = project_root_buf.as_path(); self.db.project(project_root) } From 63f44e80ad96c1171718923eedf2ef622b0c8ae1 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 15:35:18 -0500 Subject: [PATCH 03/17] wip --- crates/djls-project/src/inspector.rs | 1 - crates/djls-server/src/session.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index 6ece86a6..704d62da 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -32,7 +32,6 @@ pub struct DjlsResponse { /// /// This tracked function executes inspector queries through the shared pool /// and caches the results based on project state and query kind. -#[allow(clippy::drop_non_drop)] #[salsa::tracked] pub fn inspector_run( db: &dyn ProjectDb, diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 3e2dd246..bac7fd30 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -62,7 +62,6 @@ pub struct Session { /// but not the database (which is owned directly by Session). workspace: Workspace, - #[allow(dead_code)] client_capabilities: lsp_types::ClientCapabilities, /// Position encoding negotiated with client From bde021a5c2c688dcfebf0244e84d3db07c0ad894 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 18:10:34 -0500 Subject: [PATCH 04/17] wip --- crates/djls-project/src/db.rs | 13 +- crates/djls-project/src/django.rs | 10 +- .../djls-project/src/django/templatetags.rs | 4 +- crates/djls-project/src/inspector.rs | 5 +- crates/djls-project/src/lib.rs | 2 +- crates/djls-project/src/meta.rs | 5 - crates/djls-project/src/python.rs | 100 ++++----- crates/djls-server/src/db.rs | 66 +++--- crates/djls-server/src/server.rs | 45 ++-- crates/djls-server/src/session.rs | 197 +++--------------- 10 files changed, 146 insertions(+), 301 deletions(-) diff --git a/crates/djls-project/src/db.rs b/crates/djls-project/src/db.rs index e6c18c65..4bf3ee72 100644 --- a/crates/djls-project/src/db.rs +++ b/crates/djls-project/src/db.rs @@ -10,27 +10,18 @@ //! - Tracked functions compute derived values (Python env, Django config) //! - Database trait provides stable configuration (metadata, template tags) -use std::path::Path; use std::sync::Arc; use djls_workspace::Db as WorkspaceDb; -use crate::django::TemplateTags; use crate::inspector::pool::InspectorPool; use crate::meta::Project; /// Project-specific database trait extending the workspace database #[salsa::db] pub trait Db: WorkspaceDb { - /// Get discovered template tags for the project (if available). - /// This is populated by the LSP server after querying Django. - fn template_tags(&self) -> Option>; - - /// Get or create a Project input for a given path - fn project(&self, root: &Path) -> Project; - - /// Get the current project (typically the main project being worked on) - fn current_project(&self) -> Project; + /// Get the current project (if set) + fn project(&self) -> Option; /// Get the shared inspector pool for executing Python queries fn inspector_pool(&self) -> Arc; diff --git a/crates/djls-project/src/django.rs b/crates/djls-project/src/django.rs index 53d17bfa..52c98afb 100644 --- a/crates/djls-project/src/django.rs +++ b/crates/djls-project/src/django.rs @@ -7,7 +7,7 @@ use crate::inspector::inspector_run; use crate::inspector::queries::InspectorQueryKind; use crate::python::python_environment; -pub use templatetags::template_tags; +pub use templatetags::get_templatetags; pub use templatetags::TemplateTags; /// Check if Django is available for the current project. @@ -16,7 +16,9 @@ pub use templatetags::TemplateTags; /// First consults the inspector, then falls back to environment detection. #[salsa::tracked] pub fn django_available(db: &dyn ProjectDb) -> bool { - let project = db.current_project(); + let Some(project) = db.project() else { + return false; + }; // First try to get Django availability from inspector if let Some(json_data) = inspector_run(db, project, InspectorQueryKind::DjangoAvailable) { @@ -27,7 +29,7 @@ pub fn django_available(db: &dyn ProjectDb) -> bool { } // Fallback to environment detection - python_environment(db, project).is_some() + python_environment(db).is_some() } /// Get the Django settings module name for the current project. @@ -36,7 +38,7 @@ pub fn django_available(db: &dyn ProjectDb) -> bool { /// or DJANGO_SETTINGS_MODULE env var, or attempts to detect it. #[salsa::tracked] pub fn django_settings_module(db: &dyn ProjectDb) -> Option { - let project = db.current_project(); + let project = db.project()?; let _ = project.revision(db); let project_path = Path::new(project.root(db)); diff --git a/crates/djls-project/src/django/templatetags.rs b/crates/djls-project/src/django/templatetags.rs index 4137a6b0..66873854 100644 --- a/crates/djls-project/src/django/templatetags.rs +++ b/crates/djls-project/src/django/templatetags.rs @@ -13,8 +13,8 @@ use crate::inspector::queries::InspectorQueryKind; /// This tracked function calls the inspector to retrieve Django template tags /// and parses the JSON response into a TemplateTags struct. #[salsa::tracked] -pub fn template_tags(db: &dyn ProjectDb) -> Option { - let project = db.current_project(); +pub fn get_templatetags(db: &dyn ProjectDb) -> Option { + let project = db.project()?; let json_str = inspector_run(db, project, InspectorQueryKind::TemplateTags)?; // Parse the JSON string into a Value first diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index 704d62da..1fa85897 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -32,6 +32,7 @@ pub struct DjlsResponse { /// /// This tracked function executes inspector queries through the shared pool /// and caches the results based on project state and query kind. +#[allow(clippy::drop_non_drop)] #[salsa::tracked] pub fn inspector_run( db: &dyn ProjectDb, @@ -42,11 +43,11 @@ pub fn inspector_run( let _ = project.revision(db); // Get interpreter path - required for inspector - let _interpreter_path = resolve_interpreter(db, project)?; + let _interpreter_path = resolve_interpreter(db)?; let project_path = Path::new(project.root(db)); // Get Python environment for inspector - let python_env = python_environment(db, project)?; + let python_env = python_environment(db)?; // Create the appropriate query based on kind let query = match kind { diff --git a/crates/djls-project/src/lib.rs b/crates/djls-project/src/lib.rs index f1f7c21c..63022192 100644 --- a/crates/djls-project/src/lib.rs +++ b/crates/djls-project/src/lib.rs @@ -8,7 +8,7 @@ mod system; pub use db::Db; pub use django::django_available; pub use django::django_settings_module; -pub use django::template_tags; +pub use django::get_templatetags; pub use django::TemplateTags; pub use inspector::inspector_run; pub use inspector::queries::InspectorQueryKind; diff --git a/crates/djls-project/src/meta.rs b/crates/djls-project/src/meta.rs index 9d71de2f..a0781dd6 100644 --- a/crates/djls-project/src/meta.rs +++ b/crates/djls-project/src/meta.rs @@ -11,9 +11,6 @@ pub struct Project { /// The project root path #[returns(ref)] pub root: String, - /// Optional virtual environment path - #[returns(ref)] - pub venv: Option, /// Interpreter specification for Python environment discovery pub interpreter: Interpreter, /// Optional Django settings module override from configuration @@ -22,5 +19,3 @@ pub struct Project { /// Revision number for invalidation tracking pub revision: u64, } - - diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 8ac1ecbf..8d6f8e36 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; use std::sync::Arc; use crate::db::Db as ProjectDb; -use crate::meta::Project; use crate::system; /// Interpreter specification for Python environment discovery. @@ -21,12 +20,13 @@ pub enum Interpreter { InterpreterPath(String), } -/// Resolve the Python interpreter path for a project. +/// Resolve the Python interpreter path for the current project. /// /// This tracked function determines the interpreter path based on the project's /// interpreter specification. #[salsa::tracked] -pub fn resolve_interpreter(db: &dyn ProjectDb, project: Project) -> Option { +pub fn resolve_interpreter(db: &dyn ProjectDb) -> Option { + let project = db.project()?; // Create dependency on project revision let _ = project.revision(db); @@ -203,7 +203,7 @@ impl fmt::Display for PythonEnvironment { } } /// -/// Find the Python environment for a Django project. +/// Find the Python environment for the current Django project. /// /// This Salsa tracked function discovers the Python environment based on: /// 1. Explicit venv path from project config @@ -211,8 +211,9 @@ impl fmt::Display for PythonEnvironment { /// 3. Common venv directories in project root (.venv, venv, env, .env) /// 4. System Python as fallback #[salsa::tracked] -pub fn python_environment(db: &dyn ProjectDb, project: Project) -> Option> { - let interpreter_path = resolve_interpreter(db, project)?; +pub fn python_environment(db: &dyn ProjectDb) -> Option> { + let project = db.project()?; + let interpreter_path = resolve_interpreter(db)?; let project_path = Path::new(project.root(db)); // For venv paths, we need to determine the venv root @@ -576,25 +577,32 @@ mod tests { use super::*; use crate::db::Db as ProjectDb; - use crate::meta::ProjectMetadata; + use salsa::Setter; + use std::sync::Mutex; /// Test implementation of ProjectDb for unit tests #[salsa::db] #[derive(Clone)] struct TestDatabase { storage: salsa::Storage, - metadata: ProjectMetadata, + project_root: PathBuf, + project: Arc>>, fs: Arc, } impl TestDatabase { - fn new(metadata: ProjectMetadata) -> Self { + fn new(project_root: PathBuf) -> Self { Self { storage: salsa::Storage::new(None), - metadata, + project_root, + project: Arc::new(Mutex::new(None)), fs: Arc::new(InMemoryFileSystem::new()), } } + + fn set_project(&self, project: crate::meta::Project) { + *self.project.lock().unwrap() = Some(project); + } } #[salsa::db] @@ -617,28 +625,23 @@ mod tests { None } - fn project(&self, root: &Path) -> crate::meta::Project { - let interpreter_spec = if let Some(venv_path) = self.metadata.venv() { - crate::python::Interpreter::VenvPath(venv_path.to_string_lossy().to_string()) - } else { - crate::python::Interpreter::Auto - }; - let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); - - crate::meta::Project::new( - self, - root.to_string_lossy().to_string(), - self.metadata.venv().map(|p| p.to_string_lossy().to_string()), - interpreter_spec, - django_settings, - 0, - ) - } - - fn current_project(&self) -> crate::meta::Project { - // For tests, return a project for the current directory - let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - self.project(current_dir.as_path()) + fn project(&self) -> Option { + // Return existing project or create a new one + let mut project_lock = self.project.lock().unwrap(); + if project_lock.is_none() { + let root = &self.project_root; + let interpreter_spec = crate::python::Interpreter::Auto; + let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); + + *project_lock = Some(crate::meta::Project::new( + self, + root.to_string_lossy().to_string(), + interpreter_spec, + django_settings, + 0, + )); + } + project_lock.clone() } fn inspector_pool(&self) -> Arc { @@ -654,16 +657,21 @@ mod tests { // Create a mock venv let venv_prefix = create_mock_venv(venv_dir.path(), None); - // Create a metadata instance with project path and explicit venv path - let metadata = - ProjectMetadata::new(project_dir.path().to_path_buf(), Some(venv_prefix.clone())); + // Create a TestDatabase with the project root + let mut db = TestDatabase::new(project_dir.path().to_path_buf()); - // Create a TestDatabase with the metadata - let db = TestDatabase::new(metadata); + // Create and configure the project with the venv path + let project = crate::meta::Project::new( + &db, + project_dir.path().to_string_lossy().to_string(), + crate::python::Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()), + None, + 0, + ); + db.set_project(project); - // Call the new tracked function - let project = db.project(project_dir.path()); - let env = crate::python_environment(&db, project); + // Call the tracked function + let env = crate::python_environment(&db); // Verify we found the environment assert!(env.is_some(), "Should find environment via salsa db"); @@ -691,19 +699,15 @@ mod tests { // Create a .venv in the project directory let venv_prefix = create_mock_venv(&project_dir.path().join(".venv"), None); - // 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 TestDatabase with the metadata - let db = TestDatabase::new(metadata); + // Create a TestDatabase with the project root + let db = TestDatabase::new(project_dir.path().to_path_buf()); // Mock to ensure VIRTUAL_ENV is not set let _guard = system::mock::MockGuard; system::mock::remove_env_var("VIRTUAL_ENV"); - // Call the new tracked function - let project = db.project(project_dir.path()); - let env = crate::python_environment(&db, project); + // Call the tracked function (should find .venv) + let env = crate::python_environment(&db); // Verify we found the environment assert!( diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index b6f00039..a30b2162 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -7,14 +7,12 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; -#[cfg(test)] use std::sync::Mutex; use dashmap::DashMap; +use djls_project::inspector::pool::InspectorPool; use djls_project::Db as ProjectDb; use djls_project::Project; -use djls_project::TemplateTags; -use djls_project::inspector::pool::InspectorPool; use djls_templates::db::Db as TemplateDb; use djls_templates::templatetags::TagSpecs; use djls_workspace::db::Db as WorkspaceDb; @@ -38,8 +36,8 @@ pub struct DjangoDatabase { /// Maps paths to [`SourceFile`] entities for O(1) lookup. files: Arc>, - /// Unified project inputs cache - maps root path to Project input - projects: Arc>, + /// The single project for this database instance + project: Arc>>, /// Shared inspector pool for executing Python queries inspector_pool: Arc, @@ -61,7 +59,7 @@ impl Default for DjangoDatabase { Self { fs: Arc::new(InMemoryFileSystem::new()), files: Arc::new(DashMap::new()), - projects: Arc::new(DashMap::new()), + project: Arc::new(Mutex::new(None)), inspector_pool: Arc::new(InspectorPool::new()), storage: salsa::Storage::new(Some(Box::new({ let logs = logs.clone(); @@ -82,15 +80,27 @@ impl Default for DjangoDatabase { } impl DjangoDatabase { + /// Set the project for this database instance + pub fn set_project(&self, root: &Path) { + let interpreter = djls_project::Interpreter::Auto; + let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); + + let project = Project::new( + self, + root.to_string_lossy().to_string(), + interpreter, + django_settings, + 0, + ); + + *self.project.lock().unwrap() = Some(project); + } /// Create a new [`DjangoDatabase`] with the given file system and file map. - pub fn new( - file_system: Arc, - files: Arc>, - ) -> Self { + pub fn new(file_system: Arc, files: Arc>) -> Self { Self { fs: file_system, files, - projects: Arc::new(DashMap::new()), + project: Arc::new(Mutex::new(None)), inspector_pool: Arc::new(InspectorPool::new()), storage: salsa::Storage::new(None), #[cfg(test)] @@ -169,7 +179,8 @@ impl WorkspaceDb for DjangoDatabase { #[salsa::db] impl TemplateDb for DjangoDatabase { fn tag_specs(&self) -> Arc { - let project_root_buf = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + let project_root_buf = + std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); let project_root = project_root_buf.as_path(); if let Ok(user_specs) = TagSpecs::load_user_specs(project_root) { @@ -189,35 +200,8 @@ impl TemplateDb for DjangoDatabase { #[salsa::db] impl ProjectDb for DjangoDatabase { - fn template_tags(&self) -> Option> { - djls_project::template_tags(self).map(Arc::new) - } - - fn project(&self, root: &Path) -> Project { - let root_buf = root.to_path_buf(); - - // Use entry API for atomic check-and-insert to avoid race conditions - *self.projects.entry(root_buf.clone()).or_insert_with(|| { - // Create a new Project input with complete configuration - let interpreter_spec = djls_project::Interpreter::Auto; - let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); - - Project::new( - self, - root.to_string_lossy().to_string(), - None, // No venv by default - interpreter_spec, - django_settings, - 0, - ) - }) - } - - fn current_project(&self) -> Project { - // For now, return a project for the current directory - // In a real implementation, this might be configurable - let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - self.project(current_dir.as_path()) + fn project(&self) -> Option { + self.project.lock().unwrap().clone() } fn inspector_pool(&self) -> Arc { diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index c93cfaa1..6dc9f5eb 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -2,10 +2,12 @@ use std::future::Future; use std::path::PathBuf; use std::sync::Arc; +use djls_project::Db as ProjectDb; use djls_templates::analyze_template; use djls_templates::TemplateDiagnostic; use djls_workspace::paths; use djls_workspace::FileKind; +use salsa::Setter; use tokio::sync::Mutex; use tower_lsp_server::jsonrpc::Result as LspResult; use tower_lsp_server::lsp_types; @@ -368,8 +370,7 @@ impl LanguageServer for DjangoLanguageServer { let position = params.text_document_position.position; let encoding = session.position_encoding(); let file_kind = FileKind::from_path(&path); - let template_tags_arc = session.with_db(djls_project::Db::template_tags); - let template_tags = template_tags_arc.as_ref().map(std::convert::AsRef::as_ref); + let template_tags = session.with_db(|db| djls_project::get_templatetags(db)); let tag_specs = session.with_db(djls_templates::Db::tag_specs); let supports_snippets = session.supports_snippets(); @@ -378,7 +379,7 @@ impl LanguageServer for DjangoLanguageServer { position, encoding, file_kind, - template_tags, + template_tags.as_ref(), Some(&tag_specs), supports_snippets, ); @@ -474,23 +475,27 @@ impl LanguageServer for DjangoLanguageServer { async fn did_change_configuration(&self, _params: lsp_types::DidChangeConfigurationParams) { tracing::info!("Configuration change detected. Reloading settings..."); - let project_path = self - .with_session_mut(|session| { - let project = session.project(); - Some(PathBuf::from(project.root(session.database()).as_str())) - }) - .await; - - if let Some(path) = project_path { - self.with_session_mut(|session| match djls_conf::Settings::new(path.as_path()) { - Ok(new_settings) => { - session.update_session_state(new_settings); - } - Err(e) => { - tracing::error!("Error loading settings: {}", e); + self.with_session_mut(|session| { + if let Some(project) = session.project() { + let project_root = PathBuf::from(project.root(session.database()).as_str()); + + match djls_conf::Settings::new(project_root.as_path()) { + Ok(new_settings) => { + session.set_settings(new_settings); + // Invalidate Salsa cache after settings change + session.with_db_mut(|db| { + if let Some(project) = db.project() { + let current_rev = project.revision(db); + project.set_revision(db).to(current_rev + 1); + } + }); + } + Err(e) => { + tracing::error!("Error loading settings: {}", e); + } } - }) - .await; - } + } + }) + .await; } } diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index bac7fd30..c458ee91 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -6,11 +6,9 @@ use std::path::PathBuf; use std::sync::Arc; -use anyhow::Result; use dashmap::DashMap; use djls_conf::Settings; use djls_project::Db as ProjectDb; -use djls_project::TemplateTags; use djls_workspace::db::SourceFile; use djls_workspace::paths; use djls_workspace::PositionEncoding; @@ -22,25 +20,6 @@ use url::Url; use crate::db::DjangoDatabase; -/// Complete LSP session configuration as a Salsa input. -/// -/// This contains all external session state including client capabilities, -/// workspace configuration, and server settings. -#[salsa::input] -pub struct SessionState { - /// The project root path - #[returns(ref)] - pub project_root: Option>, - /// Client capabilities negotiated during initialization - pub client_capabilities: lsp_types::ClientCapabilities, - /// Position encoding negotiated with client - pub position_encoding: djls_workspace::PositionEncoding, - /// Server settings from configuration - pub server_settings: djls_conf::Settings, - /// Revision number for invalidation tracking - pub revision: u64, -} - /// LSP Session managing project-specific state and database operations. /// /// The Session serves as the main entry point for LSP operations, managing: @@ -69,12 +48,6 @@ pub struct Session { /// The Salsa database for incremental computation db: DjangoDatabase, - - /// Session state input - complete LSP session configuration - state: Option, - - /// Cached template tags - Session is the Arc boundary - template_tags: Option>, } impl Session { @@ -102,40 +75,30 @@ impl Session { let files = Arc::new(DashMap::new()); let mut db = DjangoDatabase::new(workspace.file_system(), files); - // Create the session state input - let project_root = project_path - .as_ref() - .and_then(|p| p.to_str()) - .map(Arc::from); - let session_state = SessionState::new( - &db, - project_root, - params.capabilities.clone(), - PositionEncoding::negotiate(params), - settings.clone(), - 0, - ); - // Initialize the project input with correct interpreter spec from settings if let Some(root_path) = &project_path { - let project = db.project(root_path); + // Set the project in the database + db.set_project(root_path); + + // Get the project to configure it + if let Some(project) = db.project() { + // Update interpreter spec based on VIRTUAL_ENV if available + if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") { + let interpreter = djls_project::Interpreter::VenvPath(virtual_env); + project.set_interpreter(&mut db).to(interpreter); + } - // Update interpreter spec based on VIRTUAL_ENV if available - if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") { - let interpreter = djls_project::Interpreter::VenvPath(virtual_env); - project.set_interpreter(&mut db).to(interpreter); - } + // Update Django settings module override if available + if let Ok(settings_module) = std::env::var("DJANGO_SETTINGS_MODULE") { + project + .set_settings_module(&mut db) + .to(Some(settings_module)); + } - // Update Django settings module override if available - if let Ok(settings_module) = std::env::var("DJANGO_SETTINGS_MODULE") { - project - .set_settings_module(&mut db) - .to(Some(settings_module)); + // Bump revision to invalidate dependent queries + let current_rev = project.revision(&db); + project.set_revision(&mut db).to(current_rev + 1); } - - // Bump revision to invalidate dependent queries - let current_rev = project.revision(&db); - project.set_revision(&mut db).to(current_rev + 1); } Self { @@ -144,29 +107,9 @@ impl Session { client_capabilities: params.capabilities.clone(), position_encoding: PositionEncoding::negotiate(params), db, - state: Some(session_state), - template_tags: None, } } - /// Refresh Django data for the project (template tags, etc.) - /// - /// This method caches the template tags in the Session (Arc boundary) - /// and warms up other tracked functions. - pub fn refresh_django_data(&mut self) -> Result<()> { - // Get the unified project input - let project = self.project(); - - // Cache template tags in Session (Arc boundary) - self.template_tags = djls_project::template_tags(&self.db).map(Arc::new); - - // Warm up other tracked functions - let _ = djls_project::django_available(&self.db); - let _ = djls_project::django_settings_module(&self.db); - - Ok(()) - } - #[must_use] pub fn db(&self) -> &DjangoDatabase { &self.db @@ -219,13 +162,17 @@ impl Session { &self.db } - /// Initialize the project with the database. - pub fn initialize_project(&mut self) -> Result<()> { - // Discover Python environment and update inputs - self.discover_python_environment()?; + /// Get the current project for this session + pub fn project(&self) -> Option { + self.db.project() + } - // Refresh Django data using the new inputs - self.refresh_django_data()?; + /// Initialize the project and refresh Django data + pub fn initialize_project(&mut self) -> anyhow::Result<()> { + // Warm up Django-related tracked functions + let _ = djls_project::django_available(&self.db); + let _ = djls_project::django_settings_module(&self.db); + let _ = djls_project::get_templatetags(&self.db); Ok(()) } @@ -305,79 +252,6 @@ impl Session { self.workspace.get_document(url) } - /// Get the session state input - #[must_use] - pub fn session_state(&self) -> Option { - self.state - } - - /// Update the session state with new settings - pub fn update_session_state(&mut self, new_settings: Settings) { - if let Some(session_state) = self.state { - // Update the settings in the input - session_state - .set_server_settings(&mut self.db) - .to(new_settings.clone()); - // Bump revision to invalidate dependent queries - let current_rev = session_state.revision(&self.db); - session_state.set_revision(&mut self.db).to(current_rev + 1); - } - self.settings = new_settings; - } - - /// Get or create unified project input for the current project - pub fn project(&mut self) -> djls_project::Project { - let project_root_buf = if let Some(state) = &self.state { - if let Some(root) = state.project_root(&self.db) { - PathBuf::from(root.as_ref()) - } else { - // Fall back to current directory - std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) - } - } else { - // Fall back to current directory - std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) - }; - let project_root = project_root_buf.as_path(); - self.db.project(project_root) - } - - /// Update project configuration when settings change - pub fn update_project_config( - &mut self, - new_venv_path: Option, - new_django_settings: Option, - ) { - let project = self.project(); - - // Update the interpreter spec if venv path is provided - if let Some(venv_path) = new_venv_path { - let interpreter_spec = - djls_project::Interpreter::VenvPath(venv_path.to_string_lossy().to_string()); - project.set_interpreter(&mut self.db).to(interpreter_spec); - } - - // Update Django settings override if provided - if let Some(settings) = new_django_settings { - project.set_settings_module(&mut self.db).to(Some(settings)); - } - - // Bump revision to invalidate dependent queries - let current_rev = project.revision(&self.db); - project.set_revision(&mut self.db).to(current_rev + 1); - } - - /// Discover and update Python environment state - pub fn discover_python_environment(&mut self) -> Result<()> { - let project = self.project(); - - // Use the new tracked functions to ensure environment discovery - let _interpreter_path = djls_project::resolve_interpreter(&self.db, project); - let _env = djls_project::python_environment(&self.db, project); - - Ok(()) - } - /// Get or create a file in the database. pub fn get_or_create_file(&mut self, path: &PathBuf) -> SourceFile { self.db.get_or_create_file(path) @@ -395,22 +269,11 @@ impl Session { .and_then(|td| td.diagnostic.as_ref()) .is_some() } - - /// Get template tags for the current project. - /// - /// Returns a reference to the cached template tags, or None if not available. - /// Session acts as the Arc boundary, so this returns a borrow. - #[must_use] - pub fn template_tags(&self) -> Option<&TemplateTags> { - self.template_tags.as_deref() - } } impl Default for Session { fn default() -> Self { - let mut session = Self::new(&lsp_types::InitializeParams::default()); - session.state = None; // Default session has no state - session + Self::new(&lsp_types::InitializeParams::default()) } } From 8611b4169e6eaa0f7472bce9c7a4429cc4b41910 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 18:48:04 -0500 Subject: [PATCH 05/17] tweaks --- crates/djls-project/src/db.rs | 6 ++++ crates/djls-project/src/django.rs | 14 +++------ .../djls-project/src/django/templatetags.rs | 3 +- crates/djls-project/src/inspector.rs | 31 ++++--------------- crates/djls-project/src/meta.rs | 2 -- crates/djls-project/src/python.rs | 8 ++--- crates/djls-server/src/db.rs | 3 +- crates/djls-server/src/server.rs | 9 ------ crates/djls-server/src/session.rs | 4 --- 9 files changed, 20 insertions(+), 60 deletions(-) diff --git a/crates/djls-project/src/db.rs b/crates/djls-project/src/db.rs index 4bf3ee72..513c2cf0 100644 --- a/crates/djls-project/src/db.rs +++ b/crates/djls-project/src/db.rs @@ -10,6 +10,7 @@ //! - Tracked functions compute derived values (Python env, Django config) //! - Database trait provides stable configuration (metadata, template tags) +use std::path::Path; use std::sync::Arc; use djls_workspace::Db as WorkspaceDb; @@ -25,4 +26,9 @@ pub trait Db: WorkspaceDb { /// Get the shared inspector pool for executing Python queries fn inspector_pool(&self) -> Arc; + + /// Get the project root path if a project is set + fn project_path(&self) -> Option<&Path> { + self.project().map(|p| Path::new(p.root(self))) + } } diff --git a/crates/djls-project/src/django.rs b/crates/djls-project/src/django.rs index 52c98afb..9f1a0c49 100644 --- a/crates/djls-project/src/django.rs +++ b/crates/djls-project/src/django.rs @@ -1,7 +1,5 @@ mod templatetags; -use std::path::Path; - use crate::db::Db as ProjectDb; use crate::inspector::inspector_run; use crate::inspector::queries::InspectorQueryKind; @@ -16,12 +14,8 @@ pub use templatetags::TemplateTags; /// First consults the inspector, then falls back to environment detection. #[salsa::tracked] pub fn django_available(db: &dyn ProjectDb) -> bool { - let Some(project) = db.project() else { - return false; - }; - // First try to get Django availability from inspector - if let Some(json_data) = inspector_run(db, project, InspectorQueryKind::DjangoAvailable) { + if let Some(json_data) = inspector_run(db, InspectorQueryKind::DjangoAvailable) { // Parse the JSON response - expect a boolean if let Ok(available) = serde_json::from_str::(&json_data) { return available; @@ -39,8 +33,6 @@ pub fn django_available(db: &dyn ProjectDb) -> bool { #[salsa::tracked] pub fn django_settings_module(db: &dyn ProjectDb) -> Option { let project = db.project()?; - let _ = project.revision(db); - let project_path = Path::new(project.root(db)); // Check project override first if let Some(settings) = project.settings_module(db) { @@ -48,13 +40,15 @@ pub fn django_settings_module(db: &dyn ProjectDb) -> Option { } // Try to get settings module from inspector - if let Some(json_data) = inspector_run(db, project, InspectorQueryKind::SettingsModule) { + if let Some(json_data) = inspector_run(db, InspectorQueryKind::SettingsModule) { // Parse the JSON response - expect a string if let Ok(settings) = serde_json::from_str::(&json_data) { return Some(settings); } } + let project_path = db.project_path()?; + // Try to detect settings module if project_path.join("manage.py").exists() { // Look for common settings modules diff --git a/crates/djls-project/src/django/templatetags.rs b/crates/djls-project/src/django/templatetags.rs index 66873854..23041574 100644 --- a/crates/djls-project/src/django/templatetags.rs +++ b/crates/djls-project/src/django/templatetags.rs @@ -14,8 +14,7 @@ use crate::inspector::queries::InspectorQueryKind; /// and parses the JSON response into a TemplateTags struct. #[salsa::tracked] pub fn get_templatetags(db: &dyn ProjectDb) -> Option { - let project = db.project()?; - let json_str = inspector_run(db, project, InspectorQueryKind::TemplateTags)?; + let json_str = inspector_run(db, InspectorQueryKind::TemplateTags)?; // Parse the JSON string into a Value first let json_value: serde_json::Value = match serde_json::from_str(&json_str) { diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index 1fa85897..46ac644d 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -3,15 +3,11 @@ pub mod pool; pub mod queries; mod zipapp; -use std::path::Path; - use serde::Deserialize; use serde::Serialize; use crate::db::Db as ProjectDb; -use crate::meta::Project; use crate::python::python_environment; -use crate::python::resolve_interpreter; use queries::InspectorQueryKind; pub use queries::Query; @@ -32,37 +28,22 @@ pub struct DjlsResponse { /// /// This tracked function executes inspector queries through the shared pool /// and caches the results based on project state and query kind. -#[allow(clippy::drop_non_drop)] -#[salsa::tracked] -pub fn inspector_run( - db: &dyn ProjectDb, - project: Project, - kind: InspectorQueryKind, -) -> Option { - // Create dependency on project revision - let _ = project.revision(db); - - // Get interpreter path - required for inspector - let _interpreter_path = resolve_interpreter(db)?; - let project_path = Path::new(project.root(db)); - - // Get Python environment for inspector +pub fn inspector_run(db: &dyn ProjectDb, kind: InspectorQueryKind) -> Option { let python_env = python_environment(db)?; + let project_path = db.project_path()?; - // Create the appropriate query based on kind let query = match kind { InspectorQueryKind::TemplateTags => crate::inspector::Query::Templatetags, InspectorQueryKind::DjangoAvailable | InspectorQueryKind::SettingsModule => { crate::inspector::Query::DjangoInit } }; - let request = crate::inspector::DjlsRequest { query }; - // Use the shared inspector pool from the database - let pool = db.inspector_pool(); - - match pool.query(&python_env, project_path, &request) { + match db + .inspector_pool() + .query(&python_env, project_path, &request) + { Ok(response) => { if response.ok { if let Some(data) = response.data { diff --git a/crates/djls-project/src/meta.rs b/crates/djls-project/src/meta.rs index a0781dd6..0c2ed5be 100644 --- a/crates/djls-project/src/meta.rs +++ b/crates/djls-project/src/meta.rs @@ -16,6 +16,4 @@ pub struct Project { /// Optional Django settings module override from configuration #[returns(ref)] pub settings_module: Option, - /// Revision number for invalidation tracking - pub revision: u64, } diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 8d6f8e36..323e3849 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -27,10 +27,6 @@ pub enum Interpreter { #[salsa::tracked] pub fn resolve_interpreter(db: &dyn ProjectDb) -> Option { let project = db.project()?; - // Create dependency on project revision - let _ = project.revision(db); - - let project_path = Path::new(project.root(db)); match &project.interpreter(db) { Interpreter::InterpreterPath(path) => { @@ -59,7 +55,7 @@ pub fn resolve_interpreter(db: &dyn ProjectDb) -> Option { Interpreter::Auto => { // Try common venv directories for venv_dir in &[".venv", "venv", "env", ".env"] { - let potential_venv = project_path.join(venv_dir); + let potential_venv = db.project_path()?.join(venv_dir); if potential_venv.is_dir() { #[cfg(unix)] let interpreter_path = potential_venv.join("bin").join("python"); @@ -599,7 +595,7 @@ mod tests { fs: Arc::new(InMemoryFileSystem::new()), } } - + fn set_project(&self, project: crate::meta::Project) { *self.project.lock().unwrap() = Some(project); } diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index a30b2162..a6847806 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -90,7 +90,6 @@ impl DjangoDatabase { root.to_string_lossy().to_string(), interpreter, django_settings, - 0, ); *self.project.lock().unwrap() = Some(project); @@ -201,7 +200,7 @@ impl TemplateDb for DjangoDatabase { #[salsa::db] impl ProjectDb for DjangoDatabase { fn project(&self) -> Option { - self.project.lock().unwrap().clone() + *self.project.lock().unwrap() } fn inspector_pool(&self) -> Arc { diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 6dc9f5eb..046fe1a3 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -2,12 +2,10 @@ use std::future::Future; use std::path::PathBuf; use std::sync::Arc; -use djls_project::Db as ProjectDb; use djls_templates::analyze_template; use djls_templates::TemplateDiagnostic; use djls_workspace::paths; use djls_workspace::FileKind; -use salsa::Setter; use tokio::sync::Mutex; use tower_lsp_server::jsonrpc::Result as LspResult; use tower_lsp_server::lsp_types; @@ -482,13 +480,6 @@ impl LanguageServer for DjangoLanguageServer { match djls_conf::Settings::new(project_root.as_path()) { Ok(new_settings) => { session.set_settings(new_settings); - // Invalidate Salsa cache after settings change - session.with_db_mut(|db| { - if let Some(project) = db.project() { - let current_rev = project.revision(db); - project.set_revision(db).to(current_rev + 1); - } - }); } Err(e) => { tracing::error!("Error loading settings: {}", e); diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index c458ee91..ef3ad2c1 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -94,10 +94,6 @@ impl Session { .set_settings_module(&mut db) .to(Some(settings_module)); } - - // Bump revision to invalidate dependent queries - let current_rev = project.revision(&db); - project.set_revision(&mut db).to(current_rev + 1); } } From 9949869f8cbf522274e9d027f0431d2b77fa3ce0 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 18:49:08 -0500 Subject: [PATCH 06/17] clippy --- crates/djls-project/src/python.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 323e3849..f672ce79 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -566,15 +566,15 @@ mod tests { } mod salsa_integration { + use super::*; + use std::sync::Arc; + use std::sync::Mutex; use djls_workspace::FileSystem; use djls_workspace::InMemoryFileSystem; - use super::*; use crate::db::Db as ProjectDb; - use salsa::Setter; - use std::sync::Mutex; /// Test implementation of ProjectDb for unit tests #[salsa::db] @@ -617,10 +617,6 @@ mod tests { #[salsa::db] impl ProjectDb for TestDatabase { - fn template_tags(&self) -> Option> { - None - } - fn project(&self) -> Option { // Return existing project or create a new one let mut project_lock = self.project.lock().unwrap(); @@ -634,7 +630,6 @@ mod tests { root.to_string_lossy().to_string(), interpreter_spec, django_settings, - 0, )); } project_lock.clone() @@ -654,7 +649,7 @@ mod tests { let venv_prefix = create_mock_venv(venv_dir.path(), None); // Create a TestDatabase with the project root - let mut db = TestDatabase::new(project_dir.path().to_path_buf()); + let db = TestDatabase::new(project_dir.path().to_path_buf()); // Create and configure the project with the venv path let project = crate::meta::Project::new( @@ -662,7 +657,6 @@ mod tests { project_dir.path().to_string_lossy().to_string(), crate::python::Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()), None, - 0, ); db.set_project(project); From aafa0ce552f75e9eb61016f529d9d17c59f2e452 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 18:49:30 -0500 Subject: [PATCH 07/17] clippy --- crates/djls-project/src/python.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index f672ce79..1fbed25d 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -632,7 +632,7 @@ mod tests { django_settings, )); } - project_lock.clone() + *project_lock } fn inspector_pool(&self) -> Arc { From 0634ee7ff671bb9632e5eab51d092e7f61eb96e2 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 18:49:36 -0500 Subject: [PATCH 08/17] fmt --- crates/djls-project/src/django.rs | 6 +++--- crates/djls-project/src/inspector.rs | 4 ++-- crates/djls-project/src/python.rs | 7 ++++--- crates/djls-project/src/system.rs | 4 +++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/djls-project/src/django.rs b/crates/djls-project/src/django.rs index 9f1a0c49..11d8a517 100644 --- a/crates/djls-project/src/django.rs +++ b/crates/djls-project/src/django.rs @@ -1,13 +1,13 @@ mod templatetags; +pub use templatetags::get_templatetags; +pub use templatetags::TemplateTags; + use crate::db::Db as ProjectDb; use crate::inspector::inspector_run; use crate::inspector::queries::InspectorQueryKind; use crate::python::python_environment; -pub use templatetags::get_templatetags; -pub use templatetags::TemplateTags; - /// Check if Django is available for the current project. /// /// This determines if Django is installed and configured in the Python environment. diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index 46ac644d..b0e17d7d 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -3,13 +3,13 @@ pub mod pool; pub mod queries; mod zipapp; +use queries::InspectorQueryKind; +pub use queries::Query; use serde::Deserialize; use serde::Serialize; use crate::db::Db as ProjectDb; use crate::python::python_environment; -use queries::InspectorQueryKind; -pub use queries::Query; #[derive(Serialize)] pub struct DjlsRequest { diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 1fbed25d..10cc33d7 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -274,7 +274,9 @@ mod tests { use super::*; use crate::system::mock::MockGuard; - use crate::system::mock::{self as sys_mock}; + use crate::system::mock::{ + self as sys_mock, + }; #[test] fn test_explicit_venv_path_found() { @@ -566,14 +568,13 @@ mod tests { } mod salsa_integration { - use super::*; - use std::sync::Arc; use std::sync::Mutex; use djls_workspace::FileSystem; use djls_workspace::InMemoryFileSystem; + use super::*; use crate::db::Db as ProjectDb; /// Test implementation of ProjectDb for unit tests diff --git a/crates/djls-project/src/system.rs b/crates/djls-project/src/system.rs index 88269e38..167e51c6 100644 --- a/crates/djls-project/src/system.rs +++ b/crates/djls-project/src/system.rs @@ -103,7 +103,9 @@ mod tests { use which::Error as WhichError; use super::mock::MockGuard; - use super::mock::{self as sys_mock}; + use super::mock::{ + self as sys_mock, + }; use super::*; #[test] From b1efb752dc0988d98ae3cfb8a613d0c167f8b984 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 18:50:55 -0500 Subject: [PATCH 09/17] clippy panics --- crates/djls-server/src/db.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index a6847806..0f1bcfd2 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -81,6 +81,10 @@ impl Default for DjangoDatabase { impl DjangoDatabase { /// Set the project for this database instance + /// + /// # Panics + /// + /// Panics if the project mutex is poisoned. pub fn set_project(&self, root: &Path) { let interpreter = djls_project::Interpreter::Auto; let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); From d044f25878798b44a9e8445df93f53fa8cd3eec3 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 21:51:23 -0500 Subject: [PATCH 10/17] rename somethings --- crates/djls-project/src/db.rs | 4 +-- crates/djls-project/src/django.rs | 19 +++++------ .../djls-project/src/django/templatetags.rs | 7 ++-- crates/djls-project/src/inspector.rs | 18 +++------- crates/djls-project/src/inspector/queries.rs | 28 ++++++--------- crates/djls-project/src/lib.rs | 6 ++-- .../djls-project/src/{meta.rs => project.rs} | 4 ++- crates/djls-project/src/python.rs | 34 ++++++++----------- crates/djls-server/src/db.rs | 7 +--- crates/djls-server/src/server.rs | 12 +++++-- crates/djls-server/src/session.rs | 8 +++-- crates/djls-workspace/src/db.rs | 2 ++ 12 files changed, 68 insertions(+), 81 deletions(-) rename crates/djls-project/src/{meta.rs => project.rs} (86%) diff --git a/crates/djls-project/src/db.rs b/crates/djls-project/src/db.rs index 513c2cf0..c1bce1f2 100644 --- a/crates/djls-project/src/db.rs +++ b/crates/djls-project/src/db.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use djls_workspace::Db as WorkspaceDb; use crate::inspector::pool::InspectorPool; -use crate::meta::Project; +use crate::project::Project; /// Project-specific database trait extending the workspace database #[salsa::db] @@ -29,6 +29,6 @@ pub trait Db: WorkspaceDb { /// Get the project root path if a project is set fn project_path(&self) -> Option<&Path> { - self.project().map(|p| Path::new(p.root(self))) + self.project().map(|p| p.root(self).as_path()) } } diff --git a/crates/djls-project/src/django.rs b/crates/djls-project/src/django.rs index 11d8a517..87a6acc4 100644 --- a/crates/djls-project/src/django.rs +++ b/crates/djls-project/src/django.rs @@ -5,17 +5,18 @@ pub use templatetags::TemplateTags; use crate::db::Db as ProjectDb; use crate::inspector::inspector_run; -use crate::inspector::queries::InspectorQueryKind; +use crate::inspector::queries::Query; use crate::python::python_environment; +use crate::Project; /// Check if Django is available for the current project. /// /// This determines if Django is installed and configured in the Python environment. /// First consults the inspector, then falls back to environment detection. #[salsa::tracked] -pub fn django_available(db: &dyn ProjectDb) -> bool { +pub fn django_available(db: &dyn ProjectDb, project: Project) -> bool { // First try to get Django availability from inspector - if let Some(json_data) = inspector_run(db, InspectorQueryKind::DjangoAvailable) { + if let Some(json_data) = inspector_run(db, Query::DjangoInit) { // Parse the JSON response - expect a boolean if let Ok(available) = serde_json::from_str::(&json_data) { return available; @@ -23,7 +24,7 @@ pub fn django_available(db: &dyn ProjectDb) -> bool { } // Fallback to environment detection - python_environment(db).is_some() + python_environment(db, project).is_some() } /// Get the Django settings module name for the current project. @@ -31,30 +32,28 @@ pub fn django_available(db: &dyn ProjectDb) -> bool { /// Returns the settings_module_override from project, or inspector result, /// or DJANGO_SETTINGS_MODULE env var, or attempts to detect it. #[salsa::tracked] -pub fn django_settings_module(db: &dyn ProjectDb) -> Option { - let project = db.project()?; - +pub fn django_settings_module(db: &dyn ProjectDb, project: Project) -> Option { // Check project override first if let Some(settings) = project.settings_module(db) { return Some(settings.clone()); } // Try to get settings module from inspector - if let Some(json_data) = inspector_run(db, InspectorQueryKind::SettingsModule) { + if let Some(json_data) = inspector_run(db, Query::DjangoInit) { // Parse the JSON response - expect a string if let Ok(settings) = serde_json::from_str::(&json_data) { return Some(settings); } } - let project_path = db.project_path()?; + let project_path = project.root(db); // Try to detect settings module if project_path.join("manage.py").exists() { // Look for common settings modules for candidate in &["settings", "config.settings", "project.settings"] { let parts: Vec<&str> = candidate.split('.').collect(); - let mut path = project_path.to_path_buf(); + let mut path = project_path.clone(); for part in &parts[..parts.len() - 1] { path = path.join(part); } diff --git a/crates/djls-project/src/django/templatetags.rs b/crates/djls-project/src/django/templatetags.rs index 23041574..9f731bab 100644 --- a/crates/djls-project/src/django/templatetags.rs +++ b/crates/djls-project/src/django/templatetags.rs @@ -6,15 +6,16 @@ use serde_json::Value; use crate::db::Db as ProjectDb; use crate::inspector::inspector_run; -use crate::inspector::queries::InspectorQueryKind; +use crate::inspector::queries::Query; +use crate::Project; /// Get template tags for the current project by querying the inspector. /// /// This tracked function calls the inspector to retrieve Django template tags /// and parses the JSON response into a TemplateTags struct. #[salsa::tracked] -pub fn get_templatetags(db: &dyn ProjectDb) -> Option { - let json_str = inspector_run(db, InspectorQueryKind::TemplateTags)?; +pub fn get_templatetags(db: &dyn ProjectDb, _project: Project) -> Option { + let json_str = inspector_run(db, Query::Templatetags)?; // Parse the JSON string into a Value first let json_value: serde_json::Value = match serde_json::from_str(&json_str) { diff --git a/crates/djls-project/src/inspector.rs b/crates/djls-project/src/inspector.rs index b0e17d7d..e6cd2dd5 100644 --- a/crates/djls-project/src/inspector.rs +++ b/crates/djls-project/src/inspector.rs @@ -3,7 +3,6 @@ pub mod pool; pub mod queries; mod zipapp; -use queries::InspectorQueryKind; pub use queries::Query; use serde::Deserialize; use serde::Serialize; @@ -28,21 +27,14 @@ pub struct DjlsResponse { /// /// This tracked function executes inspector queries through the shared pool /// and caches the results based on project state and query kind. -pub fn inspector_run(db: &dyn ProjectDb, kind: InspectorQueryKind) -> Option { - let python_env = python_environment(db)?; - let project_path = db.project_path()?; - - let query = match kind { - InspectorQueryKind::TemplateTags => crate::inspector::Query::Templatetags, - InspectorQueryKind::DjangoAvailable | InspectorQueryKind::SettingsModule => { - crate::inspector::Query::DjangoInit - } - }; - let request = crate::inspector::DjlsRequest { query }; +pub fn inspector_run(db: &dyn ProjectDb, query: Query) -> Option { + let project = db.project()?; + let python_env = python_environment(db, project)?; + let project_path = project.root(db); match db .inspector_pool() - .query(&python_env, project_path, &request) + .query(&python_env, project_path, &DjlsRequest { query }) { Ok(response) => { if response.ok { diff --git a/crates/djls-project/src/inspector/queries.rs b/crates/djls-project/src/inspector/queries.rs index 6fb38420..ca34705a 100644 --- a/crates/djls-project/src/inspector/queries.rs +++ b/crates/djls-project/src/inspector/queries.rs @@ -3,21 +3,13 @@ use std::path::PathBuf; use serde::Deserialize; use serde::Serialize; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Copy)] #[serde(tag = "query", content = "args")] #[serde(rename_all = "snake_case")] pub enum Query { + DjangoInit, PythonEnv, Templatetags, - DjangoInit, -} - -/// Enum representing different kinds of inspector queries for Salsa tracking -#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] -pub enum InspectorQueryKind { - TemplateTags, - DjangoAvailable, - SettingsModule, } #[derive(Serialize, Deserialize)] @@ -56,15 +48,15 @@ mod tests { use super::*; #[test] - fn test_inspector_query_kind_enum() { - // Test that InspectorQueryKind variants exist and are copyable - let template_tags = InspectorQueryKind::TemplateTags; - let django_available = InspectorQueryKind::DjangoAvailable; - let settings_module = InspectorQueryKind::SettingsModule; + fn test_query_enum() { + // Test that Query variants exist and are copyable + let python_env = Query::PythonEnv; + let templatetags = Query::Templatetags; + let django_init = Query::DjangoInit; // Test that they can be copied - assert_eq!(template_tags, InspectorQueryKind::TemplateTags); - assert_eq!(django_available, InspectorQueryKind::DjangoAvailable); - assert_eq!(settings_module, InspectorQueryKind::SettingsModule); + assert_eq!(python_env, Query::PythonEnv); + assert_eq!(templatetags, Query::Templatetags); + assert_eq!(django_init, Query::DjangoInit); } } diff --git a/crates/djls-project/src/lib.rs b/crates/djls-project/src/lib.rs index 63022192..9ccd1ab6 100644 --- a/crates/djls-project/src/lib.rs +++ b/crates/djls-project/src/lib.rs @@ -1,7 +1,7 @@ mod db; mod django; pub mod inspector; -mod meta; +mod project; pub mod python; mod system; @@ -11,8 +11,8 @@ pub use django::django_settings_module; pub use django::get_templatetags; pub use django::TemplateTags; pub use inspector::inspector_run; -pub use inspector::queries::InspectorQueryKind; -pub use meta::Project; +pub use inspector::queries::Query; +pub use project::Project; pub use python::python_environment; pub use python::resolve_interpreter; pub use python::Interpreter; diff --git a/crates/djls-project/src/meta.rs b/crates/djls-project/src/project.rs similarity index 86% rename from crates/djls-project/src/meta.rs rename to crates/djls-project/src/project.rs index 0c2ed5be..fe75dcd0 100644 --- a/crates/djls-project/src/meta.rs +++ b/crates/djls-project/src/project.rs @@ -1,16 +1,18 @@ use crate::python::Interpreter; +use std::path::PathBuf; /// Complete project configuration as a Salsa input. /// /// Following Ruff's pattern, this contains all external project configuration /// rather than minimal keys that everything derives from. This replaces both /// Project input and ProjectMetadata. +// TODO: Add templatetags as a field on this input #[salsa::input] #[derive(Debug)] pub struct Project { /// The project root path #[returns(ref)] - pub root: String, + pub root: PathBuf, /// Interpreter specification for Python environment discovery pub interpreter: Interpreter, /// Optional Django settings module override from configuration diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 10cc33d7..82b45e68 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use crate::db::Db as ProjectDb; use crate::system; +use crate::Project; /// Interpreter specification for Python environment discovery. /// @@ -25,9 +26,7 @@ pub enum Interpreter { /// This tracked function determines the interpreter path based on the project's /// interpreter specification. #[salsa::tracked] -pub fn resolve_interpreter(db: &dyn ProjectDb) -> Option { - let project = db.project()?; - +pub fn resolve_interpreter(db: &dyn ProjectDb, project: Project) -> Option { match &project.interpreter(db) { Interpreter::InterpreterPath(path) => { let path_buf = PathBuf::from(path.as_str()); @@ -55,7 +54,7 @@ pub fn resolve_interpreter(db: &dyn ProjectDb) -> Option { Interpreter::Auto => { // Try common venv directories for venv_dir in &[".venv", "venv", "env", ".env"] { - let potential_venv = db.project_path()?.join(venv_dir); + let potential_venv = project.root(db).join(venv_dir); if potential_venv.is_dir() { #[cfg(unix)] let interpreter_path = potential_venv.join("bin").join("python"); @@ -207,10 +206,9 @@ impl fmt::Display for PythonEnvironment { /// 3. Common venv directories in project root (.venv, venv, env, .env) /// 4. System Python as fallback #[salsa::tracked] -pub fn python_environment(db: &dyn ProjectDb) -> Option> { - let project = db.project()?; - let interpreter_path = resolve_interpreter(db)?; - let project_path = Path::new(project.root(db)); +pub fn python_environment(db: &dyn ProjectDb, project: Project) -> Option> { + let interpreter_path = resolve_interpreter(db, project)?; + let project_path = project.root(db); // For venv paths, we need to determine the venv root let interpreter_spec = project.interpreter(db); @@ -274,9 +272,7 @@ mod tests { use super::*; use crate::system::mock::MockGuard; - use crate::system::mock::{ - self as sys_mock, - }; + use crate::system::mock::{self as sys_mock}; #[test] fn test_explicit_venv_path_found() { @@ -575,7 +571,6 @@ mod tests { use djls_workspace::InMemoryFileSystem; use super::*; - use crate::db::Db as ProjectDb; /// Test implementation of ProjectDb for unit tests #[salsa::db] @@ -583,7 +578,7 @@ mod tests { struct TestDatabase { storage: salsa::Storage, project_root: PathBuf, - project: Arc>>, + project: Arc>>, fs: Arc, } @@ -597,7 +592,7 @@ mod tests { } } - fn set_project(&self, project: crate::meta::Project) { + fn set_project(&self, project: crate::project::Project) { *self.project.lock().unwrap() = Some(project); } } @@ -618,7 +613,7 @@ mod tests { #[salsa::db] impl ProjectDb for TestDatabase { - fn project(&self) -> Option { + fn project(&self) -> Option { // Return existing project or create a new one let mut project_lock = self.project.lock().unwrap(); if project_lock.is_none() { @@ -626,7 +621,7 @@ mod tests { let interpreter_spec = crate::python::Interpreter::Auto; let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); - *project_lock = Some(crate::meta::Project::new( + *project_lock = Some(crate::project::Project::new( self, root.to_string_lossy().to_string(), interpreter_spec, @@ -653,7 +648,7 @@ mod tests { let db = TestDatabase::new(project_dir.path().to_path_buf()); // Create and configure the project with the venv path - let project = crate::meta::Project::new( + let project = crate::project::Project::new( &db, project_dir.path().to_string_lossy().to_string(), crate::python::Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()), @@ -662,7 +657,7 @@ mod tests { db.set_project(project); // Call the tracked function - let env = crate::python_environment(&db); + let env = crate::python_environment(&db, project); // Verify we found the environment assert!(env.is_some(), "Should find environment via salsa db"); @@ -698,7 +693,8 @@ mod tests { system::mock::remove_env_var("VIRTUAL_ENV"); // Call the tracked function (should find .venv) - let env = crate::python_environment(&db); + let project = db.project().unwrap(); + let env = crate::python_environment(&db, project); // Verify we found the environment assert!( diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 0f1bcfd2..7fa2f13e 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -89,12 +89,7 @@ impl DjangoDatabase { let interpreter = djls_project::Interpreter::Auto; let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); - let project = Project::new( - self, - root.to_string_lossy().to_string(), - interpreter, - django_settings, - ); + let project = Project::new(self, root.to_path_buf(), interpreter, django_settings); *self.project.lock().unwrap() = Some(project); } diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 046fe1a3..94356165 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -1,7 +1,7 @@ use std::future::Future; -use std::path::PathBuf; use std::sync::Arc; +use djls_project::Db as ProjectDb; use djls_templates::analyze_template; use djls_templates::TemplateDiagnostic; use djls_workspace::paths; @@ -368,7 +368,13 @@ impl LanguageServer for DjangoLanguageServer { let position = params.text_document_position.position; let encoding = session.position_encoding(); let file_kind = FileKind::from_path(&path); - let template_tags = session.with_db(|db| djls_project::get_templatetags(db)); + let template_tags = session.with_db(|db| { + if let Some(project) = db.project() { + djls_project::get_templatetags(db, project) + } else { + None + } + }); let tag_specs = session.with_db(djls_templates::Db::tag_specs); let supports_snippets = session.supports_snippets(); @@ -475,7 +481,7 @@ impl LanguageServer for DjangoLanguageServer { self.with_session_mut(|session| { if let Some(project) = session.project() { - let project_root = PathBuf::from(project.root(session.database()).as_str()); + let project_root = project.root(session.database()); match djls_conf::Settings::new(project_root.as_path()) { Ok(new_settings) => { diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index ef3ad2c1..2e9185e9 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -166,9 +166,11 @@ impl Session { /// Initialize the project and refresh Django data pub fn initialize_project(&mut self) -> anyhow::Result<()> { // Warm up Django-related tracked functions - let _ = djls_project::django_available(&self.db); - let _ = djls_project::django_settings_module(&self.db); - let _ = djls_project::get_templatetags(&self.db); + if let Some(project) = self.db.project() { + let _ = djls_project::django_available(&self.db, project); + let _ = djls_project::django_settings_module(&self.db, project); + let _ = djls_project::get_templatetags(&self.db, project); + } Ok(()) } diff --git a/crates/djls-workspace/src/db.rs b/crates/djls-workspace/src/db.rs index 846ab4e8..451e9d04 100644 --- a/crates/djls-workspace/src/db.rs +++ b/crates/djls-workspace/src/db.rs @@ -48,6 +48,7 @@ pub trait Db: salsa::Database { pub struct SourceFile { /// The file's classification for analysis routing pub kind: FileKind, + // TODO: Change from Arc to PathBuf for consistency with Project.root /// The file path #[returns(ref)] pub path: Arc, @@ -78,6 +79,7 @@ pub fn source_text(db: &dyn Db, file: SourceFile) -> Arc { /// on files identified by path rather than by SourceFile input. #[salsa::input] pub struct FilePath { + // TODO: Change from Arc to PathBuf for consistency with Project.root /// The file path as a string #[returns(ref)] pub path: Arc, From 1f6544e3b1d32126e1d30ed06eea340df4c82395 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 21:52:58 -0500 Subject: [PATCH 11/17] test fixes --- crates/djls-project/src/python.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 82b45e68..75a0feeb 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -623,7 +623,7 @@ mod tests { *project_lock = Some(crate::project::Project::new( self, - root.to_string_lossy().to_string(), + root.clone(), interpreter_spec, django_settings, )); @@ -650,7 +650,7 @@ mod tests { // Create and configure the project with the venv path let project = crate::project::Project::new( &db, - project_dir.path().to_string_lossy().to_string(), + project_dir.path().to_path_buf(), crate::python::Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()), None, ); From de5ff8d4bf1d5c7dea148af4ff5563cd775a9e6b Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 21:53:13 -0500 Subject: [PATCH 12/17] fmt --- crates/djls-project/src/project.rs | 3 ++- crates/djls-project/src/python.rs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/djls-project/src/project.rs b/crates/djls-project/src/project.rs index fe75dcd0..96780d86 100644 --- a/crates/djls-project/src/project.rs +++ b/crates/djls-project/src/project.rs @@ -1,6 +1,7 @@ -use crate::python::Interpreter; use std::path::PathBuf; +use crate::python::Interpreter; + /// Complete project configuration as a Salsa input. /// /// Following Ruff's pattern, this contains all external project configuration diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 75a0feeb..0cacd51f 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -272,7 +272,9 @@ mod tests { use super::*; use crate::system::mock::MockGuard; - use crate::system::mock::{self as sys_mock}; + use crate::system::mock::{ + self as sys_mock, + }; #[test] fn test_explicit_venv_path_found() { From c87e219c2fa16ea106d4a92d27c95a8e55f0a480 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 22:02:25 -0500 Subject: [PATCH 13/17] simplify exports --- crates/djls-project/src/lib.rs | 10 +++------- crates/djls-project/src/python.rs | 31 +++++++++++++++---------------- crates/djls-server/src/db.rs | 5 +++-- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/crates/djls-project/src/lib.rs b/crates/djls-project/src/lib.rs index 9ccd1ab6..64a7e1b2 100644 --- a/crates/djls-project/src/lib.rs +++ b/crates/djls-project/src/lib.rs @@ -1,8 +1,8 @@ mod db; mod django; -pub mod inspector; +mod inspector; mod project; -pub mod python; +mod python; mod system; pub use db::Db; @@ -10,10 +10,6 @@ pub use django::django_available; pub use django::django_settings_module; pub use django::get_templatetags; pub use django::TemplateTags; -pub use inspector::inspector_run; -pub use inspector::queries::Query; +pub use inspector::pool::InspectorPool; pub use project::Project; -pub use python::python_environment; -pub use python::resolve_interpreter; pub use python::Interpreter; -pub use python::PythonEnvironment; diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 0cacd51f..3910f8ef 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -68,7 +68,7 @@ pub fn resolve_interpreter(db: &dyn ProjectDb, project: Project) -> Option, project_root: PathBuf, - project: Arc>>, + project: Arc>>, fs: Arc, } @@ -594,7 +593,7 @@ mod tests { } } - fn set_project(&self, project: crate::project::Project) { + fn set_project(&self, project: Project) { *self.project.lock().unwrap() = Some(project); } } @@ -615,15 +614,15 @@ mod tests { #[salsa::db] impl ProjectDb for TestDatabase { - fn project(&self) -> Option { + fn project(&self) -> Option { // Return existing project or create a new one let mut project_lock = self.project.lock().unwrap(); if project_lock.is_none() { let root = &self.project_root; - let interpreter_spec = crate::python::Interpreter::Auto; + let interpreter_spec = Interpreter::Auto; let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); - *project_lock = Some(crate::project::Project::new( + *project_lock = Some(Project::new( self, root.clone(), interpreter_spec, @@ -633,8 +632,8 @@ mod tests { *project_lock } - fn inspector_pool(&self) -> Arc { - Arc::new(crate::inspector::pool::InspectorPool::new()) + fn inspector_pool(&self) -> Arc { + Arc::new(InspectorPool::new()) } } @@ -650,16 +649,16 @@ mod tests { let db = TestDatabase::new(project_dir.path().to_path_buf()); // Create and configure the project with the venv path - let project = crate::project::Project::new( + let project = Project::new( &db, project_dir.path().to_path_buf(), - crate::python::Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()), + Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()), None, ); db.set_project(project); // Call the tracked function - let env = crate::python_environment(&db, project); + let env = python_environment(&db, project); // Verify we found the environment assert!(env.is_some(), "Should find environment via salsa db"); @@ -696,7 +695,7 @@ mod tests { // Call the tracked function (should find .venv) let project = db.project().unwrap(); - let env = crate::python_environment(&db, project); + let env = python_environment(&db, project); // Verify we found the environment assert!( diff --git a/crates/djls-server/src/db.rs b/crates/djls-server/src/db.rs index 7fa2f13e..752c97c8 100644 --- a/crates/djls-server/src/db.rs +++ b/crates/djls-server/src/db.rs @@ -10,8 +10,9 @@ use std::sync::Arc; use std::sync::Mutex; use dashmap::DashMap; -use djls_project::inspector::pool::InspectorPool; use djls_project::Db as ProjectDb; +use djls_project::InspectorPool; +use djls_project::Interpreter; use djls_project::Project; use djls_templates::db::Db as TemplateDb; use djls_templates::templatetags::TagSpecs; @@ -86,7 +87,7 @@ impl DjangoDatabase { /// /// Panics if the project mutex is poisoned. pub fn set_project(&self, root: &Path) { - let interpreter = djls_project::Interpreter::Auto; + let interpreter = Interpreter::Auto; let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok(); let project = Project::new(self, root.to_path_buf(), interpreter, django_settings); From 7b95599a7bd740f55a08ad61285be072f63f94f5 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 22:21:08 -0500 Subject: [PATCH 14/17] fix some things --- crates/djls-server/src/server.rs | 2 -- crates/djls-server/src/session.rs | 10 ++-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 94356165..b33e794c 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -225,8 +225,6 @@ impl LanguageServer for DjangoLanguageServer { path_display, e ); - - // Django initialization is now handled lazily through Salsa } } } else { diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 2e9185e9..661c835c 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use dashmap::DashMap; use djls_conf::Settings; use djls_project::Db as ProjectDb; +use djls_project::Interpreter; use djls_workspace::db::SourceFile; use djls_workspace::paths; use djls_workspace::PositionEncoding; @@ -68,27 +69,20 @@ impl Session { Settings::default() }; - // Create workspace for buffer management let workspace = Workspace::new(); - // Create the concrete database with the workspace's file system let files = Arc::new(DashMap::new()); let mut db = DjangoDatabase::new(workspace.file_system(), files); - // Initialize the project input with correct interpreter spec from settings if let Some(root_path) = &project_path { - // Set the project in the database db.set_project(root_path); - // Get the project to configure it if let Some(project) = db.project() { - // Update interpreter spec based on VIRTUAL_ENV if available if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") { - let interpreter = djls_project::Interpreter::VenvPath(virtual_env); + let interpreter = Interpreter::VenvPath(virtual_env); project.set_interpreter(&mut db).to(interpreter); } - // Update Django settings module override if available if let Ok(settings_module) = std::env::var("DJANGO_SETTINGS_MODULE") { project .set_settings_module(&mut db) From 7bb7d7aaea62afad0d151beabf7ce0eb3caa724e Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 22:43:56 -0500 Subject: [PATCH 15/17] move project init arond --- crates/djls-project/src/project.rs | 10 +++++++ crates/djls-server/src/server.rs | 47 ++++++++---------------------- crates/djls-server/src/session.rs | 44 ++++++++++++---------------- 3 files changed, 41 insertions(+), 60 deletions(-) diff --git a/crates/djls-project/src/project.rs b/crates/djls-project/src/project.rs index 96780d86..a6be2939 100644 --- a/crates/djls-project/src/project.rs +++ b/crates/djls-project/src/project.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; +use crate::db::Db as ProjectDb; use crate::python::Interpreter; +use crate::{django_available, django_settings_module, get_templatetags}; /// Complete project configuration as a Salsa input. /// @@ -20,3 +22,11 @@ pub struct Project { #[returns(ref)] pub settings_module: Option, } + +impl Project { + pub fn initialize(self, db: &dyn ProjectDb) { + let _ = django_available(db, self); + let _ = django_settings_module(db, self); + let _ = get_templatetags(db, self); + } +} diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index b33e794c..a5007d70 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -182,54 +182,31 @@ impl LanguageServer for DjangoLanguageServer { }) } - #[allow(clippy::too_many_lines)] async fn initialized(&self, _params: lsp_types::InitializedParams) { tracing::info!("Server received initialized notification."); self.with_session_task(move |session_arc| async move { - let project_path_and_venv = { - let session_lock = session_arc.lock().await; - std::env::current_dir().ok().map(|current_dir| { - ( - current_dir.display().to_string(), - session_lock - .settings() - .venv_path() - .map(std::string::ToString::to_string), - ) - }) - }; + let session_lock = session_arc.lock().await; + + let project_path = session_lock + .project() + .map(|p| p.root(session_lock.database()).clone()); - if let Some((path_display, venv_path)) = project_path_and_venv { + if let Some(path) = project_path { tracing::info!( "Task: Starting initialization for project at: {}", - path_display + path.display() ); - if let Some(ref path) = venv_path { - tracing::info!("Using virtual environment from config: {}", path); + if let Some(project) = session_lock.project() { + project.initialize(session_lock.database()); } - let init_result = { - let mut session_lock = session_arc.lock().await; - session_lock.initialize_project() - }; - - match init_result { - Ok(()) => { - tracing::info!("Task: Successfully initialized project: {}", path_display); - } - Err(e) => { - tracing::error!( - "Task: Failed to initialize Django project at {}: {}", - path_display, - e - ); - } - } + tracing::info!("Task: Successfully initialized project: {}", path.display()); } else { - tracing::info!("Task: No project instance found to initialize."); + tracing::info!("Task: No project configured, skipping initialization."); } + Ok(()) }) .await; diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index 661c835c..0fee0e1f 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -34,6 +34,7 @@ use crate::db::DjangoDatabase; /// and is passed down to operations that need it. pub struct Session { /// LSP server settings + // TODO: this should really be in the database settings: Settings, /// Workspace for buffer and file system management @@ -78,11 +79,16 @@ impl Session { db.set_project(root_path); if let Some(project) = db.project() { - if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") { + // TODO: should this logic live in the project? + if let Some(venv_path) = settings.venv_path() { + let interpreter = Interpreter::VenvPath(venv_path.to_string()); + project.set_interpreter(&mut db).to(interpreter); + } else if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") { let interpreter = Interpreter::VenvPath(virtual_env); project.set_interpreter(&mut db).to(interpreter); } + // TODO: allow for configuring via settings if let Ok(settings_module) = std::env::var("DJANGO_SETTINGS_MODULE") { project .set_settings_module(&mut db) @@ -119,18 +125,6 @@ impl Session { self.position_encoding } - /// Check if the client supports snippet completions - #[must_use] - pub fn supports_snippets(&self) -> bool { - self.client_capabilities - .text_document - .as_ref() - .and_then(|td| td.completion.as_ref()) - .and_then(|c| c.completion_item.as_ref()) - .and_then(|ci| ci.snippet_support) - .unwrap_or(false) - } - /// Execute a read-only operation with access to the database. pub fn with_db(&self, f: F) -> R where @@ -157,18 +151,6 @@ impl Session { self.db.project() } - /// Initialize the project and refresh Django data - pub fn initialize_project(&mut self) -> anyhow::Result<()> { - // Warm up Django-related tracked functions - if let Some(project) = self.db.project() { - let _ = djls_project::django_available(&self.db, project); - let _ = djls_project::django_settings_module(&self.db, project); - let _ = djls_project::get_templatetags(&self.db, project); - } - - Ok(()) - } - /// Open a document in the session. /// /// Updates both the workspace buffers and database. Creates the file in @@ -261,6 +243,18 @@ impl Session { .and_then(|td| td.diagnostic.as_ref()) .is_some() } + + /// Check if the client supports snippet completions + #[must_use] + pub fn supports_snippets(&self) -> bool { + self.client_capabilities + .text_document + .as_ref() + .and_then(|td| td.completion.as_ref()) + .and_then(|c| c.completion_item.as_ref()) + .and_then(|ci| ci.snippet_support) + .unwrap_or(false) + } } impl Default for Session { From ff11bc022581eba714d5ba5b17aeefe1391a55fc Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 22:44:45 -0500 Subject: [PATCH 16/17] shut up clippy --- crates/djls-project/src/inspector/queries.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/djls-project/src/inspector/queries.rs b/crates/djls-project/src/inspector/queries.rs index ca34705a..5da21594 100644 --- a/crates/djls-project/src/inspector/queries.rs +++ b/crates/djls-project/src/inspector/queries.rs @@ -13,6 +13,7 @@ pub enum Query { } #[derive(Serialize, Deserialize)] +#[allow(clippy::struct_field_names)] pub struct PythonEnvironmentQueryData { pub sys_base_prefix: PathBuf, pub sys_executable: PathBuf, From 27a8dd3869722861cee6b23ef30d776c6226ec2c Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 10 Sep 2025 22:44:50 -0500 Subject: [PATCH 17/17] fmt --- crates/djls-project/src/project.rs | 4 +++- crates/djls-project/src/python.rs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/djls-project/src/project.rs b/crates/djls-project/src/project.rs index a6be2939..735b7945 100644 --- a/crates/djls-project/src/project.rs +++ b/crates/djls-project/src/project.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; use crate::db::Db as ProjectDb; +use crate::django_available; +use crate::django_settings_module; +use crate::get_templatetags; use crate::python::Interpreter; -use crate::{django_available, django_settings_module, get_templatetags}; /// Complete project configuration as a Salsa input. /// diff --git a/crates/djls-project/src/python.rs b/crates/djls-project/src/python.rs index 3910f8ef..9de88494 100644 --- a/crates/djls-project/src/python.rs +++ b/crates/djls-project/src/python.rs @@ -268,11 +268,13 @@ mod tests { } mod env_discovery { + use system::mock::MockGuard; + use system::mock::{ + self as sys_mock, + }; use which::Error as WhichError; use super::*; - use system::mock::MockGuard; - use system::mock::{self as sys_mock}; #[test] fn test_explicit_venv_path_found() {