Skip to content

Commit

Permalink
Add goto definition support for templates
Browse files Browse the repository at this point in the history
  • Loading branch information
krukas committed Jul 3, 2024
1 parent 4af23b8 commit 422698a
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 22 deletions.
3 changes: 3 additions & 0 deletions djlsp/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@dataclass
class Template:
name: str = ""
path: str = ""
extends: str | None = None
blocks: list[str] | None = None
context: dict = field(default_factory=dict)
Expand Down Expand Up @@ -32,6 +33,8 @@ class Library:

@dataclass
class WorkspaceIndex:
src_path: str = ""
env_path: str = ""
file_watcher_globs: [str] = field(default_factory=list)
static_files: [str] = field(default_factory=list)
urls: [str] = field(default_factory=list)
Expand Down
44 changes: 43 additions & 1 deletion djlsp/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from functools import cached_property
from re import Match

from lsprotocol.types import CompletionItem, Hover
from lsprotocol.types import CompletionItem, Hover, Location, Position, Range
from pygls.workspace import TextDocument

from djlsp.constants import BUILTIN
Expand Down Expand Up @@ -252,3 +252,45 @@ def _get_full_hover_name(self, line, character, first_part):
):
return first_part + match_after.group(1)
return first_part

###################################################################################
# Goto definition
###################################################################################
def goto_definition(self, line, character):
line_fragment = self.document.lines[line][:character]
matchers = [
(
re.compile(r""".*{% ?(extends|include) ('|")([\w\-\./]*)$"""),
self.get_template_definition,
),
]
for regex, definition in matchers:
if match := regex.match(line_fragment):
return definition(line, character, match)
return None

def get_template_definition(self, line, character, match: Match):
if match_after := re.match(r'^(.*)".*', self.document.lines[line][character:]):
template_name = match.group(3) + match_after.group(1)
logger.debug(f"Find template goto definition for: {template_name}")
if template := self.workspace_index.templates.get(template_name):
location, path = template.path.split(":")
root_path = (
self.workspace_index.src_path
if location == "src"
else self.workspace_index.env_path
)
return Location(
uri=f"file://{root_path}/{path}",
range=Range(
start=Position(line=0, character=0),
end=Position(line=0, character=0),
),
)

def _get_full_definition_name(self, line, character, first_part):
if match_after := re.match(
r"^([\w\d]+).*", self.document.lines[line][character:]
):
return first_part + match_after.group(1)
return first_part
48 changes: 33 additions & 15 deletions djlsp/scripts/django-collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import re
import sys
from dataclasses import dataclass
from unittest.mock import patch

import django
Expand Down Expand Up @@ -319,7 +320,14 @@ def _parse_library(lib) -> dict:
}


def get_templates():
@dataclass
class Template:
path: str = ""
name: str = ""
content: str = ""


