Skip to content

Commit 5091be6

Browse files
authored
Merge pull request #49 from fourdigits/add-url-goto-hover
Add url goto hover
2 parents 7a85fcf + 3b586fd commit 5091be6

File tree

5 files changed

+92
-29
lines changed

5 files changed

+92
-29
lines changed

README.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1-
# Django template LSP
1+
# Django Template Language Server (LSP)
22

3-
A simple Django template LSP for completions that has support for:
3+
The Django Template Language Server (LSP) enhances your Django development
4+
experience with powerful features for navigating and editing template files.
5+
This LSP supports:
6+
7+
### Completions
8+
9+
- **Custom Tags and Filters**: Autocomplete for your custom template tags and filters.
10+
- **Template**: Suggestions for `extends` and `includes` statements.
11+
- **Load Tag**: Autocomplete for `{% load %}` tags.
12+
- **Static Files**: Path suggestions for `{% static %}` tags.
13+
- **URLs**: Autocomplete for `{% url %}` tags.
14+
15+
### Go to Definitions
16+
17+
- **Template**: Jump directly to the templates used in `extends` and `includes`.
18+
- **URL Tag**: Navigate to the views referenced in `{% url %}` tags.
19+
- **Tags and Filters**: Quickly access the definitions of custom tags and filters.
20+
- **Context Variables**: Partial support for jumping to context definitions.
21+
22+
### Hover Documentation
23+
24+
- **URLs**: Inline documentation for `{% url %}` tags.
25+
- **Tags and Filters**: Detailed descriptions for template tags and filters.
426

5-
- Custom `tags` and `filters`
6-
- templates for `extends` and `includes`
7-
- load tag
8-
- static files
9-
- urls
1027

1128
## Support (tested)
1229

@@ -22,7 +39,7 @@ A simple Django template LSP for completions that has support for:
2239

2340
- `docker_compose_file` (string) default: "docker-compose.yml"
2441
- `docker_compose_service` (string) default: "django"
25-
- `django_settings_module` (string) default: ""
42+
- `django_settings_module` (string) default (auto detected when empty): ""
2643

2744
## Type hints
2845

