Skip to content
Merged
2 changes: 1 addition & 1 deletion crates/djls-conf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
37 changes: 20 additions & 17 deletions crates/djls-project/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::meta::ProjectMetadata;
use crate::python::PythonEnvironment;
use crate::inspector::pool::InspectorPool;
use crate::project::Project;

/// 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 the current project (if set)
fn project(&self) -> Option<Project>;

/// 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<PythonEnvironment> {
let project_path = db.metadata().root().as_path();
let venv_path = db.metadata().venv().and_then(|p| p.to_str());
/// Get the shared inspector pool for executing Python queries
fn inspector_pool(&self) -> Arc<InspectorPool>;

PythonEnvironment::new(project_path, venv_path)
/// Get the project root path if a project is set
fn project_path(&self) -> Option<&Path> {
self.project().map(|p| p.root(self).as_path())
}
}
71 changes: 71 additions & 0 deletions crates/djls-project/src/django.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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::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, project: Project) -> bool {
// First try to get Django availability from inspector
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::<bool>(&json_data) {
return available;
}
}

// Fallback to environment detection
python_environment(db, project).is_some()
}

/// 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<String> {
// 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, Query::DjangoInit) {
// Parse the JSON response - expect a string
if let Ok(settings) = serde_json::from_str::<String>(&json_data) {
return Some(settings);
}
}

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.clone();
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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::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, _project: Project) -> Option<TemplateTags> {
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) {
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<TemplateTag>);

impl Deref for TemplateTags {
Expand Down Expand Up @@ -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()));
}
}
32 changes: 32 additions & 0 deletions crates/djls-project/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub use queries::Query;
use serde::Deserialize;
use serde::Serialize;

use crate::db::Db as ProjectDb;
use crate::python::python_environment;

#[derive(Serialize)]
pub struct DjlsRequest {
#[serde(flatten)]
Expand All @@ -19,3 +22,32 @@ pub struct DjlsResponse {
pub data: Option<serde_json::Value>,
pub error: Option<String>,
}

/// Run an inspector query and return the JSON result as a string.
///
/// 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, query: Query) -> Option<String> {
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, &DjlsRequest { query })
{
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,
}
}
6 changes: 0 additions & 6 deletions crates/djls-project/src/inspector/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InspectorPool> = 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
Expand Down
23 changes: 21 additions & 2 deletions crates/djls-project/src/inspector/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ 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,
}

#[derive(Serialize, Deserialize)]
#[allow(clippy::struct_field_names)]
pub struct PythonEnvironmentQueryData {
pub sys_base_prefix: PathBuf,
pub sys_executable: PathBuf,
Expand Down Expand Up @@ -42,3 +43,21 @@ pub struct TemplateTag {
pub module: String,
pub doc: Option<String>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
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!(python_env, Query::PythonEnv);
assert_eq!(templatetags, Query::Templatetags);
assert_eq!(django_init, Query::DjangoInit);
}
}
Loading