Skip to content

Commit d60b597

Browse files
Refactor project state management to use Salsa (#216)
1 parent 007a009 commit d60b597

File tree

15 files changed

+516
-343
lines changed

15 files changed

+516
-343
lines changed

crates/djls-conf/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub enum ConfigError {
2121
PyprojectSerialize(#[from] toml::ser::Error),
2222
}
2323

24-
#[derive(Debug, Deserialize, Default, PartialEq)]
24+
#[derive(Debug, Deserialize, Default, PartialEq, Clone)]
2525
pub struct Settings {
2626
#[serde(default)]
2727
debug: bool,

crates/djls-project/src/db.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,33 @@
22
//!
33
//! This module extends the workspace database trait with project-specific
44
//! functionality including metadata access and Python environment discovery.
5+
//!
6+
//! ## Architecture
7+
//!
8+
//! Following the Salsa pattern established in workspace and templates crates:
9+
//! - `DjangoProject` is a Salsa input representing external project state
10+
//! - Tracked functions compute derived values (Python env, Django config)
11+
//! - Database trait provides stable configuration (metadata, template tags)
12+
13+
use std::path::Path;
14+
use std::sync::Arc;
515

616
use djls_workspace::Db as WorkspaceDb;
717

8-
use crate::meta::ProjectMetadata;
9-
use crate::python::PythonEnvironment;
18+
use crate::inspector::pool::InspectorPool;
19+
use crate::project::Project;
1020

1121
/// Project-specific database trait extending the workspace database
1222
#[salsa::db]
1323
pub trait Db: WorkspaceDb {
14-
/// Get the project metadata containing root path and venv configuration
15-
fn metadata(&self) -> &ProjectMetadata;
16-
}
24+
/// Get the current project (if set)
25+
fn project(&self) -> Option<Project>;
1726

18-
/// Find the Python environment for the project.
19-
///
20-
/// This Salsa tracked function discovers the Python environment based on:
21-
/// 1. Explicit venv path from metadata
22-
/// 2. VIRTUAL_ENV environment variable
23-
/// 3. Common venv directories in project root (.venv, venv, env, .env)
24-
/// 4. System Python as fallback
25-
#[salsa::tracked]
26-
pub fn find_python_environment(db: &dyn Db) -> Option<PythonEnvironment> {
27-
let project_path = db.metadata().root().as_path();
28-
let venv_path = db.metadata().venv().and_then(|p| p.to_str());
27+
/// Get the shared inspector pool for executing Python queries
28+
fn inspector_pool(&self) -> Arc<InspectorPool>;
2929

30-
PythonEnvironment::new(project_path, venv_path)
30+
/// Get the project root path if a project is set
31+
fn project_path(&self) -> Option<&Path> {
32+
self.project().map(|p| p.root(self).as_path())
33+
}
3134
}

crates/djls-project/src/django.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
mod templatetags;
2+
3+
pub use templatetags::get_templatetags;
4+
pub use templatetags::TemplateTags;
5+
6+
use crate::db::Db as ProjectDb;
7+
use crate::inspector::inspector_run;
8+
use crate::inspector::queries::Query;
9+
use crate::python::python_environment;
10+
use crate::Project;
11+
12+
/// Check if Django is available for the current project.
13+
///
14+
/// This determines if Django is installed and configured in the Python environment.
15+
/// First consults the inspector, then falls back to environment detection.
16+
#[salsa::tracked]
17+
pub fn django_available(db: &dyn ProjectDb, project: Project) -> bool {
18+
// First try to get Django availability from inspector
19+
if let Some(json_data) = inspector_run(db, Query::DjangoInit) {
20+
// Parse the JSON response - expect a boolean
21+
if let Ok(available) = serde_json::from_str::<bool>(&json_data) {
22+
return available;
23+
}
24+
}
25+
26+
// Fallback to environment detection
27+
python_environment(db, project).is_some()
28+
}
29+
30+
/// Get the Django settings module name for the current project.
31+
///
32+
/// Returns the settings_module_override from project, or inspector result,
33+
/// or DJANGO_SETTINGS_MODULE env var, or attempts to detect it.
34+
#[salsa::tracked]
35+
pub fn django_settings_module(db: &dyn ProjectDb, project: Project) -> Option<String> {
36+
// Check project override first
37+
if let Some(settings) = project.settings_module(db) {
38+
return Some(settings.clone());
39+
}
40+
41+
// Try to get settings module from inspector
42+
if let Some(json_data) = inspector_run(db, Query::DjangoInit) {
43+
// Parse the JSON response - expect a string
44+
if let Ok(settings) = serde_json::from_str::<String>(&json_data) {
45+
return Some(settings);
46+
}
47+
}
48+
49+
let project_path = project.root(db);
50+
51+
// Try to detect settings module
52+
if project_path.join("manage.py").exists() {
53+
// Look for common settings modules
54+
for candidate in &["settings", "config.settings", "project.settings"] {
55+
let parts: Vec<&str> = candidate.split('.').collect();
56+
let mut path = project_path.clone();
57+
for part in &parts[..parts.len() - 1] {
58+
path = path.join(part);
59+
}
60+
if let Some(last) = parts.last() {
61+
path = path.join(format!("{last}.py"));
62+
}
63+
64+
if path.exists() {
65+
return Some((*candidate).to_string());
66+
}
67+
}
68+
}
69+
70+
None
71+
}

crates/djls-project/src/templatetags.rs renamed to crates/djls-project/src/django/templatetags.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,30 @@ use anyhow::Context;
44
use anyhow::Result;
55
use serde_json::Value;
66

7-
#[derive(Debug, Default, Clone)]
7+
use crate::db::Db as ProjectDb;
8+
use crate::inspector::inspector_run;
9+
use crate::inspector::queries::Query;
10+
use crate::Project;
11+
12+
/// Get template tags for the current project by querying the inspector.
13+
///
14+
/// This tracked function calls the inspector to retrieve Django template tags
15+
/// and parses the JSON response into a TemplateTags struct.
16+
#[salsa::tracked]
17+
pub fn get_templatetags(db: &dyn ProjectDb, _project: Project) -> Option<TemplateTags> {
18+
let json_str = inspector_run(db, Query::Templatetags)?;
19+
20+
// Parse the JSON string into a Value first
21+
let json_value: serde_json::Value = match serde_json::from_str(&json_str) {
22+
Ok(value) => value,
23+
Err(_) => return None,
24+
};
25+
26+
// Parse the JSON data into TemplateTags
27+
TemplateTags::from_json(&json_value).ok()
28+
}
29+
30+
#[derive(Debug, Default, Clone, PartialEq)]
831
pub struct TemplateTags(Vec<TemplateTag>);
932

1033
impl Deref for TemplateTags {
@@ -83,3 +106,29 @@ impl TemplateTag {
83106
self.doc.as_ref()
84107
}
85108
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
114+
#[test]
115+
fn test_template_tags_parsing() {
116+
// Test that TemplateTags can parse valid JSON
117+
let json_data = r#"{
118+
"templatetags": [
119+
{
120+
"name": "test_tag",
121+
"module": "test_module",
122+
"doc": "Test documentation"
123+
}
124+
]
125+
}"#;
126+
127+
let value: serde_json::Value = serde_json::from_str(json_data).unwrap();
128+
let tags = TemplateTags::from_json(&value).unwrap();
129+
assert_eq!(tags.len(), 1);
130+
assert_eq!(tags[0].name(), "test_tag");
131+
assert_eq!(tags[0].library(), "test_module");
132+
assert_eq!(tags[0].doc(), Some(&"Test documentation".to_string()));
133+
}
134+
}

crates/djls-project/src/inspector.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ pub use queries::Query;
77
use serde::Deserialize;
88
use serde::Serialize;
99

10+
use crate::db::Db as ProjectDb;
11+
use crate::python::python_environment;
12+
1013
#[derive(Serialize)]
1114
pub struct DjlsRequest {
1215
#[serde(flatten)]
@@ -19,3 +22,32 @@ pub struct DjlsResponse {
1922
pub data: Option<serde_json::Value>,
2023
pub error: Option<String>,
2124
}
25+
26+
/// Run an inspector query and return the JSON result as a string.
27+
///
28+
/// This tracked function executes inspector queries through the shared pool
29+
/// and caches the results based on project state and query kind.
30+
pub fn inspector_run(db: &dyn ProjectDb, query: Query) -> Option<String> {
31+
let project = db.project()?;
32+
let python_env = python_environment(db, project)?;
33+
let project_path = project.root(db);
34+
35+
match db
36+
.inspector_pool()
37+
.query(&python_env, project_path, &DjlsRequest { query })
38+
{
39+
Ok(response) => {
40+
if response.ok {
41+
if let Some(data) = response.data {
42+
// Convert to JSON string
43+
serde_json::to_string(&data).ok()
44+
} else {
45+
None
46+
}
47+
} else {
48+
None
49+
}
50+
}
51+
Err(_) => None,
52+
}
53+
}

crates/djls-project/src/inspector/pool.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ use super::DjlsRequest;
1111
use super::DjlsResponse;
1212
use crate::python::PythonEnvironment;
1313

14-
/// Global singleton pool for convenience
15-
static GLOBAL_POOL: std::sync::OnceLock<InspectorPool> = std::sync::OnceLock::new();
16-
17-
pub fn global_pool() -> &'static InspectorPool {
18-
GLOBAL_POOL.get_or_init(InspectorPool::new)
19-
}
2014
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
2115

2216
/// Manages a pool of inspector processes with automatic cleanup

crates/djls-project/src/inspector/queries.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ use std::path::PathBuf;
33
use serde::Deserialize;
44
use serde::Serialize;
55

6-
#[derive(Serialize, Deserialize)]
6+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Copy)]
77
#[serde(tag = "query", content = "args")]
88
#[serde(rename_all = "snake_case")]
99
pub enum Query {
10+
DjangoInit,
1011
PythonEnv,
1112
Templatetags,
12-
DjangoInit,
1313
}
1414

1515
#[derive(Serialize, Deserialize)]
16+
#[allow(clippy::struct_field_names)]
1617
pub struct PythonEnvironmentQueryData {
1718
pub sys_base_prefix: PathBuf,
1819
pub sys_executable: PathBuf,
@@ -42,3 +43,21 @@ pub struct TemplateTag {
4243
pub module: String,
4344
pub doc: Option<String>,
4445
}
46+
47+
#[cfg(test)]
48+
mod tests {
49+
use super::*;
50+
51+
#[test]
52+
fn test_query_enum() {
53+
// Test that Query variants exist and are copyable
54+
let python_env = Query::PythonEnv;
55+
let templatetags = Query::Templatetags;
56+
let django_init = Query::DjangoInit;
57+
58+
// Test that they can be copied
59+
assert_eq!(python_env, Query::PythonEnv);
60+
assert_eq!(templatetags, Query::Templatetags);
61+
assert_eq!(django_init, Query::DjangoInit);
62+
}
63+
}

0 commit comments

Comments
 (0)