Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add url goto hover #49

Merged
merged 2 commits into from
Aug 29, 2024
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
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
# Django template LSP
# Django Template Language Server (LSP)

A simple Django template LSP for completions that has support for:
The Django Template Language Server (LSP) enhances your Django development
experience with powerful features for navigating and editing template files.
This LSP supports:

### Completions

- **Custom Tags and Filters**: Autocomplete for your custom template tags and filters.
- **Template**: Suggestions for `extends` and `includes` statements.
- **Load Tag**: Autocomplete for `{% load %}` tags.
- **Static Files**: Path suggestions for `{% static %}` tags.
- **URLs**: Autocomplete for `{% url %}` tags.

### Go to Definitions

- **Template**: Jump directly to the templates used in `extends` and `includes`.
- **URL Tag**: Navigate to the views referenced in `{% url %}` tags.
- **Tags and Filters**: Quickly access the definitions of custom tags and filters.
- **Context Variables**: Partial support for jumping to context definitions.

### Hover Documentation

- **URLs**: Inline documentation for `{% url %}` tags.
- **Tags and Filters**: Detailed descriptions for template tags and filters.

- Custom `tags` and `filters`
- templates for `extends` and `includes`
- load tag
- static files
- urls

## Support (tested)

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

- `docker_compose_file` (string) default: "docker-compose.yml"
- `docker_compose_service` (string) default: "django"
- `django_settings_module` (string) default: ""
- `django_settings_module` (string) default (auto detected when empty): ""

## Type hints

