diff --git a/djlsp/constants.py b/djlsp/constants.py index abd06c2..c0b74a0 100644 --- a/djlsp/constants.py +++ b/djlsp/constants.py @@ -134,4 +134,7 @@ }, }, "templates": {}, + "global_template_context": { + "csrf_token": {}, + }, } diff --git a/djlsp/index.py b/djlsp/index.py index 30f231a..82acae8 100644 --- a/djlsp/index.py +++ b/djlsp/index.py @@ -30,6 +30,8 @@ class WorkspaceIndex: urls: [str] = field(default_factory=list) 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) + object_types: dict[str, dict] = field(default_factory=dict) def update(self, django_data: dict): self.file_watcher_globs = django_data.get( @@ -58,3 +60,6 @@ def update(self, django_data: dict): name: Template(name=name, **options) for name, options in django_data.get("templates", {}).items() } + + self.global_template_context = django_data.get("global_template_context", {}) + self.object_types = django_data.get("object_types", {}) diff --git a/djlsp/parser.py b/djlsp/parser.py index d732eb7..87ebbad 100644 --- a/djlsp/parser.py +++ b/djlsp/parser.py @@ -19,6 +19,7 @@ class TemplateParser: re_tag = re.compile(r"^.*{% ?(\w*)$") re_filter = re.compile(r"^.*({%|{{) ?[\w \.\|]*\|(\w*)$") re_template = re.compile(r""".*{% ?(extends|include) ('|")([\w\-:]*)$""") + re_context = re.compile(r".*({{|{% \w+).* ([\w\d_\.]*)$") def __init__(self, workspace_index: WorkspaceIndex, document: TextDocument): self.workspace_index: WorkspaceIndex = workspace_index @@ -54,6 +55,8 @@ def completions(self, line, character): return self.get_tag_completions(match) elif match := self.re_filter.match(line_fragment): return self.get_filter_completions(match) + elif match := self.re_context.match(line_fragment): + return self.get_context_completions(match) return [] @@ -123,3 +126,28 @@ def get_filter_completions(self, match: Match): return sorted( [filter_name for filter_name in filters if filter_name.startswith(prefix)] ) + + def get_context_completions(self, match: Match): + prefix = match.group(2) + logger.debug(f"Find context matches for: {prefix}") + context = self.workspace_index.global_template_context.copy() + + prefix, lookup_context = self._recursive_context_lookup( + prefix.strip().split("."), context + ) + + return [var for var in lookup_context if var.startswith(prefix)] + + def _recursive_context_lookup(self, parts: [str], context: dict[str, str]): + if len(parts) == 1: + return parts[0], context + + variable, *parts = parts + + # Get new context + if variable_type := context.get(variable): + if new_context := self.workspace_index.object_types.get(variable_type): + return self._recursive_context_lookup(parts, new_context) + + # No suggesions found + return "", [] diff --git a/djlsp/scripts/django-collector.py b/djlsp/scripts/django-collector.py index 19b876c..3defd66 100644 --- a/djlsp/scripts/django-collector.py +++ b/djlsp/scripts/django-collector.py @@ -9,6 +9,7 @@ import django from django.apps import apps from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.staticfiles.finders import get_finders from django.template.backends.django import get_installed_libraries from django.template.engine import Engine @@ -83,6 +84,51 @@ }, } +# Context processors are functions and therefore hard to parse +# Use hardcoded mapping for know context processors. +TEMPLATE_CONTEXT_PROCESSORS = { + # Django + "django.template.context_processors.csrf": { + "csrf_token": None, + }, + "django.template.context_processors.debug": { + "debug": None, + "sql_queries": None, + }, + "django.template.context_processors.i18n": { + "LANGUAGES": None, + "LANGUAGE_CODE": None, + "LANGUAGE_BIDI": None, + }, + "django.template.context_processors.tz": { + "TIME_ZONE": None, + }, + "django.template.context_processors.static": { + "STATIC_URL": None, + }, + "django.template.context_processors.media": { + "MEDIA_URL": None, + }, + "django.template.context_processors.request": { + "request": None, + }, + # Django: auth + "django.contrib.auth.context_processors.auth": { + "user": None, + "perms": None, + }, + # Django: messages + "django.contrib.messages.context_processors.messages": { + "messages": None, + "DEFAULT_MESSAGE_LEVELS": None, + }, + # Wagtail: settings + "wagtail.contrib.settings.context_processors.settings": { + # TODO: add fake settings object type with reference to models + "settings": None, + }, +} + def get_file_watcher_globs(): """ @@ -268,6 +314,23 @@ def _get_template_content(engine: Engine, template_name): re_block = re.compile(r".*{% ?block (\w*) ?%}.*") +def get_global_template_context(): + global_context = {} + + # Update object types + TEMPLATE_CONTEXT_PROCESSORS["django.contrib.auth.context_processors.auth"][ + "user" + ] = f"{get_user_model().__module__}.{get_user_model().__name__}" + + for context_processor in Engine.get_default().template_context_processors: + module_path = ".".join( + [context_processor.__module__, context_processor.__name__] + ) + if context := TEMPLATE_CONTEXT_PROCESSORS.get(module_path): + global_context.update(context) + return global_context + + def _parse_template(content): extends = None blocks = set() @@ -291,6 +354,7 @@ def collect_project_data(): "urls": get_urls(), "libraries": get_libraries(), "templates": get_templates(), + "global_template_context": get_global_template_context(), "object_types": get_object_types(), } diff --git a/djlsp/server.py b/djlsp/server.py index a31f279..4a4a466 100644 --- a/djlsp/server.py +++ b/djlsp/server.py @@ -103,6 +103,9 @@ def get_django_data(self): logger.info(f" - Templates: {len(django_data['templates'])}") logger.info(f" - Static files: {len(django_data['static_files'])}") logger.info(f" - Urls: {len(django_data['urls'])}") + logger.info( + f" - Global context: {len(django_data['global_template_context'])}" + ) else: logger.info("Could not collect Django data")