djlsp/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
FALLBACK_DJANGO_DATA = {
44
"file_watcher_globs": ["**/templates/**", "**/templatetags/**", "**/static/**"],
55
"static_files": [],
6-
"urls": [],
6+
"urls": {},
77
"libraries": {
88
"__builtins__": {
99
"tags": {

djlsp/index.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ class Template:
1010
context: dict = field(default_factory=dict)
1111

1212

13+
@dataclass
14+
class Url:
15+
name: str = ""
16+
docs: str = ""
17+
source: str = ""
18+
19+
1320
@dataclass
1421
class Tag:
1522
name: str = ""
@@ -39,7 +46,7 @@ class WorkspaceIndex:
3946
env_path: str = ""
4047
file_watcher_globs: [str] = field(default_factory=list)
4148
static_files: [str] = field(default_factory=list)
42-
urls: [str] = field(default_factory=list)
49+
urls: dict[str, Url] = field(default_factory=dict)
4350
libraries: dict[str, Library] = field(default_factory=dict)
4451
templates: dict[str, Template] = field(default_factory=dict)
4552
global_template_context: dict[str, str] = field(default_factory=dict)
@@ -49,7 +56,14 @@ def update(self, django_data: dict):
4956
"file_watcher_globs", self.file_watcher_globs
5057
)
5158
self.static_files = django_data.get("static_files", self.static_files)
52-
self.urls = django_data.get("urls", self.urls)
59+
self.urls = {
60+
name: Url(
61+
name=name,
62+
docs=options.get("docs", ""),
63+
source=options.get("source", ""),
64+
)
65+
for name, options in django_data.get("urls", {}).items()
66+
}
5367

5468
self.libraries = {
5569
lib_name: Library(

djlsp/parser.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,9 @@ def get_url_completions(self, match: Match):
216216
prefix = match.group(2)
217217
logger.debug(f"Find url matches for: {prefix}")
218218
return [
219-
CompletionItem(label=url)
220-
for url in self.workspace_index.urls
221-
if url.startswith(prefix)
219+
CompletionItem(label=url.name, documentation=url.docs)
220+
for url in self.workspace_index.urls.values()
221+
if url.name.startswith(prefix)
222222
]
223223

224224
def get_template_completions(self, match: Match):
@@ -315,6 +315,7 @@ def get_context_completions(self, match: Match):
315315
def hover(self, line, character):
316316
line_fragment = self.document.lines[line][:character]
317317
matchers = [
318+
(re.compile(r""".*{% ?url ('|")([\w\-:]*)$"""), self.get_url_hover),
318319
(re.compile(r"^.*({%|{{) ?[\w \.\|]*\|(\w*)$"), self.get_filter_hover),
319320
(re.compile(r"^.*{% ?(\w*)$"), self.get_tag_hover),
320321
]
@@ -323,6 +324,16 @@ def hover(self, line, character):
323324
return hover(line, character, match)
324325
return None
325326

327+
def get_url_hover(self, line, character, match: Match):
328+
full_match = self._get_full_hover_name(
329+
line, character, match.group(2), regex=r"^([\w\d:\-]+).*"
330+
)
331+
logger.debug(f"Find url hover for: {full_match}")
332+
if url := self.workspace_index.urls.get(full_match):
333+
return Hover(
334+
contents=url.docs,
335+
)
336+
326337
def get_filter_hover(self, line, character, match: Match):
327338
filter_name = self._get_full_hover_name(line, character, match.group(2))
328339
logger.debug(f"Find filter hover for: {filter_name}")
@@ -343,10 +354,8 @@ def get_tag_hover(self, line, character, match: Match):
343354
)
344355
return None
345356

346-
def _get_full_hover_name(self, line, character, first_part):
347-
if match_after := re.match(
348-
r"^([\w\d]+).*", self.document.lines[line][character:]
349-
):
357+
def _get_full_hover_name(self, line, character, first_part, regex=r"^([\w\d]+).*"):
358+
if match_after := re.match(regex, self.document.lines[line][character:]):
350359
return first_part + match_after.group(1)
351360
return first_part
352361

@@ -360,6 +369,7 @@ def goto_definition(self, line, character):
360369
re.compile(r""".*{% ?(extends|include) ('|")([\w\-\./]*)$"""),
361370
self.get_template_definition,
362371
),
372+
(re.compile(r""".*{% ?url ('|")([\w\-:]*)$"""), self.get_url_definition),
363373
(re.compile(r"^.*{% ?(\w*)$"), self.get_tag_definition),
364374
(re.compile(r"^.*({%|{{).*?\|(\w*)$"), self.get_filter_definition),
365375
(
@@ -394,6 +404,15 @@ def get_template_definition(self, line, character, match: Match):
394404
location, path = template.path.split(":")
395405
return self.create_location(location, path, 0)
396406

407+
def get_url_definition(self, line, character, match: Match):
408+
full_match = self._get_full_definition_name(
409+
line, character, match.group(2), regex=r"^([\w\d:\-]+).*"
410+
)
411+
logger.debug(f"Find url goto definition for: {full_match}")
412+
if url := self.workspace_index.urls.get(full_match):
413+
if url.source:
414+
return self.create_location(*url.source.split(":"))
415+
397416
def get_tag_definition(self, line, character, match: Match):
398417
full_match = self._get_full_definition_name(line, character, match.group(1))
399418
logger.debug(f"Find tag goto definition for: {full_match}")
@@ -430,9 +449,9 @@ def get_context_definition(self, line, character, match: Match):
430449
),
431450
)
432451

433-
def _get_full_definition_name(self, line, character, first_part):
434-
if match_after := re.match(
435-
r"^([\w\d]+).*", self.document.lines[line][character:]
436-
):
452+
def _get_full_definition_name(
453+
self, line, character, first_part, regex=r"^([\w\d]+).*"
454+
):
455+
if match_after := re.match(regex, self.document.lines[line][character:]):
437456
return first_part + match_after.group(1)
438457
return first_part

djlsp/scripts/django-collector.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def __init__(self, project_src_path):
158158
# Index data
159159
self.file_watcher_globs = []
160160
self.static_files = []
161-
self.urls = []
161+
self.urls = {}
162162
self.libraries = {}
163163
self.templates: dict[str, Template] = {}
164164
self.global_template_context = {}
@@ -256,10 +256,10 @@ def get_urls(self):
256256
try:
257257
urlpatterns = __import__(settings.ROOT_URLCONF, {}, {}, [""]).urlpatterns
258258
except Exception:
259-
return []
259+
return {}
260260

261-
def recursive_get_views(urlpatterns, namespace=None):
262-
views = []
261+
def recursive_get_views(urlpatterns, namespace=None, pattern=""):
262+
views = {}
263263
for p in urlpatterns:
264264
if isinstance(p, URLPattern):
265265
# TODO: Get view path/line and template context
@@ -269,7 +269,14 @@ def recursive_get_views(urlpatterns, namespace=None):
269269
name = "{0}:{1}".format(namespace, p.name)
270270
else:
271271
name = p.name
272-
views.append(name)
272+
273+
if name:
274+
views[name] = {
275+
"docs": f"{pattern}{p.pattern}",
276+
"source": self.get_source_from_type(
277+
getattr(p.callback, "view_class", p.callback)
278+
),
279+
}
273280
elif isinstance(p, URLResolver):
274281
try:
275282
patterns = p.url_patterns
@@ -279,8 +286,14 @@ def recursive_get_views(urlpatterns, namespace=None):
279286
_namespace = "{0}:{1}".format(namespace, p.namespace)
280287
else:
281288
_namespace = p.namespace or namespace
282-
views.extend(recursive_get_views(patterns, namespace=_namespace))
283-
return list(filter(None, views))
289+
views.update(
290+
recursive_get_views(
291+
patterns,
292+
namespace=_namespace,
293+
pattern=f"{pattern}{p.pattern}",
294+
)
295+
)
296+
return views
284297

285298
return recursive_get_views(urlpatterns)
286299

0 commit comments

Comments
 (0)