Expand Down
2 changes: 1 addition & 1 deletion djlsp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
FALLBACK_DJANGO_DATA = {
"file_watcher_globs": ["**/templates/**", "**/templatetags/**", "**/static/**"],
"static_files": [],
"urls": [],
"urls": {},
"libraries": {
"__builtins__": {
"tags": {
Expand Down
18 changes: 16 additions & 2 deletions djlsp/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ class Template:
context: dict = field(default_factory=dict)


@dataclass
class Url:
name: str = ""
docs: str = ""
source: str = ""


@dataclass
class Tag:
name: str = ""
Expand Down Expand Up @@ -39,7 +46,7 @@ class WorkspaceIndex:
env_path: str = ""
file_watcher_globs: [str] = field(default_factory=list)
static_files: [str] = field(default_factory=list)
urls: [str] = field(default_factory=list)
urls: dict[str, Url] = field(default_factory=dict)
libraries: dict[str, Library] = field(default_factory=dict)
templates: dict[str, Template] = field(default_factory=dict)
global_template_context: dict[str, str] = field(default_factory=dict)
Expand All @@ -49,7 +56,14 @@ def update(self, django_data: dict):
"file_watcher_globs", self.file_watcher_globs
)
self.static_files = django_data.get("static_files", self.static_files)
self.urls = django_data.get("urls", self.urls)
self.urls = {
name: Url(
name=name,
docs=options.get("docs", ""),
source=options.get("source", ""),
)
for name, options in django_data.get("urls", {}).items()
}

self.libraries = {
lib_name: Library(
Expand Down
41 changes: 30 additions & 11 deletions djlsp/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ def get_url_completions(self, match: Match):
prefix = match.group(2)
logger.debug(f"Find url matches for: {prefix}")
return [
CompletionItem(label=url)
for url in self.workspace_index.urls
if url.startswith(prefix)
CompletionItem(label=url.name, documentation=url.docs)
for url in self.workspace_index.urls.values()
if url.name.startswith(prefix)
]

def get_template_completions(self, match: Match):
Expand Down Expand Up @@ -315,6 +315,7 @@ def get_context_completions(self, match: Match):
def hover(self, line, character):
line_fragment = self.document.lines[line][:character]
matchers = [
(re.compile(r""".*{% ?url ('|")([\w\-:]*)$"""), self.get_url_hover),
(re.compile(r"^.*({%|{{) ?[\w \.\|]*\|(\w*)$"), self.get_filter_hover),
(re.compile(r"^.*{% ?(\w*)$"), self.get_tag_hover),
]
Expand All @@ -323,6 +324,16 @@ def hover(self, line, character):
return hover(line, character, match)
return None

def get_url_hover(self, line, character, match: Match):
full_match = self._get_full_hover_name(
line, character, match.group(2), regex=r"^([\w\d:\-]+).*"
)
logger.debug(f"Find url hover for: {full_match}")
if url := self.workspace_index.urls.get(full_match):
return Hover(
contents=url.docs,
)

def get_filter_hover(self, line, character, match: Match):
filter_name = self._get_full_hover_name(line, character, match.group(2))
logger.debug(f"Find filter hover for: {filter_name}")
Expand All @@ -343,10 +354,8 @@ def get_tag_hover(self, line, character, match: Match):
)
return None

def _get_full_hover_name(self, line, character, first_part):
if match_after := re.match(
r"^([\w\d]+).*", self.document.lines[line][character:]
):
def _get_full_hover_name(self, line, character, first_part, regex=r"^([\w\d]+).*"):
if match_after := re.match(regex, self.document.lines[line][character:]):
return first_part + match_after.group(1)
return first_part

Expand All @@ -360,6 +369,7 @@ def goto_definition(self, line, character):
re.compile(r""".*{% ?(extends|include) ('|")([\w\-\./]*)$"""),
self.get_template_definition,
),
(re.compile(r""".*{% ?url ('|")([\w\-:]*)$"""), self.get_url_definition),
(re.compile(r"^.*{% ?(\w*)$"), self.get_tag_definition),
(re.compile(r"^.*({%|{{).*?\|(\w*)$"), self.get_filter_definition),
(
Expand Down Expand Up @@ -394,6 +404,15 @@ def get_template_definition(self, line, character, match: Match):
location, path = template.path.split(":")
return self.create_location(location, path, 0)

def get_url_definition(self, line, character, match: Match):
full_match = self._get_full_definition_name(
line, character, match.group(2), regex=r"^([\w\d:\-]+).*"
)
logger.debug(f"Find url goto definition for: {full_match}")
if url := self.workspace_index.urls.get(full_match):
if url.source:
return self.create_location(*url.source.split(":"))

def get_tag_definition(self, line, character, match: Match):
full_match = self._get_full_definition_name(line, character, match.group(1))
logger.debug(f"Find tag goto definition for: {full_match}")
Expand Down Expand Up @@ -430,9 +449,9 @@ def get_context_definition(self, line, character, match: Match):
),
)

def _get_full_definition_name(self, line, character, first_part):
if match_after := re.match(
r"^([\w\d]+).*", self.document.lines[line][character:]
):
def _get_full_definition_name(
self, line, character, first_part, regex=r"^([\w\d]+).*"
):
if match_after := re.match(regex, self.document.lines[line][character:]):
return first_part + match_after.group(1)
return first_part
27 changes: 20 additions & 7 deletions djlsp/scripts/django-collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def __init__(self, project_src_path):
# Index data
self.file_watcher_globs = []
self.static_files = []
self.urls = []
self.urls = {}
self.libraries = {}
self.templates: dict[str, Template] = {}
self.global_template_context = {}
Expand Down Expand Up @@ -256,10 +256,10 @@ def get_urls(self):
try:
urlpatterns = __import__(settings.ROOT_URLCONF, {}, {}, [""]).urlpatterns
except Exception:
return []
return {}

def recursive_get_views(urlpatterns, namespace=None):
views = []
def recursive_get_views(urlpatterns, namespace=None, pattern=""):
views = {}
for p in urlpatterns:
if isinstance(p, URLPattern):
# TODO: Get view path/line and template context
Expand All @@ -269,7 +269,14 @@ def recursive_get_views(urlpatterns, namespace=None):
name = "{0}:{1}".format(namespace, p.name)
else:
name = p.name
views.append(name)

if name:
views[name] = {
"docs": f"{pattern}{p.pattern}",
"source": self.get_source_from_type(
getattr(p.callback, "view_class", p.callback)
),
}
elif isinstance(p, URLResolver):
try:
patterns = p.url_patterns
Expand All @@ -279,8 +286,14 @@ def recursive_get_views(urlpatterns, namespace=None):
_namespace = "{0}:{1}".format(namespace, p.namespace)
else:
_namespace = p.namespace or namespace
views.extend(recursive_get_views(patterns, namespace=_namespace))
return list(filter(None, views))
views.update(
recursive_get_views(
patterns,
namespace=_namespace,
pattern=f"{pattern}{p.pattern}",
)
)
return views

return recursive_get_views(urlpatterns)

Expand Down