Skip to content

Commit a80d4ed

Browse files
add go to definition feature for extends/include tags (#274)
1 parent 3507a08 commit a80d4ed

File tree

30 files changed

+610
-83
lines changed

30 files changed

+610
-83
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ tracing = "0.1"
3636
tracing-appender = "0.2"
3737
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] }
3838
url = "2.5"
39+
walkdir = "2.5"
3940
which = "8.0"
4041

4142
# testing

crates/djls-bench/src/db.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,8 @@ impl SemanticDb for Db {
7373
fn tag_index(&self) -> TagIndex<'_> {
7474
TagIndex::from_specs(self)
7575
}
76+
77+
fn template_dirs(&self) -> Option<Vec<Utf8PathBuf>> {
78+
None
79+
}
7680
}

crates/djls-ide/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ djls-workspace = { workspace = true }
1212

1313
salsa = { workspace = true }
1414
tower-lsp-server = { workspace = true }
15+
tracing = { workspace = true }
1516

1617
[lints]
1718
workspace = true

crates/djls-ide/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod completions;
22
mod diagnostics;
3+
mod navigation;
34
mod snippets;
45

56
pub use completions::handle_completion;
67
pub use diagnostics::collect_diagnostics;
8+
pub use navigation::goto_template_definition;
79
pub use snippets::generate_partial_snippet;
810
pub use snippets::generate_snippet_for_tag;
911
pub use snippets::generate_snippet_for_tag_with_end;

crates/djls-ide/src/navigation.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use djls_semantic::resolve_template;
2+
use djls_semantic::ResolveResult;
3+
use djls_source::File;
4+
use djls_source::LineCol;
5+
use djls_source::Offset;
6+
use djls_source::PositionEncoding;
7+
use djls_templates::parse_template;
8+
use djls_templates::Node;
9+
use tower_lsp_server::lsp_types;
10+
use tower_lsp_server::UriExt;
11+
12+
pub fn goto_template_definition(
13+
db: &dyn djls_semantic::Db,
14+
file: File,
15+
position: lsp_types::Position,
16+
encoding: PositionEncoding,
17+
) -> Option<lsp_types::GotoDefinitionResponse> {
18+
let nodelist = parse_template(db, file)?;
19+
20+
let line_index = file.line_index(db);
21+
let source = file.source(db);
22+
let line_col = LineCol::new(position.line, position.character);
23+
24+
let offset = encoding.line_col_to_offset(line_index, line_col, source.as_str())?;
25+
26+
let template_name = find_template_name_at_offset(nodelist.nodelist(db), offset)?;
27+
tracing::debug!("Found template reference: '{}'", template_name);
28+
29+
match resolve_template(db, &template_name) {
30+
ResolveResult::Found(template) => {
31+
let path = template.path_buf(db);
32+
tracing::debug!("Resolved template to: {}", path);
33+
let uri = lsp_types::Uri::from_file_path(path.as_std_path())?;
34+
35+
Some(lsp_types::GotoDefinitionResponse::Scalar(
36+
lsp_types::Location {
37+
uri,
38+
range: lsp_types::Range::default(),
39+
},
40+
))
41+
}
42+
ResolveResult::NotFound { tried, .. } => {
43+
tracing::warn!("Template '{}' not found. Tried: {:?}", template_name, tried);
44+
None
45+
}
46+
}
47+
}
48+
49+
fn find_template_name_at_offset(nodes: &[Node], offset: Offset) -> Option<String> {
50+
for node in nodes {
51+
if let Node::Tag {
52+
name, bits, span, ..
53+
} = node
54+
{
55+
if (name == "extends" || name == "include") && span.contains(offset) {
56+
let template_str = bits.first()?;
57+
let template_name = template_str
58+
.trim()
59+
.trim_start_matches('"')
60+
.trim_end_matches('"')
61+
.trim_start_matches('\'')
62+
.trim_end_matches('\'')
63+
.to_string();
64+
return Some(template_name);
65+
}
66+
}
67+
}
68+
None
69+
}

crates/djls-project/inspector/inspector.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
from queries import QueryData
1111
from queries import get_installed_templatetags
1212
from queries import get_python_environment_info
13+
from queries import get_template_dirs
1314
from queries import initialize_django
1415
except ImportError:
1516
# Fall back to relative import (when running with python -m)
1617
from .queries import Query
1718
from .queries import QueryData
1819
from .queries import get_installed_templatetags
1920
from .queries import get_python_environment_info
21+
from .queries import get_template_dirs
2022
from .queries import initialize_django
2123

2224

@@ -40,11 +42,19 @@ def to_dict(self) -> dict[str, Any]:
4042
data_dict = asdict(self.data)
4143
# Convert Path objects to strings
4244
for key, value in data_dict.items():
43-
if key in ["sys_base_prefix", "sys_executable", "sys_prefix"]:
44-
if value:
45-
data_dict[key] = str(value)
46-
elif key == "sys_path":
45+
# Handle single Path objects
46+
if hasattr(value, "__fspath__"): # Path-like object
47+
data_dict[key] = str(value)
48+
# Handle lists of Path objects
49+
elif (
50+
isinstance(value, list)
51+
and value
52+
and hasattr(value[0], "__fspath__")
53+
):
4754
data_dict[key] = [str(p) for p in value]
55+
# Handle optional Path objects (could be None)
56+
elif value is None:
57+
pass # Keep None as is
4858
d["data"] = data_dict
4959
return d
5060

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

