Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] }
url = "2.5"
walkdir = "2.5"
which = "8.0"

# testing
Expand Down
4 changes: 4 additions & 0 deletions crates/djls-bench/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,8 @@ impl SemanticDb for Db {
fn tag_index(&self) -> TagIndex<'_> {
TagIndex::from_specs(self)
}

fn template_dirs(&self) -> Option<Vec<Utf8PathBuf>> {
None
}
}
1 change: 1 addition & 0 deletions crates/djls-ide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ djls-workspace = { workspace = true }

salsa = { workspace = true }
tower-lsp-server = { workspace = true }
tracing = { workspace = true }

[lints]
workspace = true
2 changes: 2 additions & 0 deletions crates/djls-ide/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod completions;
mod diagnostics;
mod navigation;
mod snippets;

pub use completions::handle_completion;
pub use diagnostics::collect_diagnostics;
pub use navigation::goto_template_definition;
pub use snippets::generate_partial_snippet;
pub use snippets::generate_snippet_for_tag;
pub use snippets::generate_snippet_for_tag_with_end;
Expand Down
69 changes: 69 additions & 0 deletions crates/djls-ide/src/navigation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use djls_semantic::resolve_template;
use djls_semantic::ResolveResult;
use djls_source::File;
use djls_source::LineCol;
use djls_source::Offset;
use djls_source::PositionEncoding;
use djls_templates::parse_template;
use djls_templates::Node;
use tower_lsp_server::lsp_types;
use tower_lsp_server::UriExt;

pub fn goto_template_definition(
db: &dyn djls_semantic::Db,
file: File,
position: lsp_types::Position,
encoding: PositionEncoding,
) -> Option<lsp_types::GotoDefinitionResponse> {
let nodelist = parse_template(db, file)?;

let line_index = file.line_index(db);
let source = file.source(db);
let line_col = LineCol::new(position.line, position.character);

let offset = encoding.line_col_to_offset(line_index, line_col, source.as_str())?;

let template_name = find_template_name_at_offset(nodelist.nodelist(db), offset)?;
tracing::debug!("Found template reference: '{}'", template_name);

match resolve_template(db, &template_name) {
ResolveResult::Found(template) => {
let path = template.path_buf(db);
tracing::debug!("Resolved template to: {}", path);
let uri = lsp_types::Uri::from_file_path(path.as_std_path())?;

Some(lsp_types::GotoDefinitionResponse::Scalar(
lsp_types::Location {
uri,
range: lsp_types::Range::default(),
},
))
}
ResolveResult::NotFound { tried, .. } => {
tracing::warn!("Template '{}' not found. Tried: {:?}", template_name, tried);
None
}
}
}

fn find_template_name_at_offset(nodes: &[Node], offset: Offset) -> Option<String> {
for node in nodes {
if let Node::Tag {
name, bits, span, ..
} = node
{
if (name == "extends" || name == "include") && span.contains(offset) {
let template_str = bits.first()?;
let template_name = template_str
.trim()
.trim_start_matches('"')
.trim_end_matches('"')
.trim_start_matches('\'')
.trim_end_matches('\'')
.to_string();
return Some(template_name);
}
}
}
None
}
31 changes: 22 additions & 9 deletions crates/djls-project/inspector/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
from queries import QueryData
from queries import get_installed_templatetags
from queries import get_python_environment_info
from queries import get_template_dirs
from queries import initialize_django
except ImportError:
# Fall back to relative import (when running with python -m)
from .queries import Query
from .queries import QueryData
from .queries import get_installed_templatetags
from .queries import get_python_environment_info
from .queries import get_template_dirs
from .queries import initialize_django


Expand All @@ -40,11 +42,19 @@ def to_dict(self) -> dict[str, Any]:
data_dict = asdict(self.data)
# Convert Path objects to strings
for key, value in data_dict.items():
if key in ["sys_base_prefix", "sys_executable", "sys_prefix"]:
if value:
data_dict[key] = str(value)
elif key == "sys_path":
# Handle single Path objects
if hasattr(value, "__fspath__"): # Path-like object
data_dict[key] = str(value)
# Handle lists of Path objects
elif (
isinstance(value, list)
and value
and hasattr(value[0], "__fspath__")
):
data_dict[key] = [str(p) for p in value]
# Handle optional Path objects (could be None)
elif value is None:
pass # Keep None as is
d["data"] = data_dict
return d

Expand All @@ -62,16 +72,19 @@ def handle_request(request: dict[str, Any]) -> DjlsResponse:

args = request.get("args")

if query == Query.PYTHON_ENV:
if query == Query.DJANGO_INIT:
success, error = initialize_django()
return DjlsResponse(ok=success, data=None, error=error)

elif query == Query.PYTHON_ENV:
return DjlsResponse(ok=True, data=get_python_environment_info())

elif query == Query.TEMPLATE_DIRS:
return DjlsResponse(ok=True, data=get_template_dirs())

elif query == Query.TEMPLATETAGS:
return DjlsResponse(ok=True, data=get_installed_templatetags())

elif query == Query.DJANGO_INIT:
success, error = initialize_django()
return DjlsResponse(ok=success, data=None, error=error)

return DjlsResponse(ok=False, error=f"Unhandled query type: {query}")

except Exception as e:
Expand Down
51 changes: 39 additions & 12 deletions crates/djls-project/inspector/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,27 @@


