From f48b840cfa478d6bfeb6e16f24fd5b650c70e4f2 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Wed, 20 Nov 2024 10:41:56 +0100 Subject: [PATCH 01/34] compile: fix flag values gtk builder does not support combining interger values with | in flags properties, so the short names are used instead. --- blueprintcompiler/language/values.py | 2 +- tests/samples/flags.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 63cf4fc..e060b65 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -216,7 +216,7 @@ class Flag(AstNode): if not isinstance(type, Enumeration): return None elif member := type.members.get(self.name): - return member.value + return member.name else: return None diff --git a/tests/samples/flags.ui b/tests/samples/flags.ui index 2f0a26e..d13c424 100644 --- a/tests/samples/flags.ui +++ b/tests/samples/flags.ui @@ -7,7 +7,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - 1|4 + is_service|handles_open 1 From 2ae41020abfb04649e9791f57c84fc708d428283 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Thu, 21 Nov 2024 09:28:40 +0100 Subject: [PATCH 02/34] Fix flag return value type --- blueprintcompiler/language/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index e060b65..4cd600d 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -211,7 +211,7 @@ class Flag(AstNode): return self.tokens["value"] @property - def value(self) -> T.Optional[int]: + def value(self) -> T.Optional[str]: type = self.context[ValueTypeCtx].value_type if not isinstance(type, Enumeration): return None From e07da3c33946e7ab4afed9c564a9e7ae0b3fbbb8 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Tue, 26 Nov 2024 10:52:37 +0100 Subject: [PATCH 03/34] flags: use nick instead of name --- blueprintcompiler/language/values.py | 2 +- tests/samples/flags.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 4cd600d..96787ee 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -216,7 +216,7 @@ class Flag(AstNode): if not isinstance(type, Enumeration): return None elif member := type.members.get(self.name): - return member.name + return member.nick else: return None diff --git a/tests/samples/flags.ui b/tests/samples/flags.ui index d13c424..44eb2c4 100644 --- a/tests/samples/flags.ui +++ b/tests/samples/flags.ui @@ -7,7 +7,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler. - is_service|handles_open + is-service|handles-open 1 From aa13c8f5af137bbedd1781d903e923d45d7c41fb Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 14:27:59 -0600 Subject: [PATCH 04/34] Warn about single-quoted translated strings gettext only recognizes double quoted strings --- blueprintcompiler/language/values.py | 13 +++++++++++++ docs/translations.rst | 2 ++ tests/sample_errors/single_quoted_translated.blp | 5 +++++ tests/sample_errors/single_quoted_translated.err | 1 + 4 files changed, 21 insertions(+) create mode 100644 tests/sample_errors/single_quoted_translated.blp create mode 100644 tests/sample_errors/single_quoted_translated.err diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 5556d99..cb80975 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -58,6 +58,19 @@ class Translated(AstNode): f"Cannot convert translated string to {expected_type.full_name}" ) + @validate("context") + def context_double_quoted(self): + if self.translate_context is None: + return + + if not str(self.group.tokens["context"]).startswith('"'): + raise CompileWarning("gettext may not recognize single-quoted strings") + + @validate("string") + def string_double_quoted(self): + if not str(self.group.tokens["string"]).startswith('"'): + raise CompileWarning("gettext may not recognize single-quoted strings") + @docs() def ref_docs(self): return get_docs_section("Syntax Translated") diff --git a/docs/translations.rst b/docs/translations.rst index 7ebf929..7af2099 100644 --- a/docs/translations.rst +++ b/docs/translations.rst @@ -24,6 +24,8 @@ If you're using Meson's `i18n module Date: Fri, 17 Jan 2025 17:04:52 -0600 Subject: [PATCH 05/34] Release v0.16.0 --- NEWS.md | 32 ++++++++++++++++++++++++++++++++ docs/flatpak.rst | 2 +- meson.build | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 389f82c..a12dab0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,35 @@ +# v0.16.0 + +## Added +- Added more "go to reference" implementations in the language server +- Added semantic token support for flag members in the language server +- Added property documentation to the hover tooltip for notify signals +- The language server now shows relevant sections of the reference documentation when hovering over keywords and symbols +- Added `not-swapped` flag to signal handlers, which may be needed for signal handlers that specify an object +- Added expression literals, which allow you to specify a Gtk.Expression property (as opposed to the existing expression support, which is for property bindings) + +## Changed +- The formatter adds trailing commas to lists (Alexey Yerin) +- The formatter removes trailing whitespace from comments (Alexey Yerin) +- Autocompleting a commonly translated property automatically adds the `_("")` syntax +- Marking a single-quoted string as translatable now generates a warning, since gettext does not recognize it when using the configuration recommended in the blueprint documentation + +## Fixed +- Added support for libgirepository-2.0 so that blueprint doesn't crash due to import conflicts on newer versions of PyGObject (Jordan Petridis) +- Fixed a bug when decompiling/porting files with enum values +- Fixed several issues where tests would fail with versions of GTK that added new deprecations +- Addressed a problem with the language server protocol in some editors (Luoyayu) +- Fixed an issue where the compiler would crash instead of reporting compiler errors +- Fixed a crash in the language server that occurred when a detailed signal (e.g. `notify::*`) was not complete +- The language server now properly implements the shutdown command, fixing support for some editors and improving robustness when restarting (Alexey Yerin) +- Marking a string in an array as translatable now generates an error, since it doesn't work +- + +## Documentation +- Added mention of `null` in the Literal Values section +- Add apps to Built with Blueprint section (Benedek Dévényi, Vladimir Vaskov) +- Corrected and updated many parts of the documentation + # v0.14.0 ## Added diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 0071d2f..86112cf 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -17,7 +17,7 @@ a module in your flatpak manifest: { "type": "git", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", - "tag": "v0.14.0" + "tag": "v0.16.0" } ] } diff --git a/meson.build b/meson.build index 63d9489..f298d15 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('blueprint-compiler', - version: '0.14.0', + version: '0.16.0', ) prefix = get_option('prefix') From 404ae767870b6da6005f2f4aaa878deed82829e5 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 17 Jan 2025 17:25:21 -0600 Subject: [PATCH 06/34] Update MAINTENANCE.md --- MAINTENANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 220c117..3ab4fa2 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -8,7 +8,7 @@ in the NEWS file. 3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. 4. Create a "Post-release version bump" commit. 5. Go to the Releases page in GitLab and create a new release from the tag. -6. Announce the release through relevant channels (Twitter, TWIG, etc.) +6. Announce the release through relevant channels (Mastodon, TWIG, etc.) ## Related projects From a4e0c3701b1510191f72cc606c2fa20d97800444 Mon Sep 17 00:00:00 2001 From: Chris Mayo Date: Thu, 30 Jan 2025 19:23:40 +0000 Subject: [PATCH 07/34] docs: Update overview example using format and compile --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 34b942c..a71b968 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces. using Gtk 4.0; - template MyAppWindow : ApplicationWindow { + template $MyAppWindow: ApplicationWindow { default-width: 600; default-height: 300; title: _("Hello, Blueprint!"); @@ -35,7 +35,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces. HeaderBar {} Label { - label: bind MyAppWindow.main_text; + label: bind template.main_text; } } From 394014429e819347d107eb5fa9fd2871bdb3d0ac Mon Sep 17 00:00:00 2001 From: Sertonix Date: Thu, 20 Mar 2025 10:52:57 +0000 Subject: [PATCH 08/34] Sort keys in collect-sections.py This makes sure that the reference_docs.json file is build reproducible. Ref https://reproducible-builds.org/ --- docs/collect-sections.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/collect-sections.py b/docs/collect-sections.py index e6227e7..07bc5f6 100755 --- a/docs/collect-sections.py +++ b/docs/collect-sections.py @@ -132,5 +132,8 @@ if __name__ == "__main__": # print the sections to a json file with open(outfile, "w") as f: json.dump( - {name: section.to_json() for name, section in sections.items()}, f, indent=2 + {name: section.to_json() for name, section in sections.items()}, + f, + indent=2, + sort_keys=True, ) From f93d5d2acd53365f70e48a0002e91e94be37f733 Mon Sep 17 00:00:00 2001 From: Tom Greig Date: Fri, 28 Mar 2025 20:26:19 +0000 Subject: [PATCH 09/34] Handle nested CDATA from nested templates When putting CDATA into the output, any instances of ']]>' in the text are replaced with ']]]]>'. This allows nested templates, e.g. from list views inside list views to work properly. --- blueprintcompiler/outputs/xml/xml_emitter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index ea91e03..d34eff4 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -73,6 +73,7 @@ class XmlEmitter: self._needs_newline = False def put_cdata(self, text: str): + text = text.replace("]]>", "]]]]>") self.result += f"" self._needs_newline = False From cc09f3d3bb8e6f273072c1b4727929bf7b0bad83 Mon Sep 17 00:00:00 2001 From: Tom Greig Date: Sun, 30 Mar 2025 10:27:11 +0100 Subject: [PATCH 10/34] Add tests for nested templates Basically just a copy of the list_factory test, but with an extra copy of the list factory inside it. --- tests/samples/list_factory_nested.blp | 17 +++++++++ tests/samples/list_factory_nested.ui | 44 +++++++++++++++++++++++ tests/samples/list_factory_nested_dec.blp | 18 ++++++++++ 3 files changed, 79 insertions(+) create mode 100644 tests/samples/list_factory_nested.blp create mode 100644 tests/samples/list_factory_nested.ui create mode 100644 tests/samples/list_factory_nested_dec.blp diff --git a/tests/samples/list_factory_nested.blp b/tests/samples/list_factory_nested.blp new file mode 100644 index 0000000..86a59b3 --- /dev/null +++ b/tests/samples/list_factory_nested.blp @@ -0,0 +1,17 @@ +using Gtk 4.0; + +Gtk.ListView { + factory: Gtk.BuilderListItemFactory list_item_factory { + template ListItem { + child: Gtk.ListView { + factory: Gtk.BuilderListItemFactory list_item_factory { + template ListItem { + child: Gtk.Label { + label: bind template.item as <$MyObject>.name; + }; + } + }; + }; + } + }; +} diff --git a/tests/samples/list_factory_nested.ui b/tests/samples/list_factory_nested.ui new file mode 100644 index 0000000..44cdb2b --- /dev/null +++ b/tests/samples/list_factory_nested.ui @@ -0,0 +1,44 @@ + + + + + + + + + + +]]> + + + + diff --git a/tests/samples/list_factory_nested_dec.blp b/tests/samples/list_factory_nested_dec.blp new file mode 100644 index 0000000..755491c --- /dev/null +++ b/tests/samples/list_factory_nested_dec.blp @@ -0,0 +1,18 @@ +using Gtk 4.0; + +ListView { + factory: BuilderListItemFactory list_item_factory { + template ListItem { + child: ListView { + factory: BuilderListItemFactory list_item_factory { + template ListItem { + child: Label { + label: bind template.item as <$MyObject>.name; + }; + } + }; + }; + } + }; +} + From f50b898e4ce74d84cfa92ce44e90f9aaca2ec133 Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 1 Apr 2025 19:27:59 -0500 Subject: [PATCH 11/34] adw_breakpoint: Fix crash in language server Fix a crash that happened when an AdwBreakpointSetter rule was incomplete, such as when you're still typing it. Fixes #189. --- blueprintcompiler/language/adw_breakpoint.py | 9 ++++++--- blueprintcompiler/outputs/xml/__init__.py | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py index 4ad5b24..3d2c10d 100644 --- a/blueprintcompiler/language/adw_breakpoint.py +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -81,8 +81,8 @@ class AdwBreakpointSetter(AstNode): return self.tokens["property"] @property - def value(self) -> Value: - return self.children[Value][0] + def value(self) -> T.Optional[Value]: + return self.children[Value][0] if len(self.children[Value]) > 0 else None @property def gir_class(self) -> T.Optional[GirType]: @@ -106,7 +106,10 @@ class AdwBreakpointSetter(AstNode): return None @property - def document_symbol(self) -> DocumentSymbol: + def document_symbol(self) -> T.Optional[DocumentSymbol]: + if self.value is None: + return None + return DocumentSymbol( f"{self.object_id}.{self.property_name}", SymbolKind.Property, diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 5c03761..15850f7 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -308,6 +308,9 @@ class XmlOutput(OutputFormat): elif isinstance(extension, AdwBreakpointSetters): for setter in extension.setters: + if setter.value is None: + continue + attrs = {} if isinstance(setter.value.child, Translated): From 6a77bfee0a5b2a03390c4ed7f945902a070ef697 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Apr 2025 13:27:20 -0500 Subject: [PATCH 12/34] tests: Fix typing --- tests/test_tokenizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tokenizer.py b/tests/test_tokenizer.py index 2bca595..ad5f828 100644 --- a/tests/test_tokenizer.py +++ b/tests/test_tokenizer.py @@ -25,7 +25,7 @@ from blueprintcompiler.tokenizer import Token, TokenType, tokenize class TestTokenizer(unittest.TestCase): - def assert_tokenize(self, string: str, expect: [Token]): + def assert_tokenize(self, string: str, expect: list[Token]): try: tokens = tokenize(string) self.assertEqual(len(tokens), len(expect)) From e9d61cb6f915d6688dc46cb9142eb6acae7110f7 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 25 Apr 2025 18:15:15 -0500 Subject: [PATCH 13/34] Update URLs after move to GNOME namespace on GitLab --- .gitlab-ci.yml | 4 ++-- blueprintcompiler/errors.py | 2 +- blueprintcompiler/interactive_port.py | 4 ++-- docs/collect-sections.py | 2 +- docs/flatpak.rst | 2 +- docs/index.rst | 2 +- docs/reference/extensions.rst | 2 +- docs/setup.rst | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1ec071e..6d373cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - pages build: - image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + image: registry.gitlab.gnome.org/gnome/blueprint-compiler stage: build script: - black --check --diff ./ tests @@ -33,7 +33,7 @@ build: path: coverage.xml fuzz: - image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + image: registry.gitlab.gnome.org/gnome/blueprint-compiler stage: build script: - meson _build diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 1e7297c..df1c2e1 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -219,7 +219,7 @@ def report_bug(): # pragma: no cover f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, along with the input file(s) if possible, on GitLab: -{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue +{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue {Colors.CLEAR}""" ) diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index 0c37885..12dd485 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -71,7 +71,7 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: print( f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: -{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" +{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" ) return CouldNotPort("does not compile") @@ -136,7 +136,7 @@ def step1(): wrap.write( f"""[wrap-git] directory = blueprint-compiler -url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git revision = {VERSION} depth = 1 diff --git a/docs/collect-sections.py b/docs/collect-sections.py index 07bc5f6..a2dd004 100755 --- a/docs/collect-sections.py +++ b/docs/collect-sections.py @@ -9,7 +9,7 @@ from pathlib import Path __all__ = ["get_docs_section"] -DOCS_ROOT = "https://jwestman.pages.gitlab.gnome.org/blueprint-compiler" +DOCS_ROOT = "https://gnome.pages.gitlab.gnome.org/blueprint-compiler" sections: dict[str, "Section"] = {} diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 86112cf..8081c8d 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -16,7 +16,7 @@ a module in your flatpak manifest: "sources": [ { "type": "git", - "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", + "url": "https://gitlab.gnome.org/GNOME/blueprint-compiler", "tag": "v0.16.0" } ] diff --git a/docs/index.rst b/docs/index.rst index a71b968..6cd130f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,7 +59,7 @@ Features Links ----- -- `Source code `_ +- `Source code `_ - `Workbench `_ lets you try, preview and export Blueprint - `GNOME Builder `_ provides builtin support - `Vim syntax highlighting plugin by thetek42 `_ diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index 0961d14..2fd5dbb 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -10,7 +10,7 @@ Properties are the main way to set values on objects, but they are limited by th Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() `_ for internal details. - Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. + Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. .. rst-class:: grammar-block diff --git a/docs/setup.rst b/docs/setup.rst index 839f8f6..914c753 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -8,7 +8,7 @@ Setting up Blueprint on a new or existing project Using the porting tool ~~~~~~~~~~~~~~~~~~~~~~ -Clone `blueprint-compiler `_ +Clone `blueprint-compiler `_ from source. You can install it using ``meson _build`` and ``ninja -C _build install``, or you can leave it uninstalled. @@ -29,7 +29,7 @@ blueprint-compiler works as a meson subproject. [wrap-git] directory = blueprint-compiler - url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git + url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git revision = main depth = 1 From 3816f4fe8da291704d54a9dd945ba253b9233ca1 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 25 Apr 2025 18:26:45 -0500 Subject: [PATCH 14/34] Add .doap file --- blueprint-compiler.doap | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 blueprint-compiler.doap diff --git a/blueprint-compiler.doap b/blueprint-compiler.doap new file mode 100644 index 0000000..f3e4000 --- /dev/null +++ b/blueprint-compiler.doap @@ -0,0 +1,27 @@ + + + Blueprint + A modern language for creating GTK interfaces + Blueprint is a language and associated tooling for building user interfaces for GTK. + + Python + + + + + + + + James Westman + + jwestman + + + From a83c7e936dafa19afd9753138fd66b7e3aaee008 Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 25 Apr 2025 18:32:33 -0500 Subject: [PATCH 15/34] black: Update formatting --- blueprintcompiler/parse_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index ae062fb..e590539 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -17,7 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -""" Utilities for parsing an AST from a token stream. """ +"""Utilities for parsing an AST from a token stream.""" import typing as T from enum import Enum From a12d3f5c81ee231f8bdb87cb06cf8afe5e35f4ad Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 25 Apr 2025 20:13:01 -0500 Subject: [PATCH 16/34] decompile: Fix bug in lookup tags A lookup tag with no type attribute would crash the decompiler, even if that was valid. This wasn't caught by the tests since blueprint never generates such XML. Also fixed a bug in the tests that caused decompiler-only tests not to run. --- blueprintcompiler/decompiler.py | 8 +++++--- blueprintcompiler/language/expression.py | 12 +++++++++--- tests/samples/issue_187.ui | 15 +++++++++++++++ tests/samples/issue_187_dec.blp | 7 +++++++ tests/test_samples.py | 10 +++------- 5 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 tests/samples/issue_187.ui create mode 100644 tests/samples/issue_187_dec.blp diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index de6c06f..850b6d8 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -255,7 +255,11 @@ def decompile_element( ctx._node_stack.append(xml) ctx.start_block() - gir = decompiler(*args, **kwargs) + + try: + gir = decompiler(*args, **kwargs) + except TypeError as e: + raise UnsupportedError(tag=xml.tag) if not decompiler._skip_children: for child in xml.children: @@ -266,8 +270,6 @@ def decompile_element( except UnsupportedError as e: raise e - except TypeError as e: - raise UnsupportedError(tag=xml.tag) def decompile(data: str) -> str: diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index e0b4246..de6fbf1 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -302,12 +302,18 @@ expr.children = [ @decompiler("lookup", skip_children=True, cdata=True) def decompile_lookup( - ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str + ctx: DecompileCtx, + gir: gir.GirContext, + cdata: str, + name: str, + type: T.Optional[str] = None, ): if ctx.parent_node is not None and ctx.parent_node.tag == "property": ctx.print("expr ") - if t := ctx.type_by_cname(type): + if type is None: + type = "" + elif t := ctx.type_by_cname(type): type = decompile.full_name(t) else: type = "$" + type @@ -327,7 +333,7 @@ def decompile_lookup( if constant == ctx.template_class: ctx.print("template." + name) elif constant == "": - ctx.print("item as <" + type + ">." + name) + ctx.print(f"item as <{type}>.{name}") else: ctx.print(constant + "." + name) return diff --git a/tests/samples/issue_187.ui b/tests/samples/issue_187.ui new file mode 100644 index 0000000..941a00f --- /dev/null +++ b/tests/samples/issue_187.ui @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/tests/samples/issue_187_dec.blp b/tests/samples/issue_187_dec.blp new file mode 100644 index 0000000..30b997c --- /dev/null +++ b/tests/samples/issue_187_dec.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template ListItem { + child: Label { + label: bind template.item as <$RecentObject>.filename; + }; +} diff --git a/tests/test_samples.py b/tests/test_samples.py index 1f56eb6..f96d0eb 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -181,11 +181,7 @@ class TestSamples(unittest.TestCase): def test_samples(self): # list the samples directory - samples = [ - f.stem - for f in Path(__file__).parent.glob("samples/*.blp") - if not f.stem.endswith("_dec") - ] + samples = [f.stem for f in Path(__file__).parent.glob("samples/*.blp")] samples.sort() for sample in samples: REQUIRE_ADW_1_4 = ["adw_breakpoint"] @@ -215,7 +211,7 @@ class TestSamples(unittest.TestCase): ] # Decompiler-only tests - SKIP_COMPILE = ["issue_177", "translator_comments"] + SKIP_COMPILE = ["issue_177", "issue_187", "translator_comments"] SKIP_DECOMPILE = [ # Comments are not preserved in either direction @@ -228,7 +224,7 @@ class TestSamples(unittest.TestCase): continue with self.subTest(sample): - if sample not in SKIP_COMPILE: + if sample not in SKIP_COMPILE and not sample.endswith("_dec"): self.assert_sample(sample, skip_run=sample in SKIP_RUN) with self.subTest("decompile/" + sample): From 2e42dc68486c62eb250215a756958b2e0000a6ff Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 07:46:34 -0500 Subject: [PATCH 17/34] decompiler: Fix bug in signals with template object If a signal handler had the template as its object, the decompiler would output the class name instead of the 'template' keyword. --- blueprintcompiler/language/gobject_signal.py | 8 +++++++- tests/samples/signal_template_object.blp | 7 +++++++ tests/samples/signal_template_object.ui | 16 ++++++++++++++++ tests/test_samples.py | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/samples/signal_template_object.blp create mode 100644 tests/samples/signal_template_object.ui diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 9c27b97..3b4235f 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -225,8 +225,14 @@ class Signal(AstNode): @decompiler("signal") -def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None): +def decompile_signal( + ctx: DecompileCtx, gir, name, handler, swapped=None, after="false", object=None +): object_name = object or "" + + if object_name == ctx.template_class: + object_name = "template" + name = name.replace("_", "-") line = f"{name} => ${handler}({object_name})" diff --git a/tests/samples/signal_template_object.blp b/tests/samples/signal_template_object.blp new file mode 100644 index 0000000..16dd5a0 --- /dev/null +++ b/tests/samples/signal_template_object.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template $MyTemplate { + Button { + clicked => $my_signal_handler(template); + } +} diff --git a/tests/samples/signal_template_object.ui b/tests/samples/signal_template_object.ui new file mode 100644 index 0000000..c9a680a --- /dev/null +++ b/tests/samples/signal_template_object.ui @@ -0,0 +1,16 @@ + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index f96d0eb..0807d65 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -198,6 +198,7 @@ class TestSamples(unittest.TestCase): "parseable", "signal", "signal_not_swapped", + "signal_template_object", "template", "template_binding", "template_binding_extern", From bf4d8579b6e79a99deb97dbfdaab9b501cf8150b Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 13:45:47 -0600 Subject: [PATCH 18/34] lsp: Fix completions when editing existing item Many completion snippets insert more than just the name. For example, the object completer inserts the braces and places your cursor inside them automatically, to save some typing. However, if you're changing the class of an existing object, this isn't what you want. Changed so that if the next token is '{', only the name is inserted. Made similar changes to the property and signal completers. --- blueprintcompiler/completions.py | 158 +++++++++--------- blueprintcompiler/completions_utils.py | 23 ++- .../language/adw_response_dialog.py | 4 +- blueprintcompiler/language/gtk_a11y.py | 8 +- .../language/gtk_combo_box_text.py | 2 +- blueprintcompiler/language/gtk_file_filter.py | 2 +- blueprintcompiler/language/gtk_layout.py | 2 +- blueprintcompiler/language/gtk_menu.py | 4 +- blueprintcompiler/language/gtk_scale.py | 4 +- blueprintcompiler/language/gtk_size_group.py | 2 +- blueprintcompiler/language/gtk_string_list.py | 2 +- blueprintcompiler/language/gtk_styles.py | 2 +- tests/test_samples.py | 6 +- 13 files changed, 122 insertions(+), 97 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b10ec3e..f1c17b6 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -31,13 +31,18 @@ Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] def _complete( - lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int + lsp, + ast_node: AstNode, + tokens: T.List[Token], + idx: int, + token_idx: int, + next_token: Token, ) -> T.Iterator[Completion]: for child in ast_node.children: if child.group.start <= idx and ( idx < child.group.end or (idx == child.group.end and child.incomplete) ): - yield from _complete(lsp, child, tokens, idx, token_idx) + yield from _complete(lsp, child, tokens, idx, token_idx, next_token) return prev_tokens: T.List[Token] = [] @@ -50,7 +55,7 @@ def _complete( token_idx -= 1 for completer in ast_node.completers: - yield from completer(prev_tokens, ast_node, lsp) + yield from completer(prev_tokens, next_token, ast_node, lsp) def complete( @@ -62,16 +67,24 @@ def complete( if token.start < idx <= token.end: token_idx = i + if tokens[token_idx].type == TokenType.EOF: + next_token = tokens[token_idx] + else: + next_token_idx = token_idx + 1 + while tokens[next_token_idx].type == TokenType.WHITESPACE: + next_token_idx += 1 + next_token = tokens[next_token_idx] + # if the current token is an identifier or whitespace, move to the token before it while tokens[token_idx].type in [TokenType.IDENT, TokenType.WHITESPACE]: idx = tokens[token_idx].start token_idx -= 1 - yield from _complete(lsp, ast_node, tokens, idx, token_idx) + yield from _complete(lsp, ast_node, tokens, idx, token_idx, next_token) @completer([language.GtkDirective]) -def using_gtk(lsp, ast_node, match_variables): +def using_gtk(_ctx: CompletionContext): yield Completion( "using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n" ) @@ -81,9 +94,9 @@ def using_gtk(lsp, ast_node, match_variables): applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) -def namespace(lsp, ast_node, match_variables): +def namespace(ctx: CompletionContext): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") - for ns in ast_node.root.children[language.Import]: + for ns in ctx.ast_node.root.children[language.Import]: if ns.gir_namespace is not None: yield Completion( ns.gir_namespace.name, @@ -99,14 +112,18 @@ def namespace(lsp, ast_node, match_variables): [(TokenType.IDENT, None), (TokenType.OP, ".")], ], ) -def object_completer(lsp, ast_node, match_variables): - ns = ast_node.root.gir.namespaces.get(match_variables[0]) +def object_completer(ctx: CompletionContext): + ns = ctx.ast_node.root.gir.namespaces.get(ctx.match_variables[0]) if ns is not None: for c in ns.classes.values(): + snippet = c.name + if str(ctx.next_token) != "{": + snippet += " {\n $0\n}" + yield Completion( c.name, CompletionItemKind.Class, - snippet=f"{c.name} {{\n $0\n}}", + snippet=snippet, docs=c.doc, detail=c.detail, ) @@ -116,14 +133,18 @@ def object_completer(lsp, ast_node, match_variables): applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) -def gtk_object_completer(lsp, ast_node, match_variables): - ns = ast_node.root.gir.namespaces.get("Gtk") +def gtk_object_completer(ctx: CompletionContext): + ns = ctx.ast_node.root.gir.namespaces.get("Gtk") if ns is not None: for c in ns.classes.values(): + snippet = c.name + if str(ctx.next_token) != "{": + snippet += " {\n $0\n}" + yield Completion( c.name, CompletionItemKind.Class, - snippet=f"{c.name} {{\n $0\n}}", + snippet=snippet, docs=c.doc, detail=c.detail, ) @@ -133,76 +154,55 @@ def gtk_object_completer(lsp, ast_node, match_variables): applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def property_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"): - for prop_name, prop in ast_node.gir_class.properties.items(): - if ( +def property_completer(ctx: CompletionContext): + assert isinstance(ctx.ast_node, language.ObjectContent) + if ctx.ast_node.gir_class and hasattr(ctx.ast_node.gir_class, "properties"): + for prop_name, prop in ctx.ast_node.gir_class.properties.items(): + if str(ctx.next_token) == ":": + snippet = prop_name + elif ( isinstance(prop.type, gir.BoolType) - and lsp.client_supports_completion_choice + and ctx.client_supports_completion_choice ): - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: ${{1|true,false|}};", - docs=prop.doc, - detail=prop.detail, - ) + snippet = f"{prop_name}: ${{1|true,false|}};" elif isinstance(prop.type, gir.StringType): snippet = ( f'{prop_name}: _("$0");' if annotations.is_property_translated(prop) else f'{prop_name}: "$0";' ) - - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=snippet, - docs=prop.doc, - detail=prop.detail, - ) elif ( isinstance(prop.type, gir.Enumeration) and len(prop.type.members) <= 10 - and lsp.client_supports_completion_choice + and ctx.client_supports_completion_choice ): choices = ",".join(prop.type.members.keys()) - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: ${{1|{choices}|}};", - docs=prop.doc, - detail=prop.detail, - ) + snippet = f"{prop_name}: ${{1|{choices}|}};" elif prop.type.full_name == "Gtk.Expression": - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: expr $0;", - docs=prop.doc, - detail=prop.detail, - ) + snippet = f"{prop_name}: expr $0;" else: - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: $0;", - docs=prop.doc, - detail=prop.detail, - ) + snippet = f"{prop_name}: $0;" + + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=snippet, + docs=prop.doc, + detail=prop.detail, + ) @completer( applies_in=[language.Property, language.A11yProperty], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) -def prop_value_completer(lsp, ast_node, match_variables): - if (vt := ast_node.value_type) is not None: +def prop_value_completer(ctx: CompletionContext): + assert isinstance(ctx.ast_node, language.Property) or isinstance( + ctx.ast_node, language.A11yProperty + ) + + if (vt := ctx.ast_node.value_type) is not None: if isinstance(vt.value_type, gir.Enumeration): for name, member in vt.value_type.members.items(): yield Completion( @@ -221,30 +221,38 @@ def prop_value_completer(lsp, ast_node, match_variables): applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def signal_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"): - for signal_name, signal in ast_node.gir_class.signals.items(): - if not isinstance(ast_node.parent, language.Object): - name = "on" +def signal_completer(ctx: CompletionContext): + assert isinstance(ctx.ast_node, language.ObjectContent) + + if ctx.ast_node.gir_class and hasattr(ctx.ast_node.gir_class, "signals"): + for signal_name, signal in ctx.ast_node.gir_class.signals.items(): + if str(ctx.next_token) == "=>": + snippet = signal_name else: - name = "on_" + ( - ast_node.parent.children[ClassName][0].tokens["id"] - or ast_node.parent.children[ClassName][0] - .tokens["class_name"] - .lower() - ) + if not isinstance(ctx.ast_node.parent, language.Object): + name = "on" + else: + name = "on_" + ( + ctx.ast_node.parent.children[ClassName][0].tokens["id"] + or ctx.ast_node.parent.children[ClassName][0] + .tokens["class_name"] + .lower() + ) + + snippet = f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;" + yield Completion( signal_name, CompletionItemKind.Event, sort_text=f"1 {signal_name}", - snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;", + snippet=snippet, docs=signal.doc, detail=signal.detail, ) @completer(applies_in=[language.UI], matches=new_statement_patterns) -def template_completer(lsp, ast_node, match_variables): +def template_completer(_ctx: CompletionContext): yield Completion( "template", CompletionItemKind.Snippet, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index eccf125..effb152 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -19,10 +19,21 @@ import typing as T +from dataclasses import dataclass +from .ast_utils import AstNode from .lsp_utils import Completion from .tokenizer import Token, TokenType + +@dataclass +class CompletionContext: + client_supports_completion_choice: bool + ast_node: AstNode + match_variables: T.List[str] + next_token: Token + + new_statement_patterns = [ [(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "}")], @@ -32,8 +43,8 @@ new_statement_patterns = [ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): - def decorator(func): - def inner(prev_tokens: T.List[Token], ast_node, lsp): + def decorator(func: T.Callable[[CompletionContext], T.Generator[Completion]]): + def inner(prev_tokens: T.List[Token], next_token: Token, ast_node, lsp): # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: @@ -66,7 +77,13 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None if not any_match: return - yield from func(lsp, ast_node, match_variables) + context = CompletionContext( + client_supports_completion_choice=lsp.client_supports_completion_choice, + ast_node=ast_node, + match_variables=match_variables, + next_token=next_token, + ) + yield from func(context) for c in applies_in: c.completers.append(inner) diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index d2680fd..516c69a 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -143,7 +143,7 @@ class ExtAdwResponseDialog(AstNode): applies_in_subclass=("Adw", "MessageDialog"), matches=new_statement_patterns, ) -def complete_adw_message_dialog(lsp, ast_node, match_variables): +def complete_adw_message_dialog(_ctx: CompletionContext): yield Completion( "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" ) @@ -154,7 +154,7 @@ def complete_adw_message_dialog(lsp, ast_node, match_variables): applies_in_subclass=("Adw", "AlertDialog"), matches=new_statement_patterns, ) -def complete_adw_alert_dialog(lsp, ast_node, match_variables): +def complete_adw_alert_dialog(_ctx: CompletionContext): yield Completion( "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" ) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 0cc3cb3..417eaec 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -232,7 +232,7 @@ class ExtAccessibility(AstNode): applies_in=[ObjectContent], matches=new_statement_patterns, ) -def a11y_completer(lsp, ast_node, match_variables): +def a11y_completer(_ctx: CompletionContext): yield Completion( "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" ) @@ -242,12 +242,12 @@ def a11y_completer(lsp, ast_node, match_variables): applies_in=[ExtAccessibility], matches=new_statement_patterns, ) -def a11y_name_completer(lsp, ast_node, match_variables): - for name, type in get_types(ast_node.root.gir).items(): +def a11y_name_completer(ctx: CompletionContext): + for name, type in get_types(ctx.ast_node.root.gir).items(): yield Completion( name, CompletionItemKind.Property, - docs=_get_docs(ast_node.root.gir, type.name), + docs=_get_docs(ctx.ast_node.root.gir, type.name), ) diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 32b3486..e162cca 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -94,7 +94,7 @@ class ExtComboBoxItems(AstNode): applies_in_subclass=("Gtk", "ComboBoxText"), matches=new_statement_patterns, ) -def items_completer(lsp, ast_node, match_variables): +def items_completer(_ctx: CompletionContext): yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index e84afc7..754130a 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -101,7 +101,7 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") applies_in_subclass=("Gtk", "FileFilter"), matches=new_statement_patterns, ) -def file_filter_completer(lsp, ast_node, match_variables): +def file_filter_completer(_ctx: CompletionContext): yield Completion( "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' ) diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8d3e37a..901c40d 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -93,7 +93,7 @@ class ExtLayout(AstNode): applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) -def layout_completer(lsp, ast_node, match_variables): +def layout_completer(_ctx: CompletionContext): yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index c7ef5f2..d5bf4fb 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -243,7 +243,7 @@ from .ui import UI applies_in=[UI], matches=new_statement_patterns, ) -def menu_completer(lsp, ast_node, match_variables): +def menu_completer(_ctx: CompletionContext): yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") @@ -251,7 +251,7 @@ def menu_completer(lsp, ast_node, match_variables): applies_in=[Menu], matches=new_statement_patterns, ) -def menu_content_completer(lsp, ast_node, match_variables): +def menu_content_completer(_ctx: CompletionContext): yield Completion( "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" ) diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 1fd5ac3..2615a67 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -137,14 +137,14 @@ class ExtScaleMarks(AstNode): applies_in_subclass=("Gtk", "Scale"), matches=new_statement_patterns, ) -def complete_marks(lsp, ast_node, match_variables): +def complete_marks(_ctx: CompletionContext): yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") @completer( applies_in=[ExtScaleMarks], ) -def complete_mark(lsp, ast_node, match_variables): +def complete_mark(_ctx: CompletionContext): yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 54d85e5..3505a06 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -104,7 +104,7 @@ class ExtSizeGroupWidgets(AstNode): applies_in_subclass=("Gtk", "SizeGroup"), matches=new_statement_patterns, ) -def size_group_completer(lsp, ast_node, match_variables): +def size_group_completer(_ctx: CompletionContext): yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index a146f35..0e6c00a 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -75,7 +75,7 @@ class ExtStringListStrings(AstNode): applies_in_subclass=("Gtk", "StringList"), matches=new_statement_patterns, ) -def strings_completer(lsp, ast_node, match_variables): +def strings_completer(_ctx: CompletionContext): yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 8617522..0cdea0b 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -80,7 +80,7 @@ class ExtStyles(AstNode): applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) -def style_completer(lsp, ast_node, match_variables): +def style_completer(_ctx: CompletionContext): yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') diff --git a/tests/test_samples.py b/tests/test_samples.py index 0807d65..9cd5baf 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -64,11 +64,11 @@ class TestSamples(unittest.TestCase): def assert_ast_doesnt_crash(self, text, tokens, ast: AstNode): lsp = LanguageServer() - for i in range(len(text)): + for i in range(len(text) + 1): ast.get_docs(i) - for i in range(len(text)): + for i in range(len(text) + 1): list(complete(lsp, ast, tokens, i)) - for i in range(len(text)): + for i in range(len(text) + 1): ast.get_reference(i) ast.get_document_symbols() From 3d0593bc2b2fed94d0320a2b373fb3062fd35be4 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 16:05:27 -0600 Subject: [PATCH 19/34] completions: Complete available namespaces Add completions for namespaces in the typelib path that can be imported. Accepting the completion automatically adds an import statement. --- blueprintcompiler/completions.py | 20 +++++++++++++++++++- blueprintcompiler/language/ui.py | 18 ++++++++++++------ blueprintcompiler/lsp_utils.py | 6 ++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index f1c17b6..60cadad 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -23,7 +23,7 @@ from . import annotations, gir, language from .ast_utils import AstNode from .completions_utils import * from .language.types import ClassName -from .lsp_utils import Completion, CompletionItemKind +from .lsp_utils import Completion, CompletionItemKind, TextEdit from .parser import SKIP_TOKENS from .tokenizer import Token, TokenType @@ -96,14 +96,32 @@ def using_gtk(_ctx: CompletionContext): ) def namespace(ctx: CompletionContext): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") + + imported_namespaces = set(["Gtk"]) + for ns in ctx.ast_node.root.children[language.Import]: if ns.gir_namespace is not None: + imported_namespaces.add(ns.gir_namespace.name) yield Completion( ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".", ) + for ns, version in gir.get_available_namespaces(): + if ns not in imported_namespaces: + yield Completion( + ns, + CompletionItemKind.Module, + text=ns + ".", + signature=f" using {ns} {version}", + additional_text_edits=[ + TextEdit( + ctx.ast_node.root.import_range(ns), f"\nusing {ns} {version};" + ) + ], + ) + @completer( applies_in=[language.UI, language.ObjectContent, language.Template], diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index d55a22a..896c0f7 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -110,16 +110,22 @@ class UI(AstNode): and self.template.class_name.glib_type_name == id ) - def import_code_action(self, ns: str, version: str) -> CodeAction: - if len(self.children[Import]): - pos = self.children[Import][-1].range.end - else: - pos = self.children[GtkDirective][0].range.end + def import_range(self, ns: str): + """Returns a range to insert a new import statement""" + pos = self.children[GtkDirective][0].range.end + # try to insert alphabetically + for import_ in self.children[Import]: + if ns.lower() > import_.namespace.lower(): + pos = import_.range.end + + return Range(pos, pos, self.group.text) + + def import_code_action(self, ns: str, version: str) -> CodeAction: return CodeAction( f"Import {ns} {version}", f"\nusing {ns} {version};", - Range(pos, pos, self.group.text), + self.import_range(ns), ) @cached_property diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index b938181..2a4380a 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -87,6 +87,7 @@ class Completion: text: T.Optional[str] = None snippet: T.Optional[str] = None detail: T.Optional[str] = None + additional_text_edits: T.Optional[T.List["TextEdit"]] = None def to_json(self, snippets: bool): insert_text = self.text or self.label @@ -114,6 +115,11 @@ class Completion: "insertText": insert_text, "insertTextFormat": insert_text_format, "detail": self.detail if self.detail else None, + "additionalTextEdits": ( + [edit.to_json() for edit in self.additional_text_edits] + if self.additional_text_edits + else None + ), } return {k: v for k, v in result.items() if v is not None} From f5cef37db85391490b52870628dfc1a6dfbe216f Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 16:23:42 -0600 Subject: [PATCH 20/34] completions: Add translation-domain completer --- blueprintcompiler/completions.py | 26 ++++++++++++++++++++++++-- blueprintcompiler/completions_utils.py | 6 +++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 60cadad..cdd8d7d 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -23,7 +23,7 @@ from . import annotations, gir, language from .ast_utils import AstNode from .completions_utils import * from .language.types import ClassName -from .lsp_utils import Completion, CompletionItemKind, TextEdit +from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS from .tokenizer import Token, TokenType @@ -55,7 +55,7 @@ def _complete( token_idx -= 1 for completer in ast_node.completers: - yield from completer(prev_tokens, next_token, ast_node, lsp) + yield from completer(prev_tokens, next_token, ast_node, lsp, idx) def complete( @@ -90,6 +90,28 @@ def using_gtk(_ctx: CompletionContext): ) +@completer([language.UI]) +def translation_domain(ctx: CompletionContext): + if ctx.ast_node.root.translation_domain is not None: + return + + # Translation domain must be after the import statements but before any content + for i in ctx.ast_node.root.children: + if isinstance(i, language.Import): + if ctx.index <= i.range.start: + return + elif not isinstance(i, language.GtkDirective): + if ctx.index >= i.range.end: + return + + yield Completion( + "translation-domain", + CompletionItemKind.Keyword, + snippet='translation-domain "$0";', + docs=get_docs_section("Syntax TranslationDomain"), + ) + + @completer( applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index effb152..eb0bb79 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -32,6 +32,7 @@ class CompletionContext: ast_node: AstNode match_variables: T.List[str] next_token: Token + index: int new_statement_patterns = [ @@ -44,7 +45,9 @@ new_statement_patterns = [ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def decorator(func: T.Callable[[CompletionContext], T.Generator[Completion]]): - def inner(prev_tokens: T.List[Token], next_token: Token, ast_node, lsp): + def inner( + prev_tokens: T.List[Token], next_token: Token, ast_node, lsp, idx: int + ): # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: @@ -82,6 +85,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None ast_node=ast_node, match_variables=match_variables, next_token=next_token, + index=idx, ) yield from func(context) From 64b96137f54fc31cdbfd5dff4575c7b63e39b3aa Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 16:29:10 -0600 Subject: [PATCH 21/34] completions: Add completer for import statements --- blueprintcompiler/completions.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index cdd8d7d..c2186dd 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -90,6 +90,29 @@ def using_gtk(_ctx: CompletionContext): ) +@completer([language.UI]) +def using(ctx: CompletionContext): + imported_namespaces = set( + [import_.namespace for import_ in ctx.ast_node.root.using] + ) + + # Import statements must be before any content + for i in ctx.ast_node.root.children: + if not isinstance(i, language.GtkDirective) and not isinstance( + i, language.Import + ): + if ctx.index >= i.range.end: + return + + for ns, version in gir.get_available_namespaces(): + if ns not in imported_namespaces and ns != "Gtk": + yield Completion( + f"using {ns} {version}", + CompletionItemKind.Module, + text=f"using {ns} {version};", + ) + + @completer([language.UI]) def translation_domain(ctx: CompletionContext): if ctx.ast_node.root.translation_domain is not None: From 860580e560fd9309737473651def0bf1c7282760 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 20:22:02 -0600 Subject: [PATCH 22/34] completions: Add object value completions For object properties, add completions for named objects in the blueprint and for matching classes in imported namespaces. Also add null and bind. --- blueprintcompiler/completions.py | 84 +++++++++++++++---- .../language/gobject_property.py | 6 +- blueprintcompiler/language/gtk_a11y.py | 4 +- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index c2186dd..be881fa 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -135,26 +135,13 @@ def translation_domain(ctx: CompletionContext): ) -@completer( - applies_in=[language.UI, language.ObjectContent, language.Template], - matches=new_statement_patterns, -) -def namespace(ctx: CompletionContext): - yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") - - imported_namespaces = set(["Gtk"]) - - for ns in ctx.ast_node.root.children[language.Import]: - if ns.gir_namespace is not None: - imported_namespaces.add(ns.gir_namespace.name) - yield Completion( - ns.gir_namespace.name, - CompletionItemKind.Module, - text=ns.gir_namespace.name + ".", - ) +def _ns_prefix_completions(ctx: CompletionContext): + imported_namespaces = set( + [import_.namespace for import_ in ctx.ast_node.root.using] + ) for ns, version in gir.get_available_namespaces(): - if ns not in imported_namespaces: + if ns not in imported_namespaces and ns != "Gtk": yield Completion( ns, CompletionItemKind.Module, @@ -168,6 +155,24 @@ def namespace(ctx: CompletionContext): ) +@completer( + applies_in=[language.UI, language.ObjectContent, language.Template], + matches=new_statement_patterns, +) +def namespace(ctx: CompletionContext): + yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") + + for ns in ctx.ast_node.root.children[language.Import]: + if ns.gir_namespace is not None: + yield Completion( + ns.gir_namespace.name, + CompletionItemKind.Module, + text=ns.gir_namespace.name + ".", + ) + + yield from _ns_prefix_completions(ctx) + + @completer( applies_in=[language.UI, language.ObjectContent, language.Template], matches=[ @@ -261,6 +266,14 @@ def property_completer(ctx: CompletionContext): matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) def prop_value_completer(ctx: CompletionContext): + if isinstance(ctx.ast_node, language.Property): + yield Completion( + "bind", + CompletionItemKind.Keyword, + snippet="bind $0", + docs=get_docs_section("Syntax Binding"), + ) + assert isinstance(ctx.ast_node, language.Property) or isinstance( ctx.ast_node, language.A11yProperty ) @@ -279,6 +292,41 @@ def prop_value_completer(ctx: CompletionContext): yield Completion("true", CompletionItemKind.Constant) yield Completion("false", CompletionItemKind.Constant) + elif isinstance(vt.value_type, gir.Class) or isinstance( + vt.value_type, gir.Interface + ): + yield Completion( + "null", + CompletionItemKind.Constant, + ) + + yield from _ns_prefix_completions(ctx) + + for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items(): + if obj.gir_class is not None and obj.gir_class.assignable_to( + vt.value_type + ): + yield Completion( + id, + CompletionItemKind.Variable, + detail=obj.signature, + ) + + for ns in ctx.ast_node.root.gir.namespaces.values(): + for c in ns.classes.values(): + if not c.abstract and c.assignable_to(vt.value_type): + name = c.name if ns.name == "Gtk" else ns.name + "." + c.name + snippet = name + if str(ctx.next_token) != "{": + snippet += " {\n $0\n}" + yield Completion( + name, + CompletionItemKind.Class, + snippet=snippet, + detail=c.detail, + docs=c.doc, + ) + @completer( applies_in=[language.ObjectContent], diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 50a7512..67f2555 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -26,7 +26,11 @@ from .values import ArrayValue, ExprValue, ObjectValue, Value class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) + UseIdent("name"), + ":", + AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue).expected( + "property value" + ), ) @property diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 417eaec..a8bf300 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -121,7 +121,9 @@ class A11yProperty(AstNode): grammar = Statement( UseIdent("name"), ":", - AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]), + AnyOf( + Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"] + ).expected("value"), ) @property From d0394136cfd153004c1e889becb6021a00a59b93 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 14:16:29 -0600 Subject: [PATCH 23/34] completions: Sort completion items --- blueprintcompiler/completions.py | 31 ++++++++++++--- blueprintcompiler/completions_utils.py | 16 ++++++++ .../language/adw_response_dialog.py | 5 ++- blueprintcompiler/language/gtk_a11y.py | 5 ++- .../language/gtk_combo_box_text.py | 7 +++- blueprintcompiler/language/gtk_file_filter.py | 19 +++++++-- blueprintcompiler/language/gtk_layout.py | 7 +++- blueprintcompiler/language/gtk_menu.py | 39 ++++++++++++++++--- blueprintcompiler/language/gtk_scale.py | 7 +++- blueprintcompiler/language/gtk_size_group.py | 7 +++- blueprintcompiler/language/gtk_string_list.py | 7 +++- blueprintcompiler/language/gtk_styles.py | 7 +++- 12 files changed, 135 insertions(+), 22 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index be881fa..b5a0721 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -110,6 +110,7 @@ def using(ctx: CompletionContext): f"using {ns} {version}", CompletionItemKind.Module, text=f"using {ns} {version};", + sort_text=get_sort_key(CompletionPriority.NAMESPACE, ns), ) @@ -130,6 +131,7 @@ def translation_domain(ctx: CompletionContext): yield Completion( "translation-domain", CompletionItemKind.Keyword, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "translation-domain"), snippet='translation-domain "$0";', docs=get_docs_section("Syntax TranslationDomain"), ) @@ -146,6 +148,7 @@ def _ns_prefix_completions(ctx: CompletionContext): ns, CompletionItemKind.Module, text=ns + ".", + sort_text=get_sort_key(CompletionPriority.IMPORT_NAMESPACE, ns), signature=f" using {ns} {version}", additional_text_edits=[ TextEdit( @@ -168,6 +171,9 @@ def namespace(ctx: CompletionContext): ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".", + sort_text=get_sort_key( + CompletionPriority.NAMESPACE, ns.gir_namespace.name + ), ) yield from _ns_prefix_completions(ctx) @@ -191,6 +197,7 @@ def object_completer(ctx: CompletionContext): yield Completion( c.name, CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, c.name), snippet=snippet, docs=c.doc, detail=c.detail, @@ -212,6 +219,7 @@ def gtk_object_completer(ctx: CompletionContext): yield Completion( c.name, CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, c.name), snippet=snippet, docs=c.doc, detail=c.detail, @@ -254,7 +262,7 @@ def property_completer(ctx: CompletionContext): yield Completion( prop_name, CompletionItemKind.Property, - sort_text=f"0 {prop_name}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, prop_name), snippet=snippet, docs=prop.doc, detail=prop.detail, @@ -272,6 +280,7 @@ def prop_value_completer(ctx: CompletionContext): CompletionItemKind.Keyword, snippet="bind $0", docs=get_docs_section("Syntax Binding"), + sort_text=get_sort_key(CompletionPriority.KEYWORD, "bind"), ) assert isinstance(ctx.ast_node, language.Property) or isinstance( @@ -286,11 +295,20 @@ def prop_value_completer(ctx: CompletionContext): CompletionItemKind.EnumMember, docs=member.doc, detail=member.detail, + sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, name), ) elif isinstance(vt.value_type, gir.BoolType): - yield Completion("true", CompletionItemKind.Constant) - yield Completion("false", CompletionItemKind.Constant) + yield Completion( + "true", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, "true"), + ) + yield Completion( + "false", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, "false"), + ) elif isinstance(vt.value_type, gir.Class) or isinstance( vt.value_type, gir.Interface @@ -298,6 +316,7 @@ def prop_value_completer(ctx: CompletionContext): yield Completion( "null", CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), ) yield from _ns_prefix_completions(ctx) @@ -309,7 +328,8 @@ def prop_value_completer(ctx: CompletionContext): yield Completion( id, CompletionItemKind.Variable, - detail=obj.signature, + signature=" " + obj.signature, + sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), ) for ns in ctx.ast_node.root.gir.namespaces.values(): @@ -322,6 +342,7 @@ def prop_value_completer(ctx: CompletionContext): yield Completion( name, CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, name), snippet=snippet, detail=c.detail, docs=c.doc, @@ -355,7 +376,7 @@ def signal_completer(ctx: CompletionContext): yield Completion( signal_name, CompletionItemKind.Event, - sort_text=f"1 {signal_name}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, signal_name), snippet=snippet, docs=signal.doc, detail=signal.detail, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index eb0bb79..4d9ceb2 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -20,12 +20,28 @@ import typing as T from dataclasses import dataclass +from enum import Enum from .ast_utils import AstNode from .lsp_utils import Completion from .tokenizer import Token, TokenType +class CompletionPriority(Enum): + ENUM_MEMBER = "00" + NAMED_OBJECT = "01" + OBJECT_MEMBER = "02" + CLASS = "03" + NAMESPACE = "04" + KEYWORD = "05" + # An available namespace that hasn't been imported yet + IMPORT_NAMESPACE = "99" + + +def get_sort_key(priority: CompletionPriority, name: str): + return f"{priority.value} {name}" + + @dataclass class CompletionContext: client_supports_completion_choice: bool diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index 516c69a..c621df0 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -156,7 +156,10 @@ def complete_adw_message_dialog(_ctx: CompletionContext): ) def complete_adw_alert_dialog(_ctx: CompletionContext): yield Completion( - "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" + "responses", + CompletionItemKind.Keyword, + snippet="responses [\n\t$0\n]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "responses"), ) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index a8bf300..cd52746 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -236,7 +236,10 @@ class ExtAccessibility(AstNode): ) def a11y_completer(_ctx: CompletionContext): yield Completion( - "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" + "accessibility", + CompletionItemKind.Snippet, + snippet="accessibility {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "accessibility"), ) diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index e162cca..5a7a892 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -95,7 +95,12 @@ class ExtComboBoxItems(AstNode): matches=new_statement_patterns, ) def items_completer(_ctx: CompletionContext): - yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") + yield Completion( + "items", + CompletionItemKind.Snippet, + snippet="items [$0]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "items"), + ) @decompiler("items", parent_type="Gtk.ComboBoxText") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 754130a..d0e53d2 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -103,10 +103,23 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") ) def file_filter_completer(_ctx: CompletionContext): yield Completion( - "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' + "mime-types", + CompletionItemKind.Snippet, + snippet='mime-types ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "mime-types"), + ) + yield Completion( + "patterns", + CompletionItemKind.Snippet, + snippet='patterns ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "patterns"), + ) + yield Completion( + "suffixes", + CompletionItemKind.Snippet, + snippet='suffixes ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "suffixes"), ) - yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]') - yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]') @decompiler("mime-types") diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 901c40d..63bc0f6 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -94,7 +94,12 @@ class ExtLayout(AstNode): matches=new_statement_patterns, ) def layout_completer(_ctx: CompletionContext): - yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") + yield Completion( + "layout", + CompletionItemKind.Snippet, + snippet="layout {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "layout"), + ) @decompiler("layout") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index d5bf4fb..ed7ede8 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -253,21 +253,48 @@ def menu_completer(_ctx: CompletionContext): ) def menu_content_completer(_ctx: CompletionContext): yield Completion( - "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" + "submenu", + CompletionItemKind.Snippet, + snippet="submenu {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.CLASS, "1 submenu"), ) yield Completion( - "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" + "section", + CompletionItemKind.Snippet, + snippet="section {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.CLASS, "1 section"), + ) + yield Completion( + "item", + CompletionItemKind.Snippet, + snippet="item {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.CLASS, "1 item"), ) - yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}") yield Completion( "item (shorthand)", CompletionItemKind.Snippet, snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', + sort_text=get_sort_key(CompletionPriority.CLASS, "0 item (shorthand)"), ) - yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;") - yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') - yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') + yield Completion( + "label", + CompletionItemKind.Snippet, + snippet="label: $0;", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "label"), + ) + yield Completion( + "action", + CompletionItemKind.Snippet, + snippet='action: "$0";', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "action"), + ) + yield Completion( + "icon", + CompletionItemKind.Snippet, + snippet='icon: "$0";', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "icon"), + ) @decompiler("menu") diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 2615a67..21089a4 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -138,7 +138,12 @@ class ExtScaleMarks(AstNode): matches=new_statement_patterns, ) def complete_marks(_ctx: CompletionContext): - yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") + yield Completion( + "marks", + CompletionItemKind.Keyword, + snippet="marks [\n\t$0\n]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "marks"), + ) @completer( diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 3505a06..d30eef9 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -105,7 +105,12 @@ class ExtSizeGroupWidgets(AstNode): matches=new_statement_patterns, ) def size_group_completer(_ctx: CompletionContext): - yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") + yield Completion( + "widgets", + CompletionItemKind.Snippet, + snippet="widgets [$0]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "widgets"), + ) @decompiler("widgets") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 0e6c00a..a4fa3b5 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -76,7 +76,12 @@ class ExtStringListStrings(AstNode): matches=new_statement_patterns, ) def strings_completer(_ctx: CompletionContext): - yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") + yield Completion( + "strings", + CompletionItemKind.Snippet, + snippet="strings [$0]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "strings"), + ) @decompiler("items", parent_type="Gtk.StringList") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 0cdea0b..7c9252c 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -81,7 +81,12 @@ class ExtStyles(AstNode): matches=new_statement_patterns, ) def style_completer(_ctx: CompletionContext): - yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') + yield Completion( + "styles", + CompletionItemKind.Keyword, + snippet='styles ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "styles"), + ) @decompiler("style") From b9910db8497af83449ab761fe4b805493219f831 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 15:21:26 -0600 Subject: [PATCH 24/34] completions: Improve accessibility properties --- blueprintcompiler/completions.py | 77 +++++++++----------------- blueprintcompiler/completions_utils.py | 37 ++++++++++++- blueprintcompiler/language/gtk_a11y.py | 30 +++++++--- 3 files changed, 86 insertions(+), 58 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b5a0721..a4e86b9 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -137,7 +137,7 @@ def translation_domain(ctx: CompletionContext): ) -def _ns_prefix_completions(ctx: CompletionContext): +def _available_namespace_completions(ctx: CompletionContext): imported_namespaces = set( [import_.namespace for import_ in ctx.ast_node.root.using] ) @@ -176,7 +176,7 @@ def namespace(ctx: CompletionContext): ), ) - yield from _ns_prefix_completions(ctx) + yield from _available_namespace_completions(ctx) @completer( @@ -234,38 +234,12 @@ def property_completer(ctx: CompletionContext): assert isinstance(ctx.ast_node, language.ObjectContent) if ctx.ast_node.gir_class and hasattr(ctx.ast_node.gir_class, "properties"): for prop_name, prop in ctx.ast_node.gir_class.properties.items(): - if str(ctx.next_token) == ":": - snippet = prop_name - elif ( - isinstance(prop.type, gir.BoolType) - and ctx.client_supports_completion_choice - ): - snippet = f"{prop_name}: ${{1|true,false|}};" - elif isinstance(prop.type, gir.StringType): - snippet = ( - f'{prop_name}: _("$0");' - if annotations.is_property_translated(prop) - else f'{prop_name}: "$0";' - ) - elif ( - isinstance(prop.type, gir.Enumeration) - and len(prop.type.members) <= 10 - and ctx.client_supports_completion_choice - ): - choices = ",".join(prop.type.members.keys()) - snippet = f"{prop_name}: ${{1|{choices}|}};" - elif prop.type.full_name == "Gtk.Expression": - snippet = f"{prop_name}: expr $0;" - else: - snippet = f"{prop_name}: $0;" - - yield Completion( + yield get_property_completion( prop_name, - CompletionItemKind.Property, - sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, prop_name), - snippet=snippet, - docs=prop.doc, - detail=prop.detail, + prop, + ctx, + annotations.is_property_translated(prop), + prop.doc, ) @@ -319,8 +293,6 @@ def prop_value_completer(ctx: CompletionContext): sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), ) - yield from _ns_prefix_completions(ctx) - for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items(): if obj.gir_class is not None and obj.gir_class.assignable_to( vt.value_type @@ -332,21 +304,26 @@ def prop_value_completer(ctx: CompletionContext): sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), ) - for ns in ctx.ast_node.root.gir.namespaces.values(): - for c in ns.classes.values(): - if not c.abstract and c.assignable_to(vt.value_type): - name = c.name if ns.name == "Gtk" else ns.name + "." + c.name - snippet = name - if str(ctx.next_token) != "{": - snippet += " {\n $0\n}" - yield Completion( - name, - CompletionItemKind.Class, - sort_text=get_sort_key(CompletionPriority.CLASS, name), - snippet=snippet, - detail=c.detail, - docs=c.doc, - ) + if isinstance(ctx.ast_node, language.Property): + yield from _available_namespace_completions(ctx) + + for ns in ctx.ast_node.root.gir.namespaces.values(): + for c in ns.classes.values(): + if not c.abstract and c.assignable_to(vt.value_type): + name = ( + c.name if ns.name == "Gtk" else ns.name + "." + c.name + ) + snippet = name + if str(ctx.next_token) != "{": + snippet += " {\n $0\n}" + yield Completion( + name, + CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, name), + snippet=snippet, + detail=c.detail, + docs=c.doc, + ) @completer( diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 4d9ceb2..bfca55a 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -22,8 +22,9 @@ import typing as T from dataclasses import dataclass from enum import Enum +from . import gir from .ast_utils import AstNode -from .lsp_utils import Completion +from .lsp_utils import Completion, CompletionItemKind from .tokenizer import Token, TokenType @@ -110,3 +111,37 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None return inner return decorator + + +def get_property_completion( + name: str, + type: gir.GirType, + ctx: CompletionContext, + translated: bool, + doc: str, +) -> Completion: + if str(ctx.next_token) == ":": + snippet = name + elif isinstance(type, gir.BoolType) and ctx.client_supports_completion_choice: + snippet = f"{name}: ${{1|true,false|}};" + elif isinstance(type, gir.StringType): + snippet = f'{name}: _("$0");' if translated else f'{name}: "$0";' + elif ( + isinstance(type, gir.Enumeration) + and len(type.members) <= 10 + and ctx.client_supports_completion_choice + ): + choices = ",".join(type.members.keys()) + snippet = f"{name}: ${{1|{choices}|}};" + elif type.full_name == "Gtk.Expression": + snippet = f"{name}: expr $0;" + else: + snippet = f"{name}: $0;" + + return Completion( + name, + CompletionItemKind.Property, + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, name), + snippet=snippet, + docs=doc, + ) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index cd52746..7f90b6e 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -25,7 +25,7 @@ from .gobject_object import ObjectContent, validate_parent_type from .values import Value -def get_property_types(gir): +def get_property_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: # from return { "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), @@ -50,7 +50,7 @@ def get_property_types(gir): } -def get_relation_types(gir): +def get_relation_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: # from widget = gir.get_type("Widget", "Gtk") return { @@ -75,7 +75,7 @@ def get_relation_types(gir): } -def get_state_types(gir): +def get_state_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: # from return { "busy": BoolType(), @@ -89,6 +89,20 @@ def get_state_types(gir): } +TRANSLATED = set( + [ + "description", + "help-text", + "label", + "placeholder", + "role-description", + "value-text", + "col-index-text", + "row-index-text", + ] +) + + def get_types(gir): return { **get_property_types(gir), @@ -247,12 +261,14 @@ def a11y_completer(_ctx: CompletionContext): applies_in=[ExtAccessibility], matches=new_statement_patterns, ) -def a11y_name_completer(ctx: CompletionContext): +def a11y_property_completer(ctx: CompletionContext): for name, type in get_types(ctx.ast_node.root.gir).items(): - yield Completion( + yield get_property_completion( name, - CompletionItemKind.Property, - docs=_get_docs(ctx.ast_node.root.gir, type.name), + type, + ctx, + name in TRANSLATED, + _get_docs(ctx.ast_node.root.gir, name), ) From 1e9b01bab9822076363efd60727a9b28531246a6 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 15:26:25 -0600 Subject: [PATCH 25/34] gtk_a11y: Add new state and property --- blueprintcompiler/language/gtk_a11y.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 7f90b6e..2a895ef 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -31,6 +31,7 @@ def get_property_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), "description": StringType(), "has-popup": BoolType(), + "help-text": StringType(), "key-shortcuts": StringType(), "label": StringType(), "level": IntType(), @@ -86,6 +87,7 @@ def get_state_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: "invalid": gir.get_type("AccessibleInvalidState", "Gtk"), "pressed": gir.get_type("AccessibleTristate", "Gtk"), "selected": BoolType(), + "visited": BoolType(), } From 866092ccf7ae495062f36043703fbbce7c6c2c90 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 15:34:16 -0600 Subject: [PATCH 26/34] values: Don't allow assigning true/false to object Fix a bug in the type checking code where it would not produce an error if you assigned "true" or "false" to an object property. --- blueprintcompiler/language/values.py | 9 ++++++++- tests/sample_errors/convert_bool_to_obj.blp | 5 +++++ tests/sample_errors/convert_bool_to_obj.err | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/sample_errors/convert_bool_to_obj.blp create mode 100644 tests/sample_errors/convert_bool_to_obj.err diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 833a4a3..7fa6bfa 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -338,7 +338,14 @@ class IdentLiteral(AstNode): raise CompileError( '"item" can only be used in an expression literal' ) - elif self.ident not in ["true", "false"]: + elif self.ident in ["true", "false"]: + if expected_type is not None and not isinstance( + expected_type, gir.BoolType + ): + raise CompileError( + f"Cannot assign boolean to {expected_type.full_name}" + ) + else: raise CompileError( f"Could not find object with ID {self.ident}", did_you_mean=( diff --git a/tests/sample_errors/convert_bool_to_obj.blp b/tests/sample_errors/convert_bool_to_obj.blp new file mode 100644 index 0000000..5f856c0 --- /dev/null +++ b/tests/sample_errors/convert_bool_to_obj.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Button { + child: false; +} diff --git a/tests/sample_errors/convert_bool_to_obj.err b/tests/sample_errors/convert_bool_to_obj.err new file mode 100644 index 0000000..389f69c --- /dev/null +++ b/tests/sample_errors/convert_bool_to_obj.err @@ -0,0 +1 @@ +4,10,5,Cannot assign boolean to Gtk.Widget \ No newline at end of file From b26433d865e5e136ca208f66ac3049ff3255f8aa Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 17 Jan 2025 16:44:21 -0600 Subject: [PATCH 27/34] completions: Add completions for response IDs --- blueprintcompiler/completions.py | 69 ++++++++++++++++++- blueprintcompiler/completions_utils.py | 20 ++++-- blueprintcompiler/language/__init__.py | 9 ++- .../language/adw_response_dialog.py | 16 +---- .../language/gtk_combo_box_text.py | 2 +- blueprintcompiler/language/gtk_file_filter.py | 2 +- blueprintcompiler/language/gtk_layout.py | 2 +- blueprintcompiler/language/gtk_scale.py | 2 +- blueprintcompiler/language/gtk_size_group.py | 2 +- blueprintcompiler/language/gtk_string_list.py | 2 +- blueprintcompiler/language/gtk_styles.py | 2 +- .../language/gtkbuilder_child.py | 11 ++- blueprintcompiler/language/response_id.py | 10 +-- blueprintcompiler/parse_tree.py | 12 +++- .../action_widget_have_no_id.err | 2 +- .../action_widget_in_invalid_container.err | 2 +- 16 files changed, 123 insertions(+), 42 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index a4e86b9..6a2535e 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -76,9 +76,13 @@ def complete( next_token = tokens[next_token_idx] # if the current token is an identifier or whitespace, move to the token before it - while tokens[token_idx].type in [TokenType.IDENT, TokenType.WHITESPACE]: + if tokens[token_idx].type == TokenType.IDENT: idx = tokens[token_idx].start token_idx -= 1 + else: + while tokens[token_idx].type == TokenType.WHITESPACE: + idx = tokens[token_idx].start + token_idx -= 1 yield from _complete(lsp, ast_node, tokens, idx, token_idx, next_token) @@ -236,7 +240,7 @@ def property_completer(ctx: CompletionContext): for prop_name, prop in ctx.ast_node.gir_class.properties.items(): yield get_property_completion( prop_name, - prop, + prop.type, ctx, annotations.is_property_translated(prop), prop.doc, @@ -245,7 +249,11 @@ def property_completer(ctx: CompletionContext): @completer( applies_in=[language.Property, language.A11yProperty], - matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], + matches=[ + [(TokenType.IDENT, None), (TokenType.OP, ":")], + [(TokenType.PUNCTUATION, ",")], + [(TokenType.PUNCTUATION, "[")], + ], ) def prop_value_completer(ctx: CompletionContext): if isinstance(ctx.ast_node, language.Property): @@ -367,3 +375,58 @@ def template_completer(_ctx: CompletionContext): CompletionItemKind.Snippet, snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}", ) + + +@completer( + applies_in=[language.ObjectContent, language.ChildType], + matches=[[(TokenType.PUNCTUATION, "[")]], + applies_in_subclass=[("Gtk", "Dialog"), ("Gtk", "InfoBar")], +) +def response_id_completer(ctx: CompletionContext): + yield Completion( + "action", + CompletionItemKind.Snippet, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "action"), + snippet="action response=$0", + ) + + +@completer( + [language.ChildAnnotation, language.ExtResponse], + [[(TokenType.IDENT, "action"), (TokenType.IDENT, "response"), (TokenType.OP, "=")]], +) +def complete_response_id(ctx: CompletionContext): + gir = ctx.ast_node.root.gir + response_type = gir.get_type("ResponseType", "Gtk") + yield from [ + Completion( + name, + kind=CompletionItemKind.EnumMember, + docs=member.doc, + ) + for name, member in response_type.members.items() + ] + + +@completer( + [language.ChildAnnotation, language.ExtResponse], + [ + [ + (TokenType.IDENT, "action"), + (TokenType.IDENT, "response"), + (TokenType.OP, "="), + (TokenType.IDENT, None), + ], + [ + (TokenType.IDENT, "action"), + (TokenType.IDENT, "response"), + (TokenType.OP, "="), + (TokenType.NUMBER, None), + ], + ], +) +def complete_response_default(ctx: CompletionContext): + yield Completion( + "default", + kind=CompletionItemKind.Keyword, + ) diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index bfca55a..36399b1 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -68,10 +68,22 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: - type = ast_node.root.gir.get_type( - applies_in_subclass[1], applies_in_subclass[0] - ) - if not ast_node.gir_class or not ast_node.gir_class.assignable_to(type): + parent_obj = ast_node + while parent_obj is not None and not hasattr(parent_obj, "gir_class"): + parent_obj = parent_obj.parent + + if ( + parent_obj is None + or not parent_obj.gir_class + or not any( + [ + parent_obj.gir_class.assignable_to( + parent_obj.root.gir.get_type(c[1], c[0]) + ) + for c in applies_in_subclass + ] + ) + ): return any_match = len(matches) == 0 diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 5eb2b60..7f59d96 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -34,9 +34,16 @@ from .gtk_scale import ExtScaleMarks from .gtk_size_group import ExtSizeGroupWidgets from .gtk_string_list import ExtStringListStrings from .gtk_styles import ExtStyles -from .gtkbuilder_child import Child, ChildExtension, ChildInternal, ChildType +from .gtkbuilder_child import ( + Child, + ChildAnnotation, + ChildExtension, + ChildInternal, + ChildType, +) from .gtkbuilder_template import Template from .imports import GtkDirective, Import +from .response_id import ExtResponse from .types import ClassName from .ui import UI from .values import ( diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index c621df0..b1b43a4 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -140,7 +140,7 @@ class ExtAdwResponseDialog(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Adw", "MessageDialog"), + applies_in_subclass=[("Adw", "AlertDialog"), ("Adw", "MessageDialog")], matches=new_statement_patterns, ) def complete_adw_message_dialog(_ctx: CompletionContext): @@ -149,20 +149,6 @@ def complete_adw_message_dialog(_ctx: CompletionContext): ) -@completer( - applies_in=[ObjectContent], - applies_in_subclass=("Adw", "AlertDialog"), - matches=new_statement_patterns, -) -def complete_adw_alert_dialog(_ctx: CompletionContext): - yield Completion( - "responses", - CompletionItemKind.Keyword, - snippet="responses [\n\t$0\n]", - sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "responses"), - ) - - @decompiler("responses") def decompile_responses(ctx, gir): ctx.print(f"responses [") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 5a7a892..aa1fe1d 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -91,7 +91,7 @@ class ExtComboBoxItems(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "ComboBoxText"), + applies_in_subclass=[("Gtk", "ComboBoxText")], matches=new_statement_patterns, ) def items_completer(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index d0e53d2..36e7da4 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -98,7 +98,7 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "FileFilter"), + applies_in_subclass=[("Gtk", "FileFilter")], matches=new_statement_patterns, ) def file_filter_completer(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 63bc0f6..8dd3458 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -90,7 +90,7 @@ class ExtLayout(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "Widget"), + applies_in_subclass=[("Gtk", "Widget")], matches=new_statement_patterns, ) def layout_completer(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 21089a4..e076d4c 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -134,7 +134,7 @@ class ExtScaleMarks(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "Scale"), + applies_in_subclass=[("Gtk", "Scale")], matches=new_statement_patterns, ) def complete_marks(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index d30eef9..e7a6a35 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -101,7 +101,7 @@ class ExtSizeGroupWidgets(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "SizeGroup"), + applies_in_subclass=[("Gtk", "SizeGroup")], matches=new_statement_patterns, ) def size_group_completer(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index a4fa3b5..4d15d32 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -72,7 +72,7 @@ class ExtStringListStrings(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "StringList"), + applies_in_subclass=[("Gtk", "StringList")], matches=new_statement_patterns, ) def strings_completer(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 7c9252c..0836073 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -77,7 +77,7 @@ class ExtStyles(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "Widget"), + applies_in_subclass=[("Gtk", "Widget")], matches=new_statement_patterns, ) def style_completer(_ctx: CompletionContext): diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index bee551c..0eb8f04 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -31,7 +31,12 @@ ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ class ChildInternal(AstNode): - grammar = ["internal-child", UseIdent("internal_child")] + grammar = [ + "[", + "internal-child", + UseIdent("internal_child").expected("internal child name"), + Match("]").expected(), + ] @property def internal_child(self) -> str: @@ -39,7 +44,7 @@ class ChildInternal(AstNode): class ChildType(AstNode): - grammar = UseIdent("child_type").expected("a child type") + grammar = ["[", UseIdent("child_type").expected("a child type"), "]"] @property def child_type(self) -> str: @@ -59,7 +64,7 @@ class ChildExtension(AstNode): class ChildAnnotation(AstNode): - grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] + grammar = AnyOf(ChildInternal, ChildExtension, ChildType) @property def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]: diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 939f71f..83843ed 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -28,19 +28,21 @@ class ExtResponse(AstNode): ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] - grammar = [ + grammar = Statement( + "[", Keyword("action"), Keyword("response"), - "=", + Match("=").expected(), AnyOf( UseIdent("response_id"), [ Optional(UseExact("sign", "-")), UseNumber("response_id"), ], - ), + ).expected("response ID"), Optional([Keyword("default"), UseLiteral("is_default", True)]), - ] + end="]", + ) @validate() def parent_has_action_widgets(self) -> None: diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index e590539..a215f19 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -329,8 +329,9 @@ class Statement(ParseNode): """ParseNode that attempts to match all of its children in sequence. If any child raises an error, the error will be logged but parsing will continue.""" - def __init__(self, *children): + def __init__(self, *children, end: str = ";"): self.children = [to_parse_node(child) for child in children] + self.end = end def _parse(self, ctx) -> bool: for child in self.children: @@ -340,11 +341,16 @@ class Statement(ParseNode): except CompileError as e: ctx.errors.append(e) ctx.set_group_incomplete() + + token = ctx.peek_token() + if str(token) == self.end: + ctx.next_token() + return True token = ctx.peek_token() - if str(token) != ";": - ctx.errors.append(CompileError("Expected `;`", token.range)) + if str(token) != self.end: + ctx.errors.append(CompileError(f"Expected `{self.end}`", token.range)) else: ctx.next_token() return True diff --git a/tests/sample_errors/action_widget_have_no_id.err b/tests/sample_errors/action_widget_have_no_id.err index b239d77..7d1620a 100644 --- a/tests/sample_errors/action_widget_have_no_id.err +++ b/tests/sample_errors/action_widget_have_no_id.err @@ -1 +1 @@ -4,6,22,Action widget must have ID +4,5,24,Action widget must have ID diff --git a/tests/sample_errors/action_widget_in_invalid_container.err b/tests/sample_errors/action_widget_in_invalid_container.err index ef3296c..52c5e5d 100644 --- a/tests/sample_errors/action_widget_in_invalid_container.err +++ b/tests/sample_errors/action_widget_in_invalid_container.err @@ -1 +1 @@ -4,6,18,Gtk.Box doesn't have action widgets +4,5,20,Gtk.Box doesn't have action widgets From e5d6910626549e472e530a783ead0e597bb2c540 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 09:08:05 -0500 Subject: [PATCH 28/34] completions: Fix generated signal handler name A syntax error in the snippet caused the generated signal handler name not to be used. --- blueprintcompiler/completions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 6a2535e..65db3cd 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -356,7 +356,7 @@ def signal_completer(ctx: CompletionContext): .lower() ) - snippet = f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;" + snippet = f"{signal_name} => \\$${{1:{name}_{signal_name.replace('-', '_')}}}()$0;" yield Completion( signal_name, From 8f3ae9a626fbc8fcaa37debe150032d61f6a3264 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 10:10:06 -0500 Subject: [PATCH 29/34] parser: Tweak parsing during error conditions When an explicit parsing error is encountered and a CompileError raised, apply the changes to the context state. This way, the rule that catches the exception (e.g. Statement or Until) knows where the error occurred. Also, changed "Expected" errors to be reported at the end of the previous non-whitespace token. --- blueprintcompiler/errors.py | 47 +++++++++++-------- blueprintcompiler/language/common.py | 1 + blueprintcompiler/language/contexts.py | 12 +++-- blueprintcompiler/parse_tree.py | 30 ++++++++++-- tests/sample_errors/empty.err | 2 +- tests/sample_errors/expected_semicolon.err | 2 +- tests/sample_errors/incomplete_signal.err | 3 +- .../sample_errors/menu_toplevel_attribute.err | 3 +- tests/sample_errors/no_import_version.err | 2 +- tests/test_samples.py | 4 +- 10 files changed, 69 insertions(+), 37 deletions(-) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index df1c2e1..f5d2e06 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -92,29 +92,38 @@ class CompileError(PrintableError): def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: assert self.range is not None - line_num, col_num = utils.idx_to_pos(self.range.start + 1, code) - end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code) - line = code.splitlines(True)[line_num] if code != "" else "" + def format_line(range: Range): + line_num, col_num = utils.idx_to_pos(range.start, code) + end_line_num, end_col_num = utils.idx_to_pos(range.end, code) + line = code.splitlines(True)[line_num] if code != "" else "" - # Display 1-based line numbers - line_num += 1 - end_line_num += 1 + # Display 1-based line numbers + line_num += 1 + end_line_num += 1 + col_num += 1 + end_col_num += 1 - n_spaces = col_num - 1 - n_carets = ( - (end_col_num - col_num) - if line_num == end_line_num - else (len(line) - n_spaces - 1) - ) + n_spaces = col_num - 1 + n_carets = ( + (end_col_num - col_num) + if line_num == end_line_num + else (len(line) - n_spaces - 1) + ) - n_spaces += line.count("\t", 0, col_num) - n_carets += line.count("\t", col_num, col_num + n_carets) - line = line.replace("\t", " ") + n_spaces += line.count("\t", 0, col_num) + n_carets += line.count("\t", col_num, col_num + n_carets) + line = line.replace("\t", " ") + + n_carets = max(n_carets, 1) + + return line_num, col_num, line.rstrip(), (" " * n_spaces) + ("^" * n_carets) + + line_num, col_num, line, carets = format_line(self.range) stream.write( f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n""" +{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n""" ) for hint in self.hints: @@ -139,14 +148,12 @@ at {filename} line {line_num} column {col_num}: ) for ref in self.references: - line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code) - line = code.splitlines(True)[line_num] - line_num += 1 + line_num, col_num, line, carets = format_line(ref.range) stream.write( f"""{Colors.FAINT}note: {ref.message}: at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" +{Colors.FAINT}{line_num :>4} |{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n""" ) stream.write("\n") diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 1cc1b3b..9bd04a5 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -34,6 +34,7 @@ from ..errors import ( CompileError, CompileWarning, DeprecatedWarning, + ErrorReference, MultipleErrors, UnusedWarning, UpgradeWarning, diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 6e26048..38d84f4 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -48,7 +48,7 @@ class ScopeCtx: return self.node @cached_property - def objects(self) -> T.Dict[str, Object]: + def objects(self) -> T.Dict[str, AstNode]: return { obj.tokens["id"]: obj for obj in self._iter_recursive(self.node) @@ -58,7 +58,7 @@ class ScopeCtx: def validate_unique_ids(self) -> None: from .gtk_list_item_factory import ExtListItemFactory - passed = {} + passed: T.Dict[str, AstNode] = {} for obj in self._iter_recursive(self.node): if obj.tokens["id"] is None: continue @@ -71,10 +71,16 @@ class ScopeCtx: raise CompileError( f"Duplicate object ID '{obj.tokens['id']}'", token.range, + references=[ + ErrorReference( + passed[obj.tokens["id"]].group.tokens["id"].range, + "previous declaration was here", + ) + ], ) passed[obj.tokens["id"]] = obj - def _iter_recursive(self, node: AstNode): + def _iter_recursive(self, node: AstNode) -> T.Generator[AstNode, T.Any, None]: yield node for child in node.children: if child.context[ScopeCtx] is self: diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index a215f19..3924ee5 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -235,7 +235,15 @@ class ParseNode: start_idx = ctx.index inner_ctx = ctx.create_child() - if self._parse(inner_ctx): + try: + result = self._parse(inner_ctx) + except Exception as e: + # If an exception occurs, there's an explicit error, not just a rule that didn't match. Apply the context + # state so that whichever rule handles the exception (e.g. a Statement) knows where the error occurred. + ctx.apply_child(inner_ctx) + raise e + + if result: ctx.apply_child(inner_ctx) if ctx.index == start_idx: return ParseResult.EMPTY @@ -269,11 +277,11 @@ class Err(ParseNode): if self.child.parse(ctx).failed(): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: - start_idx += 1 + start_idx -= 1 start_token = ctx.tokens[start_idx] raise CompileError( - self.message, Range(start_token.start, start_token.start, ctx.text) + self.message, Range(start_token.end, start_token.end, ctx.text) ) return True @@ -350,7 +358,20 @@ class Statement(ParseNode): token = ctx.peek_token() if str(token) != self.end: - ctx.errors.append(CompileError(f"Expected `{self.end}`", token.range)) + start_idx = ctx.index - 1 + while ctx.tokens[start_idx].type in SKIP_TOKENS: + start_idx -= 1 + start_token = ctx.tokens[start_idx] + + position = ( + start_token.start if ctx.index - 1 == start_idx else start_token.end + ) + + ctx.errors.append( + CompileError( + f"Expected `{self.end}`", Range(position, position, ctx.text) + ) + ) else: ctx.next_token() return True @@ -411,7 +432,6 @@ class Until(ParseNode): ctx.skip_unexpected_token() except CompileError as e: ctx.errors.append(e) - ctx.next_token() return True diff --git a/tests/sample_errors/empty.err b/tests/sample_errors/empty.err index 854962f..b30f437 100644 --- a/tests/sample_errors/empty.err +++ b/tests/sample_errors/empty.err @@ -1 +1 @@ -1,0,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`) \ No newline at end of file +1,1,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`) \ No newline at end of file diff --git a/tests/sample_errors/expected_semicolon.err b/tests/sample_errors/expected_semicolon.err index bfabc9a..a1b2a36 100644 --- a/tests/sample_errors/expected_semicolon.err +++ b/tests/sample_errors/expected_semicolon.err @@ -1 +1 @@ -6,1,1,Expected `;` \ No newline at end of file +5,4,0,Expected `;` \ No newline at end of file diff --git a/tests/sample_errors/incomplete_signal.err b/tests/sample_errors/incomplete_signal.err index 901ef3b..c61ef28 100644 --- a/tests/sample_errors/incomplete_signal.err +++ b/tests/sample_errors/incomplete_signal.err @@ -1,2 +1 @@ -5,1,0,Expected a signal detail name -4,9,3,Unexpected tokens \ No newline at end of file +4,11,0,Expected a signal detail name \ No newline at end of file diff --git a/tests/sample_errors/menu_toplevel_attribute.err b/tests/sample_errors/menu_toplevel_attribute.err index 8f3ef26..ee588d0 100644 --- a/tests/sample_errors/menu_toplevel_attribute.err +++ b/tests/sample_errors/menu_toplevel_attribute.err @@ -1,2 +1 @@ -4,5,21,Attributes are not permitted at the top level of a menu -4,16,10,Unexpected tokens \ No newline at end of file +4,5,21,Attributes are not permitted at the top level of a menu \ No newline at end of file diff --git a/tests/sample_errors/no_import_version.err b/tests/sample_errors/no_import_version.err index db830e0..4ee792f 100644 --- a/tests/sample_errors/no_import_version.err +++ b/tests/sample_errors/no_import_version.err @@ -1 +1 @@ -1,11,0,Expected a version number for GTK +1,10,0,Expected a version number for GTK diff --git a/tests/test_samples.py b/tests/test_samples.py index 9cd5baf..7d32ecb 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -143,9 +143,9 @@ class TestSamples(unittest.TestCase): ] def error_str(error: CompileError): - line, col = utils.idx_to_pos(error.range.start + 1, blueprint) + line, col = utils.idx_to_pos(error.range.start, blueprint) len = error.range.length - return ",".join([str(line + 1), str(col), str(len), error.message]) + return ",".join([str(line + 1), str(col + 1), str(len), error.message]) actual = "\n".join([error_str(error) for error in errors]) From d5b2ee358955921cb805e50b1374d35b1b3539a5 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 10:14:10 -0500 Subject: [PATCH 30/34] completions: Add GtkScale mark positions --- blueprintcompiler/language/gtk_scale.py | 43 +++++++++++++++++-------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index e076d4c..5dc49d8 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -23,22 +23,20 @@ from .values import StringValue class ExtScaleMark(AstNode): - grammar = [ + grammar = Statement( Keyword("mark"), Match("(").expected(), - [ - Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), - UseNumber("value"), - Optional( - [ - ",", - UseIdent("position"), - Optional([",", StringValue]), - ] - ), - ], - Match(")").expected(), - ] + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value").expected("value"), + Optional( + [ + ",", + UseIdent("position").expected("position"), + Optional([",", to_parse_node(StringValue).expected("label")]), + ] + ), + end=")", + ) @property def value(self) -> float: @@ -153,6 +151,23 @@ def complete_mark(_ctx: CompletionContext): yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") +@completer( + applies_in=[ExtScaleMark], + matches=[[(TokenType.NUMBER, None), (TokenType.PUNCTUATION, ",")]], +) +def complete_mark_position(ctx: CompletionContext): + gir = ctx.ast_node.root.gir + response_type = gir.get_type("PositionType", "Gtk") + yield from [ + Completion( + name, + kind=CompletionItemKind.EnumMember, + docs=member.doc, + ) + for name, member in response_type.members.items() + ] + + @decompiler("marks") def decompile_marks( ctx, From e9206809d62a6c30838093e119d3598cd182d526 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 13:30:55 -0500 Subject: [PATCH 31/34] completions: Add types in typeof<> and as<> --- blueprintcompiler/ast_utils.py | 7 ++ blueprintcompiler/completions.py | 79 ++++++++++++++++++----- blueprintcompiler/completions_utils.py | 10 ++- blueprintcompiler/gir.py | 8 +-- blueprintcompiler/language/__init__.py | 2 +- blueprintcompiler/language/expression.py | 12 +++- blueprintcompiler/language/types.py | 21 +++++- blueprintcompiler/language/values.py | 17 ++--- blueprintcompiler/outputs/xml/__init__.py | 1 + blueprintcompiler/parse_tree.py | 6 +- 10 files changed, 124 insertions(+), 39 deletions(-) diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 8f742e0..b0c5357 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -196,6 +196,13 @@ class AstNode: return None + def get_child_at(self, idx: int) -> "AstNode": + for child in self.children: + if idx in child.range: + return child.get_child_at(idx) + + return self + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: for child in self.children: yield from child.get_semantic_tokens() diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 65db3cd..55d719b 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -21,7 +21,16 @@ import typing as T from . import annotations, gir, language from .ast_utils import AstNode -from .completions_utils import * +from .completions_utils import ( + CompletionContext, + completers, + completer, + get_sort_key, + new_statement_patterns, + get_property_completion, + CompletionItemKind, + CompletionPriority, +) from .language.types import ClassName from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS @@ -38,13 +47,6 @@ def _complete( token_idx: int, next_token: Token, ) -> T.Iterator[Completion]: - for child in ast_node.children: - if child.group.start <= idx and ( - idx < child.group.end or (idx == child.group.end and child.incomplete) - ): - yield from _complete(lsp, child, tokens, idx, token_idx, next_token) - return - prev_tokens: T.List[Token] = [] # collect the 5 previous non-skipped tokens @@ -54,7 +56,7 @@ def _complete( prev_tokens.insert(0, token) token_idx -= 1 - for completer in ast_node.completers: + for completer in completers: yield from completer(prev_tokens, next_token, ast_node, lsp, idx) @@ -84,7 +86,17 @@ def complete( idx = tokens[token_idx].start token_idx -= 1 - yield from _complete(lsp, ast_node, tokens, idx, token_idx, next_token) + child_node = ast_node.get_child_at(idx) + # If the cursor is at the end of a node, completions should be for the next child of the parent, unless the node + # is incomplete. + while ( + child_node.range.end == idx + and not child_node.incomplete + and child_node.parent is not None + ): + child_node = child_node.parent + + yield from _complete(lsp, child_node, tokens, idx, token_idx, next_token) @completer([language.GtkDirective]) @@ -163,7 +175,13 @@ def _available_namespace_completions(ctx: CompletionContext): @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=new_statement_patterns, ) def namespace(ctx: CompletionContext): @@ -184,7 +202,13 @@ def namespace(ctx: CompletionContext): @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], @@ -195,7 +219,11 @@ def object_completer(ctx: CompletionContext): if ns is not None: for c in ns.classes.values(): snippet = c.name - if str(ctx.next_token) != "{": + if ( + str(ctx.next_token) != "{" + and not isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.BracketedTypeName) + ): snippet += " {\n $0\n}" yield Completion( @@ -209,7 +237,13 @@ def object_completer(ctx: CompletionContext): @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=new_statement_patterns, ) def gtk_object_completer(ctx: CompletionContext): @@ -217,7 +251,11 @@ def gtk_object_completer(ctx: CompletionContext): if ns is not None: for c in ns.classes.values(): snippet = c.name - if str(ctx.next_token) != "{": + if ( + str(ctx.next_token) != "{" + and not isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.BracketedTypeName) + ): snippet += " {\n $0\n}" yield Completion( @@ -229,6 +267,17 @@ def gtk_object_completer(ctx: CompletionContext): detail=c.detail, ) + if isinstance(ctx.ast_node, language.BracketedTypeName) or ( + isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.ClassName) + ): + for basic_type in gir.BASIC_TYPES: + yield Completion( + basic_type, + CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, basic_type), + ) + @completer( applies_in=[language.ObjectContent], diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 36399b1..d8bcbc2 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -57,14 +57,21 @@ new_statement_patterns = [ [(TokenType.PUNCTUATION, "}")], [(TokenType.PUNCTUATION, "]")], [(TokenType.PUNCTUATION, ";")], + [(TokenType.OP, "<")], ] +completers = [] + + def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def decorator(func: T.Callable[[CompletionContext], T.Generator[Completion]]): def inner( prev_tokens: T.List[Token], next_token: Token, ast_node, lsp, idx: int ): + if not any(isinstance(ast_node, rule) for rule in applies_in): + return + # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: @@ -118,8 +125,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None ) yield from func(context) - for c in applies_in: - c.completers.append(inner) + completers.append(inner) return inner return decorator diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 333f4ac..6392eb4 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -289,7 +289,7 @@ class TypeType(BasicType): return isinstance(other, TypeType) -_BASIC_TYPES = { +BASIC_TYPES = { "bool": BoolType, "string": StringType, "int": IntType, @@ -914,7 +914,7 @@ class Namespace(GirNode): def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: """Gets a type from this namespace by its C name.""" - for basic in _BASIC_TYPES.values(): + for basic in BASIC_TYPES.values(): if basic.glib_type_name == cname: return basic() @@ -1036,8 +1036,8 @@ class GirContext: return None def get_type(self, name: str, ns: str) -> T.Optional[GirType]: - if ns is None and name in _BASIC_TYPES: - return _BASIC_TYPES[name]() + if ns is None and name in BASIC_TYPES: + return BASIC_TYPES[name]() ns = ns or "Gtk" diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 7f59d96..88d7538 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -44,7 +44,7 @@ from .gtkbuilder_child import ( from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .response_id import ExtResponse -from .types import ClassName +from .types import BracketedTypeName, ClassName, TypeName from .ui import UI from .values import ( ArrayValue, diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index de6fbf1..910cd71 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -21,7 +21,7 @@ from ..decompiler import decompile_element from .common import * from .contexts import ScopeCtx, ValueTypeCtx -from .types import TypeName +from .types import BracketedTypeName, TypeName expr = Sequence() @@ -196,7 +196,7 @@ class CastExpr(InfixExpr): grammar = [ Keyword("as"), AnyOf( - ["<", TypeName, Match(">").expected()], + BracketedTypeName, [ UseExact("lparen", "("), TypeName, @@ -211,7 +211,13 @@ class CastExpr(InfixExpr): @property def type(self) -> T.Optional[GirType]: - return self.children[TypeName][0].gir_type + if len(self.children[BracketedTypeName]) == 1: + type_name = self.children[BracketedTypeName][0].type_name + return None if type_name is None else type_name.gir_type + elif len(self.children[TypeName]) == 1: + return self.children[TypeName][0].gir_type + else: + return None @validate() def cast_makes_sense(self): diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index fe45c4d..da41360 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -27,11 +27,11 @@ class TypeName(AstNode): [ UseIdent("namespace"), ".", - UseIdent("class_name"), + UseIdent("class_name").expected("class name"), ], [ AnyOf("$", [".", UseLiteral("old_extern", True)]), - UseIdent("class_name"), + UseIdent("class_name").expected("class name"), UseLiteral("extern", True), ], UseIdent("class_name"), @@ -47,7 +47,11 @@ class TypeName(AstNode): @validate("class_name") def type_exists(self): - if not self.tokens["extern"] and self.gir_ns is not None: + if ( + not self.tokens["extern"] + and self.gir_ns is not None + and self.tokens["class_name"] is not None + ): self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) @@ -182,3 +186,14 @@ class TemplateClassName(ClassName): self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) + + +class BracketedTypeName(AstNode): + grammar = Statement("<", to_parse_node(TypeName).expected("type name"), end=">") + + @property + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[TypeName]) == 0: + return None + + return self.children[TypeName][0] diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 7fa6bfa..2840337 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -26,7 +26,7 @@ from .common import * from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx from .expression import Expression from .gobject_object import Object -from .types import TypeName +from .types import BracketedTypeName, TypeName class Translated(AstNode): @@ -80,11 +80,7 @@ class TypeLiteral(AstNode): grammar = [ "typeof", AnyOf( - [ - "<", - to_parse_node(TypeName).expected("type name"), - Match(">").expected(), - ], + BracketedTypeName, [ UseExact("lparen", "("), to_parse_node(TypeName).expected("type name"), @@ -98,8 +94,13 @@ class TypeLiteral(AstNode): return gir.TypeType() @property - def type_name(self) -> TypeName: - return self.children[TypeName][0] + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[BracketedTypeName]) == 1: + return self.children[BracketedTypeName][0].type_name + elif len(self.children[TypeName]) == 1: + return self.children[TypeName][0] + else: + return None @validate() def validate_for_type(self) -> None: diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 15850f7..3f52ca5 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -209,6 +209,7 @@ class XmlOutput(OutputFormat): else: xml.put_text(self._object_id(value, value.ident)) elif isinstance(value, TypeLiteral): + assert value.type_name is not None xml.put_text(value.type_name.glib_type_name) else: if isinstance(value.value, float) and value.value == int(value.value): diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 3924ee5..8bb4c66 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -280,9 +280,9 @@ class Err(ParseNode): start_idx -= 1 start_token = ctx.tokens[start_idx] - raise CompileError( - self.message, Range(start_token.end, start_token.end, ctx.text) - ) + position = start_token.start if ctx.start == start_idx else start_token.end + + raise CompileError(self.message, Range(position, position, ctx.text)) return True From 1205fc42eadc9cfba4ab75ec9d81a9d55276aefd Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 14:25:34 -0500 Subject: [PATCH 32/34] completions: Fix completions in identifiers --- blueprintcompiler/completions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 55d719b..73dc27f 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -81,10 +81,10 @@ def complete( if tokens[token_idx].type == TokenType.IDENT: idx = tokens[token_idx].start token_idx -= 1 - else: - while tokens[token_idx].type == TokenType.WHITESPACE: - idx = tokens[token_idx].start - token_idx -= 1 + + while tokens[token_idx].type == TokenType.WHITESPACE: + idx = tokens[token_idx].start + token_idx -= 1 child_node = ast_node.get_child_at(idx) # If the cursor is at the end of a node, completions should be for the next child of the parent, unless the node From 67983aee2e6aa10af32518aa0c286cc492b750a7 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 14:39:02 -0500 Subject: [PATCH 33/34] completions: Object names in signal handlers --- blueprintcompiler/completions.py | 22 +++++++------------- blueprintcompiler/completions_utils.py | 17 ++++++++++++++- blueprintcompiler/language/gobject_signal.py | 8 +++++++ 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 73dc27f..ca32eac 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -23,13 +23,14 @@ from . import annotations, gir, language from .ast_utils import AstNode from .completions_utils import ( CompletionContext, - completers, - completer, - get_sort_key, - new_statement_patterns, - get_property_completion, CompletionItemKind, CompletionPriority, + completer, + completers, + get_object_id_completions, + get_property_completion, + get_sort_key, + new_statement_patterns, ) from .language.types import ClassName from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section @@ -350,16 +351,7 @@ def prop_value_completer(ctx: CompletionContext): sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), ) - for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items(): - if obj.gir_class is not None and obj.gir_class.assignable_to( - vt.value_type - ): - yield Completion( - id, - CompletionItemKind.Variable, - signature=" " + obj.signature, - sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), - ) + yield from get_object_id_completions(ctx, vt.value_type) if isinstance(ctx.ast_node, language.Property): yield from _available_namespace_completions(ctx) diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index d8bcbc2..970d429 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -22,7 +22,7 @@ import typing as T from dataclasses import dataclass from enum import Enum -from . import gir +from . import gir, language from .ast_utils import AstNode from .lsp_utils import Completion, CompletionItemKind from .tokenizer import Token, TokenType @@ -163,3 +163,18 @@ def get_property_completion( snippet=snippet, docs=doc, ) + + +def get_object_id_completions( + ctx: CompletionContext, value_type: T.Optional[gir.GirType] = None +): + for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items(): + if value_type is None or ( + obj.gir_class is not None and obj.gir_class.assignable_to(value_type) + ): + yield Completion( + id, + CompletionItemKind.Variable, + signature=" " + obj.signature, + sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), + ) diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 3b4235f..b6afb09 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -247,3 +247,11 @@ def decompile_signal( line += ";" ctx.print(line) return gir + + +@completer( + [Signal], + [[(TokenType.PUNCTUATION, "(")]], +) +def signal_object_completer(ctx: CompletionContext): + yield from get_object_id_completions(ctx) From 72319b29c610e318c3133fa00ed5f925e6b4abea Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 3 May 2025 14:44:13 -0500 Subject: [PATCH 34/34] completions: Don't suggest "null" where not allowed --- blueprintcompiler/completions.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index ca32eac..81dd03d 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -32,6 +32,7 @@ from .completions_utils import ( get_sort_key, new_statement_patterns, ) +from .language.contexts import ValueTypeCtx from .language.types import ClassName from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS @@ -320,6 +321,8 @@ def prop_value_completer(ctx: CompletionContext): ) if (vt := ctx.ast_node.value_type) is not None: + assert isinstance(vt, ValueTypeCtx) + if isinstance(vt.value_type, gir.Enumeration): for name, member in vt.value_type.members.items(): yield Completion( @@ -345,11 +348,12 @@ def prop_value_completer(ctx: CompletionContext): elif isinstance(vt.value_type, gir.Class) or isinstance( vt.value_type, gir.Interface ): - yield Completion( - "null", - CompletionItemKind.Constant, - sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), - ) + if vt.allow_null: + yield Completion( + "null", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), + ) yield from get_object_id_completions(ctx, vt.value_type)