def get_templates(project_src_path):
template_files = {}
default_engine = Engine.get_default()
for templates_dir in [
Expand All @@ -337,19 +345,24 @@ def get_templates():

# Get used template (other apps can override templates)
template_files[template_name] = _parse_template(
_get_template_content(default_engine, template_name), template_name
project_src_path,
_get_template(default_engine, template_name),
)
return template_files


def _get_template_content(engine: Engine, template_name: str):
def _get_template(engine: Engine, template_name: str) -> Template:
for loader in engine.template_loaders:
for origin in loader.get_template_sources(template_name):
try:
return loader.get_contents(origin)
return Template(
path=str(origin),
name=template_name,
content=loader.get_contents(origin),
)
except Exception:
pass
return ""
return Template(name=template_name)


re_extends = re.compile(r""".*{% ?extends ['"](.*)['"] ?%}.*""")
Expand All @@ -373,31 +386,38 @@ def get_global_template_context():
return global_context


def _parse_template(content, template_name: str) -> dict:
def _parse_template(project_src_path, template: Template) -> dict:
extends = None
blocks = set()
for line in content.splitlines():
for line in template.content.splitlines():
if match := re_extends.match(line):
extends = match.group(1)
if match := re_block.match(line):
blocks.add(match.group(1))

path = ""
if template.path.startswith(project_src_path):
path = f"src:{template.path.removeprefix(project_src_path).lstrip('/')}"
elif template.path.startswith(sys.prefix):
path = f"env:{template.path.removeprefix(sys.prefix).lstrip('/')}"

return {
"path": path,
"extends": extends,
"blocks": list(blocks),
"context": get_wagtail_page_context(
template_name
template.name
), # TODO: Find view/model/contectprocessors
}


def collect_project_data():
def collect_project_data(project_src_path):
return {
"file_watcher_globs": get_file_watcher_globs(),
"static_files": get_static_files(),
"urls": get_urls(),
"libraries": get_libraries(),
"templates": get_templates(),
"templates": get_templates(project_src_path),
"global_template_context": get_global_template_context(),
"object_types": get_object_types(),
}
Expand Down Expand Up @@ -430,10 +450,8 @@ def get_default_django_settings_module():
parser.add_argument("--project-src", action="store", type=str)
args = parser.parse_args()

if args.project_src:
sys.path.insert(0, args.project_src)
else:
sys.path.insert(0, os.getcwd())
project_src_path = args.project_src if args.project_src else os.getcwd()
sys.path.insert(0, project_src_path)

django_settings_module = (
args.django_settings_module
Expand All @@ -448,4 +466,4 @@ def get_default_django_settings_module():

django.setup()

print(json.dumps(collect_project_data(), indent=4))
print(json.dumps(collect_project_data(project_src_path), indent=4))
35 changes: 29 additions & 6 deletions djlsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
from lsprotocol.types import (
INITIALIZE,
TEXT_DOCUMENT_COMPLETION,
TEXT_DOCUMENT_DEFINITION,
TEXT_DOCUMENT_HOVER,
WORKSPACE_DID_CHANGE_WATCHED_FILES,
CompletionList,
CompletionOptions,
CompletionParams,
DefinitionParams,
DidChangeWatchedFilesParams,
DidChangeWatchedFilesRegistrationOptions,
FileSystemWatcher,
Expand Down Expand Up @@ -66,6 +68,14 @@ def project_src_path(self):
return src_path
return self.workspace.root_path

@cached_property
def project_env_path(self):
for env_dir in self.ENV_DIRECTORIES:
if os.path.exists(
os.path.join(self.workspace.root_path, env_dir, "bin", "python")
):
return os.path.join(self.workspace.root_path, env_dir)

@property
def docker_compose_path(self):
return os.path.join(self.workspace.root_path, self.docker_compose_file)
Expand Down Expand Up @@ -103,6 +113,9 @@ def set_file_watcher_capability(self):
)

def get_django_data(self):
self.workspace_index.src_path = self.project_src_path
self.workspace_index.env_path = self.project_env_path

if self._has_valid_docker_service():
django_data = self._get_django_data_from_docker()
elif python_path := self._get_python_path():
Expand Down Expand Up @@ -131,12 +144,8 @@ def get_django_data(self):
self.set_file_watcher_capability()

def _get_python_path(self):
for env_dir in self.ENV_DIRECTORIES:
env_python_path = os.path.join(
self.workspace.root_path, env_dir, "bin", "python"
)
if os.path.exists(env_python_path):
return env_python_path
if self.project_env_path:
return os.path.join(self.project_env_path, "bin", "python")
return shutil.which("python3")

def _get_django_data_from_python_path(self, python_path):
Expand Down Expand Up @@ -307,6 +316,20 @@ def hover(ls: DjangoTemplateLanguageServer, params: HoverParams):
return None


@server.feature(TEXT_DOCUMENT_DEFINITION)
def goto_definition(ls: DjangoTemplateLanguageServer, params: DefinitionParams):
logger.info(f"COMMAND: {TEXT_DOCUMENT_DEFINITION}")
logger.debug(f"PARAMS: {params}")
try:
return TemplateParser(
workspace_index=ls.workspace_index,
document=ls.workspace.get_document(params.text_document.uri),
).goto_definition(params.position.line, params.position.character)
except Exception as e:
logger.error(e)
return None


@server.feature(WORKSPACE_DID_CHANGE_WATCHED_FILES)
def files_changed(
ls: DjangoTemplateLanguageServer, params: DidChangeWatchedFilesParams
Expand Down
5 changes: 5 additions & 0 deletions tests/test_django_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ def test_django_collect():
)

assert "django_app.html" in index.templates
assert (
index.templates["django_app.html"].path
== "src:django_app/templates/django_app.html"
)

assert "django_app.js" in index.static_files
assert "django_app:index" in index.urls
assert set(index.file_watcher_globs) == {
Expand Down

0 comments on commit 422698a

Please sign in to comment.