6373
args = request.get("args")
6474

65-
if query == Query.PYTHON_ENV:
75+
if query == Query.DJANGO_INIT:
76+
success, error = initialize_django()
77+
return DjlsResponse(ok=success, data=None, error=error)
78+
79+
elif query == Query.PYTHON_ENV:
6680
return DjlsResponse(ok=True, data=get_python_environment_info())
6781

82+
elif query == Query.TEMPLATE_DIRS:
83+
return DjlsResponse(ok=True, data=get_template_dirs())
84+
6885
elif query == Query.TEMPLATETAGS:
6986
return DjlsResponse(ok=True, data=get_installed_templatetags())
7087

71-
elif query == Query.DJANGO_INIT:
72-
success, error = initialize_django()
73-
return DjlsResponse(ok=success, data=None, error=error)
74-
7588
return DjlsResponse(ok=False, error=f"Unhandled query type: {query}")
7689

7790
except Exception as e:

crates/djls-project/inspector/queries.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,27 @@
99

1010

1111
class Query(str, Enum):
12+
DJANGO_INIT = "django_init"
1213
PYTHON_ENV = "python_env"
14+
TEMPLATE_DIRS = "template_dirs"
1315
TEMPLATETAGS = "templatetags"
14-
DJANGO_INIT = "django_init"
16+
17+
18+
def initialize_django() -> tuple[bool, str | None]:
19+
import django
20+
from django.apps import apps
21+
22+
try:
23+
if not os.environ.get("DJANGO_SETTINGS_MODULE"):
24+
return False, None
25+
26+
if not apps.ready:
27+
django.setup()
28+
29+
return True, None
30+
31+
except Exception as e:
32+
return False, str(e)
1533

1634

1735
@dataclass
@@ -43,21 +61,30 @@ def get_python_environment_info():
4361
)
4462

4563

46-
def initialize_django() -> tuple[bool, str | None]:
47-
import django
64+
@dataclass
65+
class TemplateDirsQueryData:
66+
dirs: list[Path]
67+
68+
69+
def get_template_dirs() -> TemplateDirsQueryData:
4870
from django.apps import apps
71+
from django.conf import settings
4972

50-
try:
51-
if not os.environ.get("DJANGO_SETTINGS_MODULE"):
52-
return False, None
73+
dirs = []
5374

54-
if not apps.ready:
55-
django.setup()
75+
for engine in settings.TEMPLATES:
76+
if "django" not in engine["BACKEND"].lower():
77+
continue
5678

57-
return True, None
79+
dirs.extend(engine.get("DIRS", []))
5880

59-
except Exception as e:
60-
return False, str(e)
81+
if engine.get("APP_DIRS", False):
82+
for app_config in apps.get_app_configs():
83+
template_dir = Path(app_config.path) / "templates"
84+
if template_dir.exists():
85+
dirs.append(template_dir)
86+
87+
return TemplateDirsQueryData(dirs)
6188

6289

6390
@dataclass
@@ -108,4 +135,4 @@ def get_installed_templatetags() -> TemplateTagQueryData:
108135
return TemplateTagQueryData(templatetags=templatetags)
109136

110137

111-
QueryData = PythonEnvironmentQueryData | TemplateTagQueryData
138+
QueryData = PythonEnvironmentQueryData | TemplateDirsQueryData | TemplateTagQueryData

crates/djls-project/src/django.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::ops::Deref;
22

3+
use camino::Utf8PathBuf;
34
use serde::Deserialize;
45
use serde::Serialize;
56

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

32+
#[derive(Serialize)]
33+
struct TemplateDirsRequest;
34+
35+
#[derive(Deserialize)]
36+
struct TemplateDirsResponse {
37+
dirs: Vec<Utf8PathBuf>,
38+
}
39+
40+
impl InspectorRequest for TemplateDirsRequest {
41+
const NAME: &'static str = "template_dirs";
42+
type Response = TemplateDirsResponse;
43+
}
44+
45+
#[salsa::tracked]
46+
pub fn template_dirs(db: &dyn ProjectDb, _project: Project) -> Option<TemplateDirs> {
47+
tracing::debug!("Requesting template directories from inspector");
48+
49+
let response = inspector::query(db, &TemplateDirsRequest)?;
50+
51+
let dir_count = response.dirs.len();
52+
tracing::info!(
53+
"Retrieved {} template directories from inspector",
54+
dir_count
55+
);
56+
57+
for (i, dir) in response.dirs.iter().enumerate() {
58+
tracing::debug!(" Template dir [{}]: {}", i, dir);
59+
}
60+
61+
let missing_dirs: Vec<_> = response.dirs.iter().filter(|dir| !dir.exists()).collect();
62+
63+
if !missing_dirs.is_empty() {
64+
tracing::warn!(
65+
"Found {} non-existent template directories: {:?}",
66+
missing_dirs.len(),
67+
missing_dirs
68+
);
69+
}
70+
71+
Some(response.dirs)
72+
}
73+
74+
type TemplateDirs = Vec<Utf8PathBuf>;
75+
3176
#[derive(Serialize)]
3277
struct TemplatetagsRequest;
3378

crates/djls-project/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod python;
66

77
pub use db::Db;
88
pub use django::django_available;
9+
pub use django::template_dirs;
910
pub use django::templatetags;
1011
pub use django::TemplateTags;
1112
pub use inspector::Inspector;

0 commit comments

Comments
 (0)