class Query(str, Enum):
DJANGO_INIT = "django_init"
PYTHON_ENV = "python_env"
TEMPLATE_DIRS = "template_dirs"
TEMPLATETAGS = "templatetags"
DJANGO_INIT = "django_init"


def initialize_django() -> tuple[bool, str | None]:
import django
from django.apps import apps

try:
if not os.environ.get("DJANGO_SETTINGS_MODULE"):
return False, None

if not apps.ready:
django.setup()

return True, None

except Exception as e:
return False, str(e)


@dataclass
Expand Down Expand Up @@ -43,21 +61,30 @@ def get_python_environment_info():
)


def initialize_django() -> tuple[bool, str | None]:
import django
@dataclass
class TemplateDirsQueryData:
dirs: list[Path]


def get_template_dirs() -> TemplateDirsQueryData:
from django.apps import apps
from django.conf import settings

try:
if not os.environ.get("DJANGO_SETTINGS_MODULE"):
return False, None
dirs = []

if not apps.ready:
django.setup()
for engine in settings.TEMPLATES:
if "django" not in engine["BACKEND"].lower():
continue

return True, None
dirs.extend(engine.get("DIRS", []))

except Exception as e:
return False, str(e)
if engine.get("APP_DIRS", False):
for app_config in apps.get_app_configs():
template_dir = Path(app_config.path) / "templates"
if template_dir.exists():
dirs.append(template_dir)

return TemplateDirsQueryData(dirs)


@dataclass
Expand Down Expand Up @@ -108,4 +135,4 @@ def get_installed_templatetags() -> TemplateTagQueryData:
return TemplateTagQueryData(templatetags=templatetags)


QueryData = PythonEnvironmentQueryData | TemplateTagQueryData
QueryData = PythonEnvironmentQueryData | TemplateDirsQueryData | TemplateTagQueryData
45 changes: 45 additions & 0 deletions crates/djls-project/src/django.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::ops::Deref;

use camino::Utf8PathBuf;
use serde::Deserialize;
use serde::Serialize;

Expand Down Expand Up @@ -28,6 +29,50 @@ pub fn django_available(db: &dyn ProjectDb, _project: Project) -> bool {
inspector::query(db, &DjangoInitRequest).is_some()
}

#[derive(Serialize)]
struct TemplateDirsRequest;

#[derive(Deserialize)]
struct TemplateDirsResponse {
dirs: Vec<Utf8PathBuf>,
}

impl InspectorRequest for TemplateDirsRequest {
const NAME: &'static str = "template_dirs";
type Response = TemplateDirsResponse;
}

#[salsa::tracked]
pub fn template_dirs(db: &dyn ProjectDb, _project: Project) -> Option<TemplateDirs> {
tracing::debug!("Requesting template directories from inspector");

let response = inspector::query(db, &TemplateDirsRequest)?;

let dir_count = response.dirs.len();
tracing::info!(
"Retrieved {} template directories from inspector",
dir_count
);

for (i, dir) in response.dirs.iter().enumerate() {
tracing::debug!(" Template dir [{}]: {}", i, dir);
}

let missing_dirs: Vec<_> = response.dirs.iter().filter(|dir| !dir.exists()).collect();

if !missing_dirs.is_empty() {
tracing::warn!(
"Found {} non-existent template directories: {:?}",
missing_dirs.len(),
missing_dirs
);
}

Some(response.dirs)
}

type TemplateDirs = Vec<Utf8PathBuf>;

#[derive(Serialize)]
struct TemplatetagsRequest;

Expand Down
1 change: 1 addition & 0 deletions crates/djls-project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod python;

pub use db::Db;
pub use django::django_available;
pub use django::template_dirs;
pub use django::templatetags;
pub use django::TemplateTags;
pub use inspector::Inspector;
Expand Down
2 changes: 2 additions & 0 deletions crates/djls-project/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use camino::Utf8PathBuf;

use crate::db::Db as ProjectDb;
use crate::django_available;
use crate::template_dirs;
use crate::templatetags;
use crate::Interpreter;

Expand Down Expand Up @@ -80,5 +81,6 @@ impl Project {
pub fn initialize(self, db: &dyn ProjectDb) {
let _ = django_available(db, self);
let _ = templatetags(db, self);
let _ = template_dirs(db, self);
}
}
2 changes: 2 additions & 0 deletions crates/djls-semantic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ rustc-hash = { workspace = true }
salsa = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
walkdir = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
Expand Down
5 changes: 5 additions & 0 deletions crates/djls-semantic/src/blocks/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ mod tests {
use std::sync::Mutex;

use camino::Utf8Path;
use camino::Utf8PathBuf;
use djls_source::File;
use djls_source::Span;
use djls_templates::parse_template;
Expand Down Expand Up @@ -226,6 +227,10 @@ mod tests {
fn tag_index(&self) -> TagIndex<'_> {
TagIndex::from_specs(self)
}

fn template_dirs(&self) -> Option<Vec<Utf8PathBuf>> {
None
}
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions crates/djls-semantic/src/db.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use camino::Utf8PathBuf;
use djls_templates::Db as TemplateDb;

use crate::blocks::TagIndex;
Expand All @@ -10,6 +11,8 @@ pub trait Db: TemplateDb {
fn tag_specs(&self) -> TagSpecs;

fn tag_index(&self) -> TagIndex<'_>;

fn template_dirs(&self) -> Option<Vec<Utf8PathBuf>>;
}

#[salsa::accumulator]
Expand Down
Loading