diff --git a/djlsp/constants.py b/djlsp/constants.py index acb2490..abd06c2 100644 --- a/djlsp/constants.py +++ b/djlsp/constants.py @@ -1,53 +1,37 @@ BUILTIN = "__builtins__" FALLBACK_DJANGO_DATA = { - "file_watcher_globs": [ - "**/templates/**", - "**/templatetags/**", - "**/static/**", - ], + "file_watcher_globs": ["**/templates/**", "**/templatetags/**", "**/static/**"], "static_files": [], "urls": [], - "templates": [], "libraries": { "__builtins__": { - "tags": [ - "autoescape", - "comment", - "cycle", - "csrf_token", - "debug", - "filter", - "firstof", - "for", - "if", - "ifchanged", - "load", - "lorem", - "now", - "regroup", - "resetcycle", - "spaceless", - "templatetag", - "url", - "verbatim", - "widthratio", - "with", - "block", - "extends", - "include", - "endautoescape", - "endfilter", - "empty", - "endfor", - "else", - "elif", - "endifendifchanged", - "endspaceless", - "endverbatim", - "endwith", - "endblock", - ], + "tags": { + "autoescape": {"inner_tags": [], "closing_tag": "endautoescape"}, + "comment": {"inner_tags": [], "closing_tag": "endcomment"}, + "cycle": {}, + "csrf_token": {}, + "debug": {}, + "filter": {"inner_tags": [], "closing_tag": "endfilter"}, + "firstof": {}, + "for": {"inner_tags": ["empty"], "closing_tag": "endfor"}, + "if": {"inner_tags": ["else", "elif"], "closing_tag": "endif"}, + "ifchanged": {"inner_tags": [], "closing_tag": "endifchanged"}, + "load": {}, + "lorem": {}, + "now": {}, + "regroup": {}, + "resetcycle": {}, + "spaceless": {"inner_tags": [], "closing_tag": "endspaceless"}, + "templatetag": {}, + "url": {}, + "verbatim": {"inner_tags": [], "closing_tag": "endverbatim"}, + "widthratio": {}, + "with": {"inner_tags": [], "closing_tag": "endwith"}, + "block": {"inner_tags": [], "closing_tag": "endblock"}, + "extends": {}, + "include": {}, + }, "filters": [ "addslashes", "capfirst", @@ -108,21 +92,23 @@ "pprint", ], }, - "cache": {"tags": ["cache", "endcache"], "filters": []}, + "cache": { + "tags": {"cache": {"inner_tags": [], "closing_tag": "endcache"}}, + "filters": [], + }, "i18n": { - "tags": [ - "get_available_languages", - "get_language_info", - "get_language_info_list", - "get_current_language", - "get_current_language_bidi", - "trans", - "translate", - "blocktrans", - "blocktranslate", - "language", - "endlanguage", - ], + "tags": { + "get_available_languages": {}, + "get_language_info": {}, + "get_language_info_list": {}, + "get_current_language": {}, + "get_current_language_bidi": {}, + "trans": {}, + "translate": {}, + "blocktrans": {}, + "blocktranslate": {}, + "language": {"inner_tags": [], "closing_tag": "endlanguage"}, + }, "filters": [ "language_name", "language_name_translated", @@ -131,22 +117,21 @@ ], }, "l10n": { - "tags": ["localize", "endlocalize"], + "tags": {"localize": {"inner_tags": [], "closing_tag": "endlocalize"}}, "filters": ["localize", "unlocalize"], }, "static": { - "tags": ["get_static_prefix", "get_media_prefix", "static"], + "tags": {"get_static_prefix": {}, "get_media_prefix": {}, "static": {}}, "filters": [], }, "tz": { - "tags": [ - "localtime", - "timezoninitializationOptionse", - "get_current_timezone", - "endlocaltime", - "endtimezone", - ], + "tags": { + "localtime": {"inner_tags": [], "closing_tag": "endlocaltime"}, + "timezone": {"inner_tags": [], "closing_tag": "endtimezone"}, + "get_current_timezone": {}, + }, "filters": ["localtime", "utc", "timezone"], }, }, + "templates": {}, } diff --git a/djlsp/index.py b/djlsp/index.py new file mode 100644 index 0000000..30f231a --- /dev/null +++ b/djlsp/index.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass, field + + +@dataclass +class Template: + name: str = "" + extends: str | None = None + blocks: list[str] | None = None + context: dict = field(default_factory=dict) + + +@dataclass +class Tag: + name: str = "" + inner_tags: list[str] = "" + closing_tag: str = "" + + +@dataclass +class Library: + name: str = "" + tags: dict[str, Tag] = field(default_factory=dict) + filters: list[str] = field(default_factory=list) + + +@dataclass +class WorkspaceIndex: + file_watcher_globs: [str] = field(default_factory=list) + static_files: [str] = field(default_factory=list) + urls: [str] = field(default_factory=list) + libraries: dict[str, Library] = field(default_factory=dict) + templates: dict[str, Template] = field(default_factory=dict) + + def update(self, django_data: dict): + self.file_watcher_globs = django_data.get( + "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.libraries = { + lib_name: Library( + name=lib_name, + filters=lib_data.get("filters", []), + tags={ + tag: Tag( + name=tag, + inner_tags=tag_options.get("inner_tags", []), + closing_tag=tag_options.get("closing_tag"), + ) + for tag, tag_options in lib_data.get("tags", {}).items() + }, + ) + for lib_name, lib_data in django_data.get("libraries", {}).items() + } + + self.templates = { + name: Template(name=name, **options) + for name, options in django_data.get("templates", {}).items() + } diff --git a/djlsp/parser.py b/djlsp/parser.py index e07e077..d732eb7 100644 --- a/djlsp/parser.py +++ b/djlsp/parser.py @@ -6,6 +6,7 @@ from pygls.workspace import TextDocument from djlsp.constants import BUILTIN +from djlsp.index import WorkspaceIndex logger = logging.getLogger(__name__) @@ -19,8 +20,8 @@ class TemplateParser: re_filter = re.compile(r"^.*({%|{{) ?[\w \.\|]*\|(\w*)$") re_template = re.compile(r""".*{% ?(extends|include) ('|")([\w\-:]*)$""") - def __init__(self, django_data: dict, document: TextDocument): - self.django_data = django_data + def __init__(self, workspace_index: WorkspaceIndex, document: TextDocument): + self.workspace_index: WorkspaceIndex = workspace_index self.document: TextDocument = document @cached_property @@ -32,7 +33,7 @@ def loaded_libraries(self): [ lib for lib in match.group(1).strip().split(" ") - if lib in self.django_data["libraries"] + if lib in self.workspace_index.libraries ] ) logger.debug(f"Loaded libraries: {loaded}") @@ -62,7 +63,7 @@ def get_load_completions(self, match: Match): return sorted( [ lib - for lib in self.django_data["libraries"].keys() + for lib in self.workspace_index.libraries.keys() if lib != BUILTIN and lib.startswith(prefix) ] ) @@ -73,7 +74,7 @@ def get_static_completions(self, match: Match): return sorted( [ static_file - for static_file in self.django_data["static_files"] + for static_file in self.workspace_index.static_files if static_file.startswith(prefix) ] ) @@ -82,17 +83,16 @@ def get_url_completions(self, match: Match): prefix = match.group(2) logger.debug(f"Find url matches for: {prefix}") return sorted( - [url for url in self.django_data["urls"] if url.startswith(prefix)] + [url for url in self.workspace_index.urls if url.startswith(prefix)] ) def get_template_completions(self, match: Match): prefix = match.group(3) logger.debug(f"Find {match.group(1)} matches for: {prefix}") - logger.debug(self.django_data["templates"]) return sorted( [ template - for template in self.django_data["templates"] + for template in self.workspace_index.templates if template.startswith(prefix) ] ) @@ -102,9 +102,14 @@ def get_tag_completions(self, match: Match): logger.debug(f"Find tag matches for: {prefix}") tags = [] - for name, lib in self.django_data["libraries"].items(): - if name in self.loaded_libraries: - tags.extend(lib["tags"]) + for lib_name in self.loaded_libraries: + if lib := self.workspace_index.libraries.get(lib_name): + for tag in lib.tags.values(): + tags.append(tag.name) + # TODO: Only add inner/clossing if there is opening tag + tags.extend(tag.inner_tags) + if tag.closing_tag: + tags.append(tag.closing_tag) return sorted([tag for tag in tags if tag.startswith(prefix)]) @@ -112,9 +117,9 @@ def get_filter_completions(self, match: Match): prefix = match.group(2) logger.debug(f"Find filter matches for: {prefix}") filters = [] - for name, lib in self.django_data["libraries"].items(): - if name in self.loaded_libraries: - filters.extend(lib["filters"]) + for lib_name in self.loaded_libraries: + if lib := self.workspace_index.libraries.get(lib_name): + filters.extend(lib.filters) return sorted( [filter_name for filter_name in filters if filter_name.startswith(prefix)] ) diff --git a/djlsp/scripts/django-collector.py b/djlsp/scripts/django-collector.py index 3665971..b337e2e 100644 --- a/djlsp/scripts/django-collector.py +++ b/djlsp/scripts/django-collector.py @@ -3,6 +3,7 @@ import inspect import json import os +import re import sys import django @@ -18,47 +19,68 @@ # Some tags are added with a Node, like end*, elif else. # TODO: Find a way of collecting these, for now hardcoded list LIBRARIES_NODE_TAGS = { - "__builtins__": [ - # autoescape - "endautoescape", - # filter - "endfilter", - # for - "empty", - "endfor", - # if - "else", - "elif", - "endif" - # ifchanged - "endifchanged", - # spaceless - "endspaceless", - # verbatim - "endverbatim", - # with - "endwith", - # block - "endblock", - ], - "cache": [ - # cache - "endcache", - ], - "i18n": [ - # language - "endlanguage", - ], - "l10n": [ - # localize - "endlocalize", - ], - "tz": [ - # localtime - "endlocaltime", - # timezone - "endtimezone", - ], + "__builtins__": { + "autoescape": { + "closing_tag": "endautoescape", + }, + "filter": { + "closing_tag": "endfilter", + }, + "for": { + "inner_tags": [ + "empty", + ], + "closing_tag": "endfor", + }, + "if": { + "inner_tags": [ + "else", + "elif", + ], + "closing_tag": "endif", + }, + "ifchanged": { + "closing_tag": "endifchanged", + }, + "spaceless": { + "closing_tag": "endspaceless", + }, + "verbatim": { + "closing_tag": "endverbatim", + }, + "with": { + "closing_tag": "endwith", + }, + "block": { + "closing_tag": "endblock", + }, + "comment": { + "closing_tag": "endcomment", + }, + }, + "cache": { + "cache": { + "closing_tag": "endcache", + } + }, + "i18n": { + "language": { + "closing_tag": "endlanguage", + } + }, + "l10n": { + "localize": { + "closing_tag": "endlocalize", + } + }, + "tz": { + "localtime": { + "closing_tag": "endlocaltime", + }, + "timezone": { + "closing_tag": "endtimezone", + }, + }, } @@ -135,7 +157,7 @@ def recursive_get_views(urlpatterns, namespace=None): def get_libraries(): libraries = { "__builtins__": { - "tags": [], + "tags": {}, "filters": [], } } @@ -143,7 +165,7 @@ def get_libraries(): # Collect builtins for lib_mod_path in Engine.get_default().builtins: lib = importlib.import_module(lib_mod_path).register - libraries["__builtins__"]["tags"].extend(list(lib.tags.keys())) + libraries["__builtins__"]["tags"].update({tag: {} for tag in lib.tags.keys()}) libraries["__builtins__"]["filters"].extend(list(lib.filters.keys())) # Get Django templatetags @@ -156,7 +178,7 @@ def get_libraries(): lib = get_installed_libraries()[django_lib] lib = importlib.import_module(lib).register libraries[django_lib] = { - "tags": list(lib.tags.keys()), + "tags": {tag: {} for tag in lib.tags.keys()}, "filters": list(lib.filters.keys()), } except (InvalidTemplateLibrary, KeyError): @@ -181,32 +203,78 @@ def get_libraries(): continue libraries[taglib] = { - "tags": list(lib.tags.keys()), + "tags": {tag: {} for tag in lib.tags.keys()}, "filters": list(lib.filters.keys()), } # Add node tags for lib_name, tags in LIBRARIES_NODE_TAGS.items(): if lib_name in libraries: - libraries[lib_name]["tags"].extend(tags) + for tag, options in tags.items(): + if tag in libraries[lib_name]["tags"]: + libraries[lib_name]["tags"][tag]["inner_tags"] = options.get( + "inner_tags", [] + ) + libraries[lib_name]["tags"][tag]["closing_tag"] = options.get( + "closing_tag" + ) return libraries def get_templates(): - template_files = [] + template_files = {} + default_engine = Engine.get_default() for templates_dir in [ - *Engine.get_default().dirs, + *default_engine.dirs, *get_app_template_dirs("templates"), ]: for root, dirs, files in os.walk(templates_dir): for file in files: - template_files.append( - os.path.relpath(os.path.join(root, file), templates_dir) + template_name = os.path.relpath(os.path.join(root, file), templates_dir) + + if template_name in template_files: + # Skip already procecesed template + # (template have duplicates because other apps can override) + continue + + # Get used template (other apps can override templates) + template_files[template_name] = _parse_template( + _get_template_content(default_engine, template_name) ) return template_files +def _get_template_content(engine: Engine, template_name): + for loader in engine.template_loaders: + for origin in loader.get_template_sources(template_name): + try: + return loader.get_contents(origin) + except Exception: + pass + return "" + + +re_extends = re.compile(r""".*{% ?extends ['"](.*)['"] ?%}.*""") +re_block = re.compile(r".*{% ?block (\w*) ?%}.*") + + +def _parse_template(content): + extends = None + blocks = set() + for line in content.splitlines(): + if match := re_extends.match(line): + extends = match.group(1) + if match := re_block.match(line): + blocks.add(match.group(1)) + + return { + "extends": extends, + "blocks": list(blocks), + "context": {}, # TODO: Find view/model/contectprocessors + } + + def collect_project_data(): return { "file_watcher_globs": get_file_watcher_globs(), diff --git a/djlsp/server.py b/djlsp/server.py index 67a1e79..a31f279 100644 --- a/djlsp/server.py +++ b/djlsp/server.py @@ -24,6 +24,7 @@ from djlsp import __version__ from djlsp.constants import FALLBACK_DJANGO_DATA +from djlsp.index import WorkspaceIndex from djlsp.parser import TemplateParser logger = logging.getLogger(__name__) @@ -47,11 +48,12 @@ class DjangoTemplateLanguageServer(LanguageServer): def __init__(self, *args): super().__init__(*args) self.file_watcher_id = str(uuid.uuid4()) - self.current_file_watcher_globs = FALLBACK_DJANGO_DATA["file_watcher_globs"] + self.current_file_watcher_globs = [] self.docker_compose_file = "docker-compose.yml" self.docker_compose_service = "django" self.django_settings_module = "" - self.django_data = FALLBACK_DJANGO_DATA + self.workspace_index = WorkspaceIndex() + self.workspace_index.update(FALLBACK_DJANGO_DATA) def set_initialization_options(self, options: dict): self.docker_compose_file = options.get( @@ -95,7 +97,7 @@ def get_django_data(self): if django_data: # TODO: Maybe validate data - self.django_data = django_data + self.workspace_index.update(django_data) logger.info("Collected project Django data:") logger.info(f" - Libraries: {len(django_data['libraries'])}") logger.info(f" - Templates: {len(django_data['templates'])}") @@ -104,10 +106,10 @@ def get_django_data(self): else: logger.info("Could not collect Django data") - if "file_watcher_globs" in django_data and set( - django_data["file_watcher_globs"] - ) != set(self.current_file_watcher_globs): - self.current_file_watcher_globs = django_data["file_watcher_globs"] + if set(self.workspace_index.file_watcher_globs) != set( + self.current_file_watcher_globs + ): + self.current_file_watcher_globs = self.workspace_index.file_watcher_globs self.set_file_watcher_capability() def _get_python_path(self): @@ -219,7 +221,6 @@ def initialized(ls: DjangoTemplateLanguageServer, params: InitializeParams): if params.initialization_options: ls.set_initialization_options(params.initialization_options) ls.get_django_data() - ls.set_file_watcher_capability() @server.feature( @@ -230,7 +231,7 @@ def completions(ls: DjangoTemplateLanguageServer, params: CompletionParams): logger.debug(f"PARAMS: {params}") items = [] document = server.workspace.get_document(params.text_document.uri) - template = TemplateParser(ls.django_data, document) + template = TemplateParser(ls.workspace_index, document) for completion in template.completions( params.position.line, params.position.character ):