From d7097cad019c032e803d271d3671d0ee933a522b Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 20 Sep 2024 17:04:00 -0500 Subject: [PATCH 01/41] docs: Mention null in literal values section --- docs/reference/values.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/reference/values.rst b/docs/reference/values.rst index 95f6bc8..fb63e38 100644 --- a/docs/reference/values.rst +++ b/docs/reference/values.rst @@ -25,8 +25,7 @@ Literals NumberLiteral = ( '-' | '+' )? `> IdentLiteral = `> -Literals are used to specify values for properties. They can be strings, numbers, references to objects, types, boolean values, or enum members. - +Literals are used to specify values for properties. They can be strings, numbers, references to objects, ``null``, types, boolean values, or enum members. .. _Syntax TypeLiteral: From 3b6dcf072d6766b04a025a3f13283c2c497fcd19 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Oct 2024 10:21:13 -0500 Subject: [PATCH 02/41] typelib: Fix field offsets for attributes This fixes a bug where the decompiler could not recognize enums by their C identifiers because it could not correctly read attributes. Fixes #177. --- blueprintcompiler/typelib.py | 4 ++-- tests/samples/issue_177.blp | 5 +++++ tests/samples/issue_177.ui | 12 ++++++++++++ tests/test_samples.py | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tests/samples/issue_177.blp create mode 100644 tests/samples/issue_177.ui diff --git a/blueprintcompiler/typelib.py b/blueprintcompiler/typelib.py index 145bf57..be22eb1 100644 --- a/blueprintcompiler/typelib.py +++ b/blueprintcompiler/typelib.py @@ -148,8 +148,8 @@ class Typelib: SIGNATURE_ARGUMENTS = Field(0x8, "offset") ATTR_OFFSET = Field(0x0, "u32") - ATTR_NAME = Field(0x0, "string") - ATTR_VALUE = Field(0x0, "string") + ATTR_NAME = Field(0x4, "string") + ATTR_VALUE = Field(0x8, "string") TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5) TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry") diff --git a/tests/samples/issue_177.blp b/tests/samples/issue_177.blp new file mode 100644 index 0000000..0a0f613 --- /dev/null +++ b/tests/samples/issue_177.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Box { + orientation: horizontal; +} diff --git a/tests/samples/issue_177.ui b/tests/samples/issue_177.ui new file mode 100644 index 0000000..53f9b60 --- /dev/null +++ b/tests/samples/issue_177.ui @@ -0,0 +1,12 @@ + + + + + + GTK_ORIENTATION_HORIZONTAL + + \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 1d1e17f..00ef72a 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -207,7 +207,7 @@ class TestSamples(unittest.TestCase): ] # Decompiler-only tests - SKIP_COMPILE = ["translator_comments"] + SKIP_COMPILE = ["issue_177", "translator_comments"] SKIP_DECOMPILE = [ # Comments are not preserved in either direction From c805400a3919635c7151385673a8c50c52f8728e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedek=20D=C3=A9v=C3=A9nyi?= Date: Sat, 28 Sep 2024 14:34:12 +0000 Subject: [PATCH 03/41] Update file index.rst --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index e5601ea..d405498 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -86,6 +86,7 @@ Built with Blueprint - `Chance `_ - `Commit `_ - `Confy `_ +- `Cozy `_ - `Daikhan `_ - `Damask `_ - `Denaro `_ From 94b532bc35c763a15d74738df1329be9ca0ee23d Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Oct 2024 19:00:47 -0500 Subject: [PATCH 04/41] build: Update Docker container Includes a change to handle a mypy update. --- blueprintcompiler/parser.py | 4 +++- build-aux/install_deps.sh | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 89e1533..1f87647 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -33,7 +33,9 @@ def parse( original_text = tokens[0].string if len(tokens) else "" ctx = ParseContext(tokens, original_text) AnyOf(UI).parse(ctx) - ast_node = ctx.last_group.to_ast() if ctx.last_group else None + + assert ctx.last_group is not None + ast_node = ctx.last_group.to_ast() errors = [*ctx.errors, *ast_node.errors] warnings = [*ctx.warnings, *ast_node.warnings] diff --git a/build-aux/install_deps.sh b/build-aux/install_deps.sh index 381a57a..342778d 100755 --- a/build-aux/install_deps.sh +++ b/build-aux/install_deps.sh @@ -7,8 +7,8 @@ git clone --depth=1 https://gitlab.gnome.org/GNOME/gtk.git cd gtk meson setup builddir \ --prefix=/usr \ - -Dgtk_doc=true \ - -Ddemos=false \ + -Ddocumentation=true \ + -Dbuild-demos=false \ -Dbuild-examples=false \ -Dbuild-tests=false \ -Dbuild-testsuite=false From f6d05be10ba7db95c518d57ad839346db5ad2d01 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Oct 2024 20:44:34 -0500 Subject: [PATCH 05/41] lsp: Add more "go to reference" implementations --- blueprintcompiler/language/adw_breakpoint.py | 11 +++++++++++ blueprintcompiler/language/gobject_signal.py | 10 ++++++++++ blueprintcompiler/language/gtk_size_group.py | 6 ++++++ 3 files changed, 27 insertions(+) diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py index eec7c7e..e7716e6 100644 --- a/blueprintcompiler/language/adw_breakpoint.py +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -115,6 +115,17 @@ class AdwBreakpointSetter(AstNode): self.value.range.text, ) + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + if idx in self.group.tokens["object"].range: + if self.object is not None: + return LocationLink( + self.group.tokens["object"].range, + self.object.range, + self.object.ranges["id"], + ) + + return None + @context(ValueTypeCtx) def value_type(self) -> ValueTypeCtx: if self.gir_property is not None: diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 9348321..8da59a5 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -117,6 +117,16 @@ class Signal(AstNode): self.ranges["detail_start", "detail_end"].text, ) + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + if idx in self.group.tokens["object"].range: + obj = self.context[ScopeCtx].objects.get(self.object_id) + if obj is not None: + return LocationLink( + self.group.tokens["object"].range, obj.range, obj.ranges["id"] + ) + + return None + @validate("handler") def old_extern(self): if not self.tokens["extern"]: diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 0945e69..fd23738 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -39,6 +39,12 @@ class Widget(AstNode): self.group.tokens["name"].range, ) + def get_reference(self, _idx: int) -> T.Optional[LocationLink]: + if obj := self.context[ScopeCtx].objects.get(self.name): + return LocationLink(self.range, obj.range, obj.ranges["id"]) + else: + return None + @validate("name") def obj_widget(self): object = self.context[ScopeCtx].objects.get(self.tokens["name"]) From e5fba8f3c7a9d2880922078e632b70ca26f955c4 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Oct 2024 20:46:26 -0500 Subject: [PATCH 06/41] lsp: Add semantic tokens for flag members --- blueprintcompiler/language/values.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 9b29d94..2678693 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -211,6 +211,13 @@ class Flag(AstNode): else: return None + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + yield SemanticToken( + self.group.tokens["value"].start, + self.group.tokens["value"].end, + SemanticTokenType.EnumMember, + ) + @docs() def docs(self): type = self.context[ValueTypeCtx].value_type From b107a859476a510a2061e599443113b75a646eba Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Oct 2024 20:47:27 -0500 Subject: [PATCH 07/41] lsp: Add property docs on notify signal hover --- blueprintcompiler/language/gobject_signal.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 8da59a5..d2e4526 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -174,6 +174,16 @@ class Signal(AstNode): if self.gir_signal is not None: return self.gir_signal.doc + @docs("detail_name") + def detail_docs(self): + if self.name == "notify": + if self.gir_class is not None and not isinstance( + self.gir_class, ExternType + ): + prop = self.gir_class.properties.get(self.tokens["detail_name"]) + if prop is not None: + return prop.doc + @decompiler("signal") def decompile_signal( From e19975e1f8069e6500d0b05ed1a0e8ecdf0b7e01 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 19 Oct 2024 18:46:10 -0500 Subject: [PATCH 08/41] lsp: Add reference documentation on hover For most constructs and keywords, show the relevant section of the reference documentation on hover. --- blueprintcompiler/ast_utils.py | 6 +- blueprintcompiler/language/adw_breakpoint.py | 4 + .../language/adw_response_dialog.py | 4 + blueprintcompiler/language/binding.py | 8 ++ blueprintcompiler/language/common.py | 1 + blueprintcompiler/language/expression.py | 10 +- blueprintcompiler/language/gobject_signal.py | 10 +- blueprintcompiler/language/gtk_a11y.py | 4 + .../language/gtk_combo_box_text.py | 8 ++ blueprintcompiler/language/gtk_file_filter.py | 23 ++- blueprintcompiler/language/gtk_layout.py | 4 + .../language/gtk_list_item_factory.py | 4 + blueprintcompiler/language/gtk_menu.py | 20 +++ blueprintcompiler/language/gtk_scale.py | 8 ++ blueprintcompiler/language/gtk_size_group.py | 4 + blueprintcompiler/language/gtk_string_list.py | 6 +- blueprintcompiler/language/gtk_styles.py | 4 + .../language/gtkbuilder_child.py | 4 + .../language/gtkbuilder_template.py | 4 + blueprintcompiler/language/imports.py | 8 ++ blueprintcompiler/language/response_id.py | 10 ++ .../language/translation_domain.py | 4 + blueprintcompiler/language/values.py | 13 ++ blueprintcompiler/lsp_utils.py | 26 ++++ docs/collect-sections.py | 136 ++++++++++++++++++ docs/meson.build | 8 ++ docs/reference/extensions.rst | 2 +- meson.build | 4 +- 28 files changed, 326 insertions(+), 21 deletions(-) create mode 100755 docs/collect-sections.py diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index c874358..bd5befa 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -179,14 +179,16 @@ class AstNode: token = self.group.tokens.get(attr.token_name) if token and token.start <= idx < token.end: return getattr(self, name) - else: - return getattr(self, name) for child in self.children: if idx in child.range: if docs := child.get_docs(idx): return docs + for name, attr in self._attrs_by_type(Docs): + if not attr.token_name: + return getattr(self, name) + return None def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py index e7716e6..4ad5b24 100644 --- a/blueprintcompiler/language/adw_breakpoint.py +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -207,6 +207,10 @@ class AdwBreakpointSetters(AstNode): def unique(self): self.validate_unique_in_parent("Duplicate setters block") + @docs("setters") + def ref_docs(self): + return get_docs_section("Syntax ExtAdwBreakpoint") + @decompiler("condition", cdata=True) def decompile_condition(ctx: DecompileCtx, gir, cdata): diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index 46172f2..5493d4d 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -138,6 +138,10 @@ class ExtAdwResponseDialog(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate responses block") + @docs() + def ref_docs(self): + return get_docs_section("Syntax ExtAdwMessageDialog") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py index 3b9af97..07572a9 100644 --- a/blueprintcompiler/language/binding.py +++ b/blueprintcompiler/language/binding.py @@ -58,6 +58,10 @@ class BindingFlag(AstNode): "Only bindings with a single lookup can have flags", ) + @docs() + def ref_docs(self): + return get_docs_section("Syntax Binding") + class Binding(AstNode): grammar = [ @@ -99,6 +103,10 @@ class Binding(AstNode): actions=[CodeAction("use 'bind'", "bind")], ) + @docs("bind") + def ref_docs(self): + return get_docs_section("Syntax Binding") + @dataclass class SimpleBinding: diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 29df47d..1cc1b3b 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -55,6 +55,7 @@ from ..lsp_utils import ( SemanticToken, SemanticTokenType, SymbolKind, + get_docs_section, ) from ..parse_tree import * diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 37e73e8..558392c 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -174,7 +174,7 @@ class LookupOp(InfixExpr): class CastExpr(InfixExpr): grammar = [ - "as", + Keyword("as"), AnyOf( ["<", TypeName, Match(">").expected()], [ @@ -220,6 +220,10 @@ class CastExpr(InfixExpr): ], ) + @docs("as") + def ref_docs(self): + return get_docs_section("Syntax CastExpression") + class ClosureArg(AstNode): grammar = Expression @@ -269,6 +273,10 @@ class ClosureExpr(ExprBase): if not self.tokens["extern"]: raise CompileError(f"{self.closure_name} is not a builtin function") + @docs("name") + def ref_docs(self): + return get_docs_section("Syntax ClosureExpression") + expr.children = [ AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]), diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index d2e4526..79f9ae7 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -40,6 +40,10 @@ class SignalFlag(AstNode): f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag ) + @docs() + def ref_docs(self): + return get_docs_section("Syntax Signal") + class Signal(AstNode): grammar = Statement( @@ -50,7 +54,7 @@ class Signal(AstNode): UseIdent("detail_name").expected("a signal detail name"), ] ), - "=>", + Keyword("=>"), Mark("detail_start"), Optional(["$", UseLiteral("extern", True)]), UseIdent("handler").expected("the name of a function to handle the signal"), @@ -184,6 +188,10 @@ class Signal(AstNode): if prop is not None: return prop.doc + @docs("=>") + def ref_docs(self): + return get_docs_section("Syntax Signal") + @decompiler("signal") def decompile_signal( diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 8870a74..3657565 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -225,6 +225,10 @@ class ExtAccessibility(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate accessibility block") + @docs("accessibility") + def ref_docs(self): + return get_docs_section("Syntax ExtAccessibility") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 3a0d7c7..312750a 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -55,6 +55,10 @@ class Item(AstNode): f"Duplicate item '{self.name}'", lambda x: x.name == self.name ) + @docs("name") + def ref_docs(self): + return get_docs_section("Syntax ExtComboBoxItems") + class ExtComboBoxItems(AstNode): grammar = [ @@ -81,6 +85,10 @@ class ExtComboBoxItems(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate items block") + @docs("items") + def ref_docs(self): + return get_docs_section("Syntax ExtComboBoxItems") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 482d6e1..e84afc7 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -29,25 +29,23 @@ class Filters(AstNode): self.tokens["tag_name"], SymbolKind.Array, self.range, - self.group.tokens[self.tokens["tag_name"]].range, + self.group.tokens["tag_name"].range, ) @validate() def container_is_file_filter(self): validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") - @validate() + @validate("tag_name") def unique_in_parent(self): - # The token argument to validate() needs to be calculated based on - # the instance, hence wrapping it like this. - @validate(self.tokens["tag_name"]) - def wrapped_validator(self): - self.validate_unique_in_parent( - f"Duplicate {self.tokens['tag_name']} block", - check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], - ) + self.validate_unique_in_parent( + f"Duplicate {self.tokens['tag_name']} block", + check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], + ) - wrapped_validator(self) + @docs("tag_name") + def ref_docs(self): + return get_docs_section("Syntax ExtFileFilter") class FilterString(AstNode): @@ -76,8 +74,7 @@ def create_node(tag_name: str, singular: str): return Group( Filters, [ - Keyword(tag_name), - UseLiteral("tag_name", tag_name), + UseExact("tag_name", tag_name), "[", Delimited( Group( diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 508609d..8d3e37a 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -83,6 +83,10 @@ class ExtLayout(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate layout block") + @docs("layout") + def ref_docs(self): + return get_docs_section("Syntax ExtLayout") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_list_item_factory.py b/blueprintcompiler/language/gtk_list_item_factory.py index c54547f..3309c08 100644 --- a/blueprintcompiler/language/gtk_list_item_factory.py +++ b/blueprintcompiler/language/gtk_list_item_factory.py @@ -108,3 +108,7 @@ class ExtListItemFactory(AstNode): just hear to satisfy XmlOutput._emit_object_or_template """ return None + + @docs("id") + def ref_docs(self): + return get_docs_section("Syntax ExtListItemFactory") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index a77484a..c7ef5f2 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -70,6 +70,25 @@ class Menu(AstNode): if self.id in RESERVED_IDS: raise CompileWarning(f"{self.id} may be a confusing object ID") + @docs("menu") + def ref_docs_menu(self): + return get_docs_section("Syntax Menu") + + @docs("section") + def ref_docs_section(self): + return get_docs_section("Syntax Menu") + + @docs("submenu") + def ref_docs_submenu(self): + return get_docs_section("Syntax Menu") + + @docs("item") + def ref_docs_item(self): + if self.tokens["shorthand"]: + return get_docs_section("Syntax MenuItemShorthand") + else: + return get_docs_section("Syntax Menu") + class MenuAttribute(AstNode): tag_name = "attribute" @@ -156,6 +175,7 @@ menu_item_shorthand = Group( [ Keyword("item"), UseLiteral("tag", "item"), + UseLiteral("shorthand", True), "(", Group( MenuAttribute, diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index ac4b77c..1fd5ac3 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -94,6 +94,10 @@ class ExtScaleMark(AstNode): did_you_mean=(self.position, positions.keys()), ) + @docs("mark") + def ref_docs(self): + return get_docs_section("Syntax ExtScaleMarks") + class ExtScaleMarks(AstNode): grammar = [ @@ -123,6 +127,10 @@ class ExtScaleMarks(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate 'marks' block") + @docs("marks") + def ref_docs(self): + return get_docs_section("Syntax ExtScaleMarks") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index fd23738..54d85e5 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -94,6 +94,10 @@ class ExtSizeGroupWidgets(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate widgets block") + @docs("widgets") + def ref_docs(self): + return get_docs_section("Syntax ExtSizeGroupWidgets") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 36a01f6..a146f35 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -57,7 +57,7 @@ class ExtStringListStrings(AstNode): self.group.tokens["strings"].range, ) - @validate("items") + @validate("strings") def container_is_string_list(self): validate_parent_type(self, "Gtk", "StringList", "StringList items") @@ -65,6 +65,10 @@ class ExtStringListStrings(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate strings block") + @docs("strings") + def ref_docs(self): + return get_docs_section("Syntax ExtStringListStrings") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 236dde0..8617522 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -70,6 +70,10 @@ class ExtStyles(AstNode): def unique_in_parent(self): self.validate_unique_in_parent("Duplicate styles block") + @docs("styles") + def ref_docs(self): + return get_docs_section("Syntax ExtStyles") + @completer( applies_in=[ObjectContent], diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index b0563ba..bee551c 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -53,6 +53,10 @@ class ChildExtension(AstNode): def child(self) -> ExtResponse: return self.children[0] + @docs() + def ref_docs(self): + return get_docs_section("Syntax ChildExtension") + class ChildAnnotation(AstNode): grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 29f7b37..96383eb 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -88,6 +88,10 @@ class Template(Object): f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}", ) + @docs("id") + def ref_docs(self): + return get_docs_section("Syntax Template") + @decompiler("template") def decompile_template(ctx: DecompileCtx, gir, klass, parent=None): diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index bf5dddd..3060bea 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -68,6 +68,10 @@ class GtkDirective(AstNode): # For better error handling, just assume it's 4.0 return gir.get_namespace("Gtk", "4.0") + @docs() + def ref_docs(self): + return get_docs_section("Syntax GtkDecl") + class Import(AstNode): grammar = Statement( @@ -105,3 +109,7 @@ class Import(AstNode): return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) except CompileError: return None + + @docs() + def ref_docs(self): + return get_docs_section("Syntax Using") diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 8c0c807..939f71f 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -124,6 +124,16 @@ class ExtResponse(AstNode): object = self.parent_by_type(Child).object return object.id + @docs() + def ref_docs(self): + return get_docs_section("Syntax ExtResponse") + + @docs("response_id") + def response_id_docs(self): + if enum := self.root.gir.get_type("ResponseType", "Gtk"): + if member := enum.members.get(self.response_id, None): + return member.doc + def decompile_response_type(parent_element, child_element): obj_id = None diff --git a/blueprintcompiler/language/translation_domain.py b/blueprintcompiler/language/translation_domain.py index ff20ead..0f60af9 100644 --- a/blueprintcompiler/language/translation_domain.py +++ b/blueprintcompiler/language/translation_domain.py @@ -29,3 +29,7 @@ class TranslationDomain(AstNode): @property def domain(self): return self.tokens["domain"] + + @docs() + def ref_docs(self): + return get_docs_section("Syntax TranslationDomain") diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 2678693..63cf4fc 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -20,6 +20,7 @@ import typing as T from blueprintcompiler.gir import ArrayType +from blueprintcompiler.lsp_utils import SemanticToken from .common import * from .contexts import ScopeCtx, ValueTypeCtx @@ -56,6 +57,10 @@ class Translated(AstNode): f"Cannot convert translated string to {expected_type.full_name}" ) + @docs() + def ref_docs(self): + return get_docs_section("Syntax Translated") + class TypeLiteral(AstNode): grammar = [ @@ -101,6 +106,10 @@ class TypeLiteral(AstNode): ], ) + @docs() + def ref_docs(self): + return get_docs_section("Syntax TypeLiteral") + class QuotedLiteral(AstNode): grammar = UseQuoted("value") @@ -258,6 +267,10 @@ class Flags(AstNode): if expected_type is not None and not isinstance(expected_type, gir.Bitfield): raise CompileError(f"{expected_type.full_name} is not a bitfield type") + @docs() + def ref_docs(self): + return get_docs_section("Syntax Flags") + class IdentLiteral(AstNode): grammar = UseIdent("value") diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 9362e8c..b938181 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -19,6 +19,8 @@ import enum +import json +import os import typing as T from dataclasses import dataclass, field @@ -200,3 +202,27 @@ class TextEdit: def to_json(self): return {"range": self.range.to_json(), "newText": self.newText} + + +_docs_sections: T.Optional[dict[str, T.Any]] = None + + +def get_docs_section(section_name: str) -> T.Optional[str]: + global _docs_sections + + if _docs_sections is None: + try: + with open( + os.path.join(os.path.dirname(__file__), "reference_docs.json") + ) as f: + _docs_sections = json.load(f) + except FileNotFoundError: + _docs_sections = {} + + if section := _docs_sections.get(section_name): + content = section["content"] + link = section["link"] + content += f"\n\n---\n\n[Online documentation]({link})" + return content + else: + return None diff --git a/docs/collect-sections.py b/docs/collect-sections.py new file mode 100755 index 0000000..c6e01b3 --- /dev/null +++ b/docs/collect-sections.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +import json +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +__all__ = ["get_docs_section"] + +DOCS_ROOT = "https://jwestman.pages.gitlab.gnome.org/blueprint-compiler" + + +sections: dict[str, "Section"] = {} + + +@dataclass +class Section: + link: str + lines: str + + def to_json(self): + return { + "content": rst_to_md(self.lines), + "link": self.link, + } + + +def load_reference_docs(): + for filename in Path(os.path.dirname(__file__), "reference").glob("*.rst"): + with open(filename) as f: + section_name = None + lines = [] + + def close_section(): + if section_name: + html_file = re.sub(r"\.rst$", ".html", filename.name) + anchor = re.sub(r"[^a-z0-9]+", "-", section_name.lower()) + link = f"{DOCS_ROOT}/reference/{html_file}#{anchor}" + sections[section_name] = Section(link, lines) + + for line in f: + if m := re.match(r"\.\.\s+_(.*):", line): + close_section() + section_name = m.group(1) + lines = [] + else: + lines.append(line) + + close_section() + + +# This isn't a comprehensive rST to markdown converter, it just needs to handle the +# small subset of rST used in the reference docs. +def rst_to_md(lines: list[str]) -> str: + result = "" + + def rst_to_md_inline(line): + line = re.sub(r"``(.*?)``", r"`\1`", line) + line = re.sub( + r":ref:`(.*?)<(.*?)>`", + lambda m: f"[{m.group(1)}]({sections[m.group(2)].link})", + line, + ) + line = re.sub(r"`([^`]*?) <([^`>]*?)>`_", r"[\1](\2)", line) + return line + + i = 0 + n = len(lines) + heading_levels = {} + + def print_block(lang: str = "", code: bool = True, strip_links: bool = False): + nonlocal result, i + block = "" + while i < n: + line = lines[i].rstrip() + if line.startswith(" "): + line = line[3:] + elif line != "": + break + + if strip_links: + line = re.sub(r":ref:`(.*?)<(.*?)>`", r"\1", line) + + if not code: + line = rst_to_md_inline(line) + + block += line + "\n" + i += 1 + + if code: + result += f"```{lang}\n{block.strip()}\n```\n\n" + else: + result += block + + while i < n: + line = lines[i].rstrip() + i += 1 + if line == ".. rst-class:: grammar-block": + print_block(strip_links=True) + elif line == ".. code-block:: blueprint": + print_block("blueprint") + elif line == ".. note::": + result += "#### Note\n" + print_block(code=False) + elif m := re.match(r"\.\. image:: (.*)", line): + result += f"![{m.group(1)}]({DOCS_ROOT}/_images/{m.group(1)})\n" + elif i < n and re.match(r"^((-+)|(~+)|(\++))$", lines[i]): + level_char = lines[i][0] + if level_char not in heading_levels: + heading_levels[level_char] = max(heading_levels.values(), default=1) + 1 + result += ( + "#" * heading_levels[level_char] + " " + rst_to_md_inline(line) + "\n" + ) + i += 1 + else: + result += rst_to_md_inline(line) + "\n" + + return result + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: collect_sections.py ") + sys.exit(1) + + outfile = sys.argv[1] + + load_reference_docs() + + # 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 + ) diff --git a/docs/meson.build b/docs/meson.build index 95e545d..d9ad736 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -9,3 +9,11 @@ custom_target('docs', ) endif + +custom_target('reference_docs.json', + output: 'reference_docs.json', + command: [meson.current_source_dir() / 'collect-sections.py', '@OUTPUT@'], + build_always_stale: true, + install: true, + install_dir: py.get_install_dir() / 'blueprintcompiler', +) \ No newline at end of file diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index 93ed0fc..0961d14 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -227,7 +227,7 @@ Valid in `Gtk.BuilderListItemFactory `_, `Gtk.ColumnViewRow `_, or `Gtk.ColumnViewCell `_ The template object can be referenced with the ``template`` keyword. +The template type must be `Gtk.ListItem `_, `Gtk.ColumnViewRow `_, or `Gtk.ColumnViewCell `_. The template object can be referenced with the ``template`` keyword. .. code-block:: blueprint diff --git a/meson.build b/meson.build index f676459..63d9489 100644 --- a/meson.build +++ b/meson.build @@ -2,13 +2,13 @@ project('blueprint-compiler', version: '0.14.0', ) -subdir('docs') - prefix = get_option('prefix') datadir = join_paths(prefix, get_option('datadir')) py = import('python').find_installation('python3') +subdir('docs') + configure_file( input: 'blueprint-compiler.pc.in', output: 'blueprint-compiler.pc', From a529a619553a722536c614287ce9ce69bc6720dc Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 3 Nov 2024 14:17:59 -0600 Subject: [PATCH 09/41] docs: Corrections, updates, and improvements --- docs/collect-sections.py | 2 +- docs/reference/diagnostics.rst | 2 +- docs/reference/expressions.rst | 14 +++++++------- docs/reference/index.rst | 2 +- docs/reference/templates.rst | 7 ++++--- docs/reference/values.rst | 12 ++++++------ 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/collect-sections.py b/docs/collect-sections.py index c6e01b3..e6227e7 100755 --- a/docs/collect-sections.py +++ b/docs/collect-sections.py @@ -98,7 +98,7 @@ def rst_to_md(lines: list[str]) -> str: line = lines[i].rstrip() i += 1 if line == ".. rst-class:: grammar-block": - print_block(strip_links=True) + print_block("text", strip_links=True) elif line == ".. code-block:: blueprint": print_block("blueprint") elif line == ".. note::": diff --git a/docs/reference/diagnostics.rst b/docs/reference/diagnostics.rst index 2c0af59..c8774d6 100644 --- a/docs/reference/diagnostics.rst +++ b/docs/reference/diagnostics.rst @@ -21,7 +21,7 @@ The tokenizer encountered an unexpected sequence of characters that aren't part child_not_accepted ------------------ -The parent class does not have child widgets (it does not implement `Gtk.Buildable `_ and is not a subclass of `Gio.ListStore `_). Some classes use properties instead of children to add widgets. Check the parent class's documentation. +The parent class does not have child objects (it does not implement `Gtk.Buildable `_ and is not a subclass of `Gio.ListStore `_). Some classes use properties instead of children to add widgets. Check the parent class's documentation. .. _Diagnostic conversion_error: diff --git a/docs/reference/expressions.rst b/docs/reference/expressions.rst index 1ba50ee..8688ff0 100644 --- a/docs/reference/expressions.rst +++ b/docs/reference/expressions.rst @@ -8,10 +8,10 @@ automatically. .. code-block:: blueprint - label: bind MyAppWindow.account.username; - /* ^ ^ ^ - | creates lookup expressions that are re-evaluated when - | the account's username *or* the account itself changes + label: bind template.account.username; + /* ^ ^ ^ + | creates lookup expressions that are re-evaluated when + | the account's username *or* the account itself changes | binds the `label` property to the expression's output */ @@ -49,7 +49,7 @@ Lookup Expressions LookupExpression = '.' `> -Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal `_ +Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal `_. The type of a property expression is the type of the property it refers to. @@ -65,7 +65,7 @@ Closure Expressions Closure expressions allow you to perform additional calculations that aren't supported in blueprint by writing those calculations as application code. These application-defined functions are created in the same way as :ref:`signal handlers`. -Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, properties of their inputs or outside variables. +Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, not properties of their inputs or outside variables. Blueprint doesn't know the closure's return type, so closure expressions must be cast to the correct return type using a :ref:`cast expression`. @@ -79,7 +79,7 @@ Cast Expressions CastExpression = 'as' '<' :ref:`TypeName` '>' -Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. +Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types. .. code-block:: blueprint diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 7670ba5..d49feb9 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -31,7 +31,7 @@ Tokens IDENT ~~~~~ -An identifier starts with an ASCII underscore ``_`` or letter ``[A-Za-z]`` and consists of ASCII underscores, letters, digits ``[0-9]``, and dashes ``-``. Dashes are included for historical reasons, since GObject properties are traditionally kebab-case. +An identifier starts with an ASCII underscore ``_`` or letter ``[A-Za-z]`` and consists of ASCII underscores, letters, digits ``[0-9]``, and dashes ``-``. Dashes are included for historical reasons, since GObject properties and signals are traditionally kebab-case. .. _Syntax NUMBER: diff --git a/docs/reference/templates.rst b/docs/reference/templates.rst index a1a9968..74e4225 100644 --- a/docs/reference/templates.rst +++ b/docs/reference/templates.rst @@ -90,6 +90,7 @@ To reference the template object in a binding or expression, use the ``template` Language Implementations ------------------------ -- ``gtk_widget_class_set_template ()`` in C: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml -- ``#[template]`` in gtk-rs: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html -- ``GObject.registerClass()`` in GJS: https://gjs.guide/guides/gtk/3/14-templates.html \ No newline at end of file +- **C** ``gtk_widget_class_set_template ()``: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml +- **gtk-rs** ``#[template]``: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html +- **GJS** ``GObject.registerClass()``: https://gjs.guide/guides/gtk/3/14-templates.html +- **PyGObject** ``@Gtk.Template``: https://pygobject.gnome.org/guide/gtk_template.html diff --git a/docs/reference/values.rst b/docs/reference/values.rst index fb63e38..fd414a8 100644 --- a/docs/reference/values.rst +++ b/docs/reference/values.rst @@ -109,7 +109,7 @@ Bindings .. rst-class:: grammar-block Binding = 'bind' :ref:`Expression` (BindingFlag)* - BindingFlag = 'inverted' | 'bidirectional' | 'sync-create' + BindingFlag = 'inverted' | 'bidirectional' | 'no-sync-create' Bindings keep a property updated as other properties change. They can be used to keep the UI in sync with application data, or to connect two parts of the UI. @@ -120,8 +120,8 @@ Simple Bindings A binding that consists of a source object and a single lookup is called a "simple binding". These are implemented using `GObject property bindings `_ and support a few flags: -- ``bidirectional``: The binding is two-way, so changes to the target property will also update the source property. - ``inverted``: For boolean properties, the target is set to the inverse of the source property. +- ``bidirectional``: The binding is two-way, so changes to the target property will also update the source property. - ``no-sync-create``: Normally, when a binding is created, the target property is immediately updated with the current value of the source property. This flag disables that behavior, and the bound property will be updated the next time the source property changes. Complex Bindings @@ -137,11 +137,11 @@ Example /* Use bindings to show a label when a switch * is active, without any application code */ - Switch advanced_feature {} + Switch show_label {} - Label warning { - visible: bind advanced_feature.active; - label: _("This is an advanced feature. Use with caution!"); + Label { + visible: bind show_label.active; + label: _("I'm a label that's only visible when the switch is enabled!"); } .. _Syntax ObjectValue: From 3bf8fc151a2bd3366e3a46cf22a5b323c0c620e4 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 3 Nov 2024 14:36:51 -0600 Subject: [PATCH 10/41] tests: Ignore deprecation warnings Ignore deprecation warnings in the error handling tests, except in the test specifically for deprecations. This prevents them from breaking if libraries introduce new deprecations. Fixes #178. --- tests/sample_errors/legacy_template.err | 1 - tests/test_samples.py | 14 ++++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/sample_errors/legacy_template.err b/tests/sample_errors/legacy_template.err index 79ad519..876b0cf 100644 --- a/tests/sample_errors/legacy_template.err +++ b/tests/sample_errors/legacy_template.err @@ -1,3 +1,2 @@ 3,10,12,Use type syntax here (introduced in blueprint 0.8.0) -8,1,6,Gtk.Dialog is deprecated 9,18,12,Use 'template' instead of the class name (introduced in 0.8.0) \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 00ef72a..fe90774 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -130,18 +130,20 @@ class TestSamples(unittest.TestCase): if len(warnings): raise MultipleErrors(warnings) except PrintableError as e: + # Ignore deprecation warnings because new versions of libraries can introduce + # new deprecations, which would cause the tests to fail + errors = [ + error + for error in (e.errors if isinstance(e, MultipleErrors) else [e]) + if (name == "deprecations" or not isinstance(error, DeprecatedWarning)) + ] def error_str(error: CompileError): line, col = utils.idx_to_pos(error.range.start + 1, blueprint) len = error.range.length return ",".join([str(line + 1), str(col), str(len), error.message]) - if isinstance(e, CompileError): - actual = error_str(e) - elif isinstance(e, MultipleErrors): - actual = "\n".join([error_str(error) for error in e.errors]) - else: # pragma: no cover - raise AssertionError() + actual = "\n".join([error_str(error) for error in errors]) self.assertEqual(actual.strip(), expected.strip()) else: # pragma: no cover From 90308b69e0c6e968c389142de118d5c6a00135e2 Mon Sep 17 00:00:00 2001 From: Vladimir Vaskov Date: Mon, 16 Sep 2024 18:44:03 +0000 Subject: [PATCH 11/41] docs: Add app making use of Blueprint --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index d405498..345e613 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,6 +81,7 @@ Built with Blueprint - `AdwSteamGtk `_ - `Blurble `_ - `Bottles `_ +- `Cassette `_ - `Cartridges `_ - `Cavalier `_ - `Chance `_ From a42ec3a9450d865b9856d7ad3128edadd9a5fe1b Mon Sep 17 00:00:00 2001 From: Vladimir Vaskov Date: Sat, 19 Oct 2024 20:39:57 +0000 Subject: [PATCH 12/41] docs: Put the cassette in the correct alphabetical place --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 345e613..34b942c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,8 +81,8 @@ Built with Blueprint - `AdwSteamGtk `_ - `Blurble `_ - `Bottles `_ -- `Cassette `_ - `Cartridges `_ +- `Cassette `_ - `Cavalier `_ - `Chance `_ - `Commit `_ From 6acf0fe5a0f1573962cd14ff88435db94ce4a109 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 9 Dec 2024 18:59:25 -0600 Subject: [PATCH 13/41] tests: Test deprecations separately Libraries can add new deprecations, or the environment you're running the tests in might have old libraries where the things we test aren't deprecated yet. Move the deprecations test into its own module with its own code, so it can check library versions and skip the test if it won't work. --- blueprintcompiler/language/expression.py | 18 ++++ tests/sample_errors/deprecations.blp | 9 -- tests/sample_errors/deprecations.err | 1 - tests/test_deprecations.py | 110 +++++++++++++++++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) delete mode 100644 tests/sample_errors/deprecations.blp delete mode 100644 tests/sample_errors/deprecations.err create mode 100644 tests/test_deprecations.py diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 558392c..ae9c399 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -171,6 +171,24 @@ class LookupOp(InfixExpr): did_you_mean=(self.property_name, self.lhs.type.properties.keys()), ) + @validate("property") + def property_deprecated(self): + if self.lhs.type is None or not ( + isinstance(self.lhs.type, gir.Class) + or isinstance(self.lhs.type, gir.Interface) + ): + return + + if property := self.lhs.type.properties.get(self.property_name): + if property.deprecated: + hints = [] + if property.deprecated_doc: + hints.append(property.deprecated_doc) + raise DeprecatedWarning( + f"{property.signature} is deprecated", + hints=hints, + ) + class CastExpr(InfixExpr): grammar = [ diff --git a/tests/sample_errors/deprecations.blp b/tests/sample_errors/deprecations.blp deleted file mode 100644 index f67f002..0000000 --- a/tests/sample_errors/deprecations.blp +++ /dev/null @@ -1,9 +0,0 @@ -using Gtk 4.0; - -Dialog { - use-header-bar: 1; -} - -Window { - keys-changed => $on_window_keys_changed(); -} diff --git a/tests/sample_errors/deprecations.err b/tests/sample_errors/deprecations.err deleted file mode 100644 index e3abd61..0000000 --- a/tests/sample_errors/deprecations.err +++ /dev/null @@ -1 +0,0 @@ -3,1,6,Gtk.Dialog is deprecated \ No newline at end of file diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py new file mode 100644 index 0000000..0ffe726 --- /dev/null +++ b/tests/test_deprecations.py @@ -0,0 +1,110 @@ +# test_samples.py +# +# Copyright 2024 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import unittest + +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk + +from blueprintcompiler import parser, tokenizer +from blueprintcompiler.errors import DeprecatedWarning, PrintableError + +# Testing deprecation warnings requires special handling because libraries can add deprecations with new versions, +# causing tests to break if we're not careful. + + +class TestDeprecations(unittest.TestCase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.gtkVersion = f"{Gtk.get_major_version()}.{Gtk.get_minor_version()}.{Gtk.get_micro_version()}" + + def assertDeprecation(self, blueprint: str, message: str): + try: + tokens = tokenizer.tokenize(blueprint) + _ast, errors, warnings = parser.parse(tokens) + + self.assertIsNone(errors) + self.assertEqual(len(warnings), 1) + self.assertIsInstance(warnings[0], DeprecatedWarning) + self.assertEqual(warnings[0].message, message) + except PrintableError as e: # pragma: no cover + e.pretty_print("", blueprint) + raise AssertionError() + + def test_class_deprecation(self): + if Gtk.check_version(4, 10, 0) is not None: + self.skipTest(f"Gtk.Dialog is not deprecated in GTK {self.gtkVersion}") + + blueprint = """ + using Gtk 4.0; + + Dialog { + use-header-bar: 1; + } + """ + message = "Gtk.Dialog is deprecated" + + self.assertDeprecation(blueprint, message) + + def test_property_deprecation(self): + self.skipTest( + "gobject-introspection does not currently write property deprecations to the typelib. See ." + ) + + if Gtk.check_version(4, 4, 0) is not None: + self.skipTest( + f"Gtk.DropTarget:drop is not deprecated in GTK {self.gtkVersion}" + ) + + blueprint = """ + using Gtk 4.0; + + $MyObject { + a: bind drop_target.drop; + } + + DropTarget drop_target { + } + """ + + message = "Gtk.DropTarget:drop is deprecated" + + self.assertDeprecation(blueprint, message) + + def test_signal_deprecation(self): + if Gtk.check_version(4, 10, 0) is not None: + self.skipTest( + f"Gtk.Window::keys-changed is not deprecated in GTK {self.gtkVersion}" + ) + + blueprint = """ + using Gtk 4.0; + + Window { + keys-changed => $handler(); + } + """ + + message = "signal Gtk.Window::keys-changed () is deprecated" + + self.assertDeprecation(blueprint, message) From 778a979714f8eecbbe47953be8b23b6f2fe5a752 Mon Sep 17 00:00:00 2001 From: Luoyayu Date: Sat, 7 Dec 2024 07:36:14 +0000 Subject: [PATCH 14/41] lsp: Fix format of JSON-RPC content part ending with \r\n --- blueprintcompiler/lsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 25b289f..0659154 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -149,7 +149,7 @@ class LanguageServer: def _send(self, data): data["jsonrpc"] = "2.0" - line = json.dumps(data, separators=(",", ":")) + "\r\n" + line = json.dumps(data, separators=(",", ":")) printerr("output: " + line) sys.stdout.write( f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}" From ac70ea7403abebc895b3b60082842604156babfd Mon Sep 17 00:00:00 2001 From: Jordan Petridis Date: Wed, 13 Nov 2024 00:48:16 +0200 Subject: [PATCH 15/41] Port to libgirepository-2.0 pygobject 3.52 has switched [1] to using libgirepository-2.0 which comes from glib itself now, rather than the 1.0 which came from gobject-introspection. This means that it fails to load the incompatible "GIRepository 2.0" and thus must be ported to 3.0 (which is provided by libgirepository-2.0). Migration guide is here [2] [1]: https://gitlab.gnome.org/GNOME/pygobject/-/merge_requests/320 [2]: https://docs.gtk.org/girepository/migrating-gi.html This commit adds suppport for importing with "gi.require_version("GIRepository", "3.0") and falling back to the existing "GIRepository 2.0" if not found. --- blueprintcompiler/gir.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 30a5eaa..e54b849 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -24,8 +24,20 @@ from functools import cached_property import gi # type: ignore -gi.require_version("GIRepository", "2.0") -from gi.repository import GIRepository # type: ignore +try: + gi.require_version("GIRepository", "3.0") + from gi.repository import GIRepository # type: ignore + + _repo = GIRepository.Repository() +except ValueError: + # We can remove this once we can bump the minimum dependencies + # to glib 2.80 and pygobject 3.52 + # dependency('glib-2.0', version: '>= 2.80.0') + # dependency('girepository-2.0', version: '>= 2.80.0') + gi.require_version("GIRepository", "2.0") + from gi.repository import GIRepository # type: ignore + + _repo = GIRepository.Repository from . import typelib, xml_reader from .errors import CompileError, CompilerBugError @@ -42,7 +54,7 @@ def add_typelib_search_path(path: str): def get_namespace(namespace: str, version: str) -> "Namespace": - search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths] + search_paths = [*_repo.get_search_path(), *_user_search_paths] filename = f"{namespace}-{version}.typelib" @@ -74,7 +86,7 @@ def get_available_namespaces() -> T.List[T.Tuple[str, str]]: return _available_namespaces search_paths: list[str] = [ - *GIRepository.Repository.get_search_path(), + *_repo.get_search_path(), *_user_search_paths, ] From f48b840cfa478d6bfeb6e16f24fd5b650c70e4f2 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Wed, 20 Nov 2024 10:41:56 +0100 Subject: [PATCH 16/41] 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 17/41] 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 18/41] 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 5b0f662478343270a1ada82938534cc0c91bef07 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 21 Dec 2024 17:22:56 -0600 Subject: [PATCH 19/41] completions: Detect translatable properties Looked through the Gtk documentation (and a few other libraries) to make a list of all the properties that should probably be translated. If a property is on the list, the language server will mark it as translated in completions. --- blueprintcompiler/annotations.py | 191 +++++++++++++++++++++++++++++++ blueprintcompiler/completions.py | 10 +- 2 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 blueprintcompiler/annotations.py diff --git a/blueprintcompiler/annotations.py b/blueprintcompiler/annotations.py new file mode 100644 index 0000000..c40de13 --- /dev/null +++ b/blueprintcompiler/annotations.py @@ -0,0 +1,191 @@ +# annotations.py +# +# Copyright 2024 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +# Extra information about types in common libraries that's used for things like completions. + +import typing as T +from dataclasses import dataclass + +from . import gir + + +@dataclass +class Annotation: + translatable_properties: T.List[str] + + +def is_property_translated(property: gir.Property): + ns = property.get_containing(gir.Namespace) + ns_name = ns.name + "-" + ns.version + if annotation := _ANNOTATIONS.get(ns_name): + assert property.container is not None + return ( + property.container.name + ":" + property.name + in annotation.translatable_properties + ) + else: + return False + + +_ANNOTATIONS = { + "Gtk-4.0": Annotation( + translatable_properties=[ + "AboutDialog:comments", + "AboutDialog:translator-credits", + "AboutDialog:website-label", + "AlertDialog:detail", + "AlertDialog:message", + "AppChooserButton:heading", + "AppChooserDialog:heading", + "AppChooserWidget:default-text", + "AssistantPage:title", + "Button:label", + "CellRendererText:markup", + "CellRendererText:placeholder-text", + "CellRendererText:text", + "CheckButton:label", + "ColorButton:title", + "ColorDialog:title", + "ColumnViewColumn:title", + "ColumnViewRow:accessible-description", + "ColumnViewRow:accessible-label", + "Entry:placeholder-text", + "Entry:primary-icon-tooltip-markup", + "Entry:primary-icon-tooltip-text", + "Entry:secondary-icon-tooltip-markup", + "Entry:secondary-icon-tooltip-text", + "EntryBuffer:text", + "Expander:label", + "FileChooserNative:accept-label", + "FileChooserNative:cancel-label", + "FileChooserWidget:subtitle", + "FileDialog:accept-label", + "FileDialog:title", + "FileDialog:initial-name", + "FileFilter:name", + "FontButton:title", + "FontDialog:title", + "Frame:label", + "Inscription:markup", + "Inscription:text", + "Label:label", + "ListItem:accessible-description", + "ListItem:accessible-label", + "LockButton:text-lock", + "LockButton:text-unlock", + "LockButton:tooltip-lock", + "LockButton:tooltip-not-authorized", + "LockButton:tooltip-unlock", + "MenuButton:label", + "MessageDialog:secondary-text", + "MessageDialog:text", + "NativeDialog:title", + "NotebookPage:menu-label", + "NotebookPage:tab-label", + "PasswordEntry:placeholder-text", + "Picture:alternative-text", + "PrintDialog:accept-label", + "PrintDialog:title", + "Printer:name", + "PrintJob:title", + "PrintOperation:custom-tab-label", + "PrintOperation:export-filename", + "PrintOperation:job-name", + "ProgressBar:text", + "SearchEntry:placeholder-text", + "ShortcutLabel:disabled-text", + "ShortcutsGroup:title", + "ShortcutsSection:title", + "ShortcutsShortcut:title", + "ShortcutsShortcut:subtitle", + "StackPage:title", + "Text:placeholder-text", + "TextBuffer:text", + "TreeViewColumn:title", + "Widget:tooltip-markup", + "Widget:tooltip-text", + "Window:title", + "Editable:text", + "FontChooser:preview-text", + ] + ), + "Adw-1": Annotation( + translatable_properties=[ + "AboutDialog:comments", + "AboutDialog:translator-credits", + "AboutWindow:comments", + "AboutWindow:translator-credits", + "ActionRow:subtitle", + "ActionRow:title", + "AlertDialog:body", + "AlertDialog:heading", + "Avatar:text", + "Banner:button-label", + "Banner:title", + "ButtonContent:label", + "Dialog:title", + "ExpanderRow:subtitle", + "MessageDialog:body", + "MessageDialog:heading", + "NavigationPage:title", + "PreferencesGroup:description", + "PreferencesGroup:title", + "PreferencesPage:description", + "PreferencesPage:title", + "PreferencesRow:title", + "SplitButton:dropdown-tooltip", + "SplitButton:label", + "StatusPage:description", + "StatusPage:title", + "TabPage:indicator-tooltip", + "TabPage:keyword", + "TabPage:title", + "Toast:button-label", + "Toast:title", + "ViewStackPage:title", + "ViewSwitcherTitle:subtitle", + "ViewSwitcherTitle:title", + "WindowTitle:subtitle", + "WindowTitle:title", + ] + ), + "Shumate-1.0": Annotation( + translatable_properties=[ + "License:extra-text", + "MapSource:license", + "MapSource:name", + ] + ), + "GtkSource-5": Annotation( + translatable_properties=[ + "CompletionCell:markup", + "CompletionCell:text", + "CompletionSnippets:title", + "CompletionWords:title", + "GutterRendererText:markup", + "GutterRendererText:text", + "SearchSettings:search-text", + "Snippet:description", + "Snippet:name", + "SnippetChunk:tooltip-text", + "StyleScheme:description", + "StyleScheme:name", + ] + ), +} diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index e05d6ee..e682513 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -20,7 +20,7 @@ import sys import typing as T -from . import gir, language +from . import annotations, gir, language from .ast_utils import AstNode from .completions_utils import * from .language.types import ClassName @@ -154,11 +154,17 @@ def property_completer(lsp, ast_node, match_variables): detail=prop.detail, ) 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=f'{prop_name}: "$0";', + snippet=snippet, docs=prop.doc, detail=prop.detail, ) From 9b9fab832bb5dc3a23b6a25ac8233f7db1c62976 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 22 Dec 2024 18:00:39 -0600 Subject: [PATCH 20/41] Add tests, remove unused code, fix bugs - Added tests for more error messages - Test the "go to reference" feature at every character index of every test case - Delete unused code and imports - Fix some bugs I found along the way --- blueprintcompiler/ast_utils.py | 32 +++++++++++---- blueprintcompiler/completions.py | 11 ++--- blueprintcompiler/completions_utils.py | 11 ----- blueprintcompiler/formatter.py | 7 ++-- blueprintcompiler/gir.py | 41 +++++++------------ blueprintcompiler/language/__init__.py | 3 +- .../language/adw_response_dialog.py | 5 --- blueprintcompiler/language/attributes.py | 32 --------------- blueprintcompiler/language/expression.py | 21 ---------- .../language/gobject_property.py | 2 +- blueprintcompiler/language/gobject_signal.py | 2 +- blueprintcompiler/language/gtk_a11y.py | 4 +- .../language/gtk_combo_box_text.py | 1 - .../language/gtk_list_item_factory.py | 11 ++--- blueprintcompiler/language/imports.py | 14 ++----- blueprintcompiler/outputs/xml/__init__.py | 5 ++- blueprintcompiler/outputs/xml/xml_emitter.py | 4 +- blueprintcompiler/parse_tree.py | 35 +--------------- tests/formatting/comment_in.blp | 2 + tests/formatting/comment_out.blp | 2 + .../sample_errors/float_to_int_assignment.blp | 5 +++ .../sample_errors/float_to_int_assignment.err | 1 + tests/sample_errors/int_object.blp | 3 ++ tests/sample_errors/int_object.err | 1 + tests/sample_errors/menu_assignment.blp | 7 ++++ tests/sample_errors/menu_assignment.err | 1 + .../string_to_num_assignment.blp | 5 +++ .../string_to_num_assignment.err | 1 + .../string_to_object_assignment.blp | 5 +++ .../string_to_object_assignment.err | 1 + .../string_to_type_assignment.blp | 6 +++ .../string_to_type_assignment.err | 1 + tests/sample_errors/translated_assignment.blp | 5 +++ tests/sample_errors/translated_assignment.err | 1 + tests/sample_errors/typeof_assignment.blp | 5 +++ tests/sample_errors/typeof_assignment.err | 1 + tests/sample_errors/unrecognized_syntax.blp | 1 + tests/sample_errors/unrecognized_syntax.err | 1 + tests/sample_errors/upgrade_sync_create.blp | 5 +++ tests/sample_errors/upgrade_sync_create.err | 1 + .../upgrade_template_list_item.blp | 5 +++ .../upgrade_template_list_item.err | 1 + tests/samples/property_binding.blp | 1 + tests/samples/property_binding.ui | 1 + tests/samples/property_binding_dec.blp | 11 ----- tests/test_formatter.py | 1 + tests/test_samples.py | 8 +++- 47 files changed, 140 insertions(+), 190 deletions(-) delete mode 100644 blueprintcompiler/language/attributes.py create mode 100644 tests/formatting/comment_in.blp create mode 100644 tests/formatting/comment_out.blp create mode 100644 tests/sample_errors/float_to_int_assignment.blp create mode 100644 tests/sample_errors/float_to_int_assignment.err create mode 100644 tests/sample_errors/int_object.blp create mode 100644 tests/sample_errors/int_object.err create mode 100644 tests/sample_errors/menu_assignment.blp create mode 100644 tests/sample_errors/menu_assignment.err create mode 100644 tests/sample_errors/string_to_num_assignment.blp create mode 100644 tests/sample_errors/string_to_num_assignment.err create mode 100644 tests/sample_errors/string_to_object_assignment.blp create mode 100644 tests/sample_errors/string_to_object_assignment.err create mode 100644 tests/sample_errors/string_to_type_assignment.blp create mode 100644 tests/sample_errors/string_to_type_assignment.err create mode 100644 tests/sample_errors/translated_assignment.blp create mode 100644 tests/sample_errors/translated_assignment.err create mode 100644 tests/sample_errors/typeof_assignment.blp create mode 100644 tests/sample_errors/typeof_assignment.err create mode 100644 tests/sample_errors/unrecognized_syntax.blp create mode 100644 tests/sample_errors/unrecognized_syntax.err create mode 100644 tests/sample_errors/upgrade_sync_create.blp create mode 100644 tests/sample_errors/upgrade_sync_create.err create mode 100644 tests/sample_errors/upgrade_template_list_item.blp create mode 100644 tests/sample_errors/upgrade_template_list_item.err delete mode 100644 tests/samples/property_binding_dec.blp diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index bd5befa..8f742e0 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -160,6 +160,11 @@ class AstNode: yield e if e.fatal: return + except MultipleErrors as e: + for error in e.errors: + yield error + if error.fatal: + return for child in self.children: yield from child._get_errors() @@ -249,14 +254,7 @@ def validate( if skip_incomplete and self.incomplete: return - try: - func(self) - except CompileError as e: - # If the node is only partially complete, then an error must - # have already been reported at the parsing stage - if self.incomplete: - return - + def fill_error(e: CompileError): if e.range is None: e.range = ( Range.join( @@ -266,8 +264,26 @@ def validate( or self.range ) + try: + func(self) + except CompileError as e: + # If the node is only partially complete, then an error must + # have already been reported at the parsing stage + if self.incomplete: + return + + fill_error(e) + # Re-raise the exception raise e + except MultipleErrors as e: + if self.incomplete: + return + + for error in e.errors: + fill_error(error) + + raise e inner._validator = True return inner diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index e682513..5d36739 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -17,7 +17,6 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -import sys import typing as T from . import annotations, gir, language @@ -31,10 +30,6 @@ from .tokenizer import Token, TokenType Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] -def debug(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - - def _complete( lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int ) -> T.Iterator[Completion]: @@ -139,7 +134,7 @@ def gtk_object_completer(lsp, ast_node, match_variables): matches=new_statement_patterns, ) def property_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): + if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"): for prop_name, prop in ast_node.gir_class.properties.items(): if ( isinstance(prop.type, gir.BoolType) @@ -194,7 +189,7 @@ def property_completer(lsp, ast_node, match_variables): @completer( - applies_in=[language.Property, language.BaseAttribute], + applies_in=[language.Property, language.A11yProperty], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) def prop_value_completer(lsp, ast_node, match_variables): @@ -218,7 +213,7 @@ def prop_value_completer(lsp, ast_node, match_variables): matches=new_statement_patterns, ) def signal_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): + 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" diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 03bec0f..eccf125 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -31,17 +31,6 @@ new_statement_patterns = [ ] -def applies_to(*ast_types): - """Decorator describing which AST nodes the completer should apply in.""" - - def decorator(func): - for c in ast_types: - c.completers.append(func) - return func - - return decorator - - 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): diff --git a/blueprintcompiler/formatter.py b/blueprintcompiler/formatter.py index c003d45..35da5d2 100644 --- a/blueprintcompiler/formatter.py +++ b/blueprintcompiler/formatter.py @@ -20,7 +20,8 @@ import re from enum import Enum -from . import tokenizer, utils +from . import tokenizer +from .errors import CompilerBugError from .tokenizer import TokenType OPENING_TOKENS = ("{", "[") @@ -192,8 +193,8 @@ def format(data, tab_size=2, insert_space=True): commit_current_line(LineType.COMMENT, newlines_before=newlines) - else: - commit_current_line() + else: # pragma: no cover + raise CompilerBugError() elif str_item == "(" and ( re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index e54b849..333f4ac 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -467,10 +467,13 @@ class Signature(GirNode): return result @cached_property - def return_type(self) -> GirType: - return self.get_containing(Repository)._resolve_type_id( - self.tl.SIGNATURE_RETURN_TYPE - ) + def return_type(self) -> T.Optional[GirType]: + if self.tl.SIGNATURE_RETURN_TYPE == 0: + return None + else: + return self.get_containing(Repository)._resolve_type_id( + self.tl.SIGNATURE_RETURN_TYPE + ) class Signal(GirNode): @@ -490,7 +493,10 @@ class Signal(GirNode): args = ", ".join( [f"{a.type.full_name} {a.name}" for a in self.gir_signature.args] ) - return f"signal {self.container.full_name}::{self.name} ({args})" + result = f"signal {self.container.full_name}::{self.name} ({args})" + if self.gir_signature.return_type is not None: + result += f" -> {self.gir_signature.return_type.full_name}" + return result @property def online_docs(self) -> T.Optional[str]: @@ -902,14 +908,6 @@ class Namespace(GirNode): if isinstance(entry, Class) } - @cached_property - def interfaces(self) -> T.Mapping[str, Interface]: - return { - name: entry - for name, entry in self.entries.items() - if isinstance(entry, Interface) - } - def get_type(self, name) -> T.Optional[GirType]: """Gets a type (class, interface, enum, etc.) from this namespace.""" return self.entries.get(name) @@ -933,13 +931,8 @@ class Namespace(GirNode): """Looks up a type in the scope of this namespace (including in the namespace's dependencies).""" - if type_name in _BASIC_TYPES: - return _BASIC_TYPES[type_name]() - elif "." in type_name: - ns, name = type_name.split(".", 1) - return self.get_containing(Repository).get_type(name, ns) - else: - return self.get_type(type_name) + ns, name = type_name.split(".", 1) + return self.get_containing(Repository).get_type(name, ns) @property def online_docs(self) -> T.Optional[str]: @@ -958,7 +951,7 @@ class Repository(GirNode): self.includes = { name: get_namespace(name, version) for name, version in deps } - except: + except: # pragma: no cover raise CompilerBugError(f"Failed to load dependencies.") else: self.includes = {} @@ -966,12 +959,6 @@ class Repository(GirNode): def get_type(self, name: str, ns: str) -> T.Optional[GirType]: return self.lookup_namespace(ns).get_type(name) - def get_type_by_cname(self, name: str) -> T.Optional[GirType]: - for ns in [self.namespace, *self.includes.values()]: - if type := ns.get_type_by_cname(name): - return type - return None - def lookup_namespace(self, ns: str): """Finds a namespace among this namespace's dependencies.""" if ns == self.namespace.name: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index b302686..e797eaa 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -4,7 +4,6 @@ from .adw_breakpoint import ( AdwBreakpointSetters, ) from .adw_response_dialog import ExtAdwResponseDialog -from .attributes import BaseAttribute from .binding import Binding from .common import * from .contexts import ScopeCtx, ValueTypeCtx @@ -20,7 +19,7 @@ from .expression import ( from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal -from .gtk_a11y import ExtAccessibility +from .gtk_a11y import A11yProperty, ExtAccessibility from .gtk_combo_box_text import ExtComboBoxItems from .gtk_file_filter import ( Filters, diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index 5493d4d..d2680fd 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -20,7 +20,6 @@ from ..decompiler import decompile_translatable, truthy from .common import * -from .contexts import ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type from .values import StringValue @@ -94,10 +93,6 @@ class ExtAdwResponseDialogResponse(AstNode): self.value.range.text, ) - @context(ValueTypeCtx) - def value_type(self) -> ValueTypeCtx: - return ValueTypeCtx(StringType()) - @validate("id") def unique_in_parent(self): self.validate_unique_in_parent( diff --git a/blueprintcompiler/language/attributes.py b/blueprintcompiler/language/attributes.py deleted file mode 100644 index 8ff1f0b..0000000 --- a/blueprintcompiler/language/attributes.py +++ /dev/null @@ -1,32 +0,0 @@ -# attributes.py -# -# Copyright 2022 James Westman -# -# This file is free software; you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This file is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program. If not, see . -# -# SPDX-License-Identifier: LGPL-3.0-or-later - - -from .common import * - - -class BaseAttribute(AstNode): - """A helper class for attribute syntax of the form `name: literal_value;`""" - - tag_name: str = "" - attr_name: str = "name" - - @property - def name(self): - return self.tokens["name"] diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index ae9c399..f305035 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -38,10 +38,6 @@ class ExprBase(AstNode): def type(self) -> T.Optional[GirType]: raise NotImplementedError() - @property - def type_complete(self) -> bool: - return True - @property def rhs(self) -> T.Optional["ExprBase"]: if isinstance(self.parent, Expression): @@ -65,10 +61,6 @@ class Expression(ExprBase): def type(self) -> T.Optional[GirType]: return self.last.type - @property - def type_complete(self) -> bool: - return self.last.type_complete - class InfixExpr(ExprBase): @property @@ -99,15 +91,6 @@ class LiteralExpr(ExprBase): def type(self) -> T.Optional[GirType]: return self.literal.value.type - @property - def type_complete(self) -> bool: - from .values import IdentLiteral - - if isinstance(self.literal.value, IdentLiteral): - if object := self.context[ScopeCtx].objects.get(self.literal.value.ident): - return not object.gir_class.incomplete - return True - class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] @@ -211,10 +194,6 @@ class CastExpr(InfixExpr): def type(self) -> T.Optional[GirType]: return self.children[TypeName][0].gir_type - @property - def type_complete(self) -> bool: - return True - @validate() def cast_makes_sense(self): if self.type is None or self.lhs.type is None: diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 5d0c867..b553909 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -51,7 +51,7 @@ class Property(AstNode): @property def document_symbol(self) -> DocumentSymbol: - if isinstance(self.value, ObjectValue): + if isinstance(self.value, ObjectValue) or self.value is None: detail = None else: detail = self.value.range.text diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 79f9ae7..0e0332e 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -122,7 +122,7 @@ class Signal(AstNode): ) def get_reference(self, idx: int) -> T.Optional[LocationLink]: - if idx in self.group.tokens["object"].range: + if self.object_id is not None and idx in self.group.tokens["object"].range: obj = self.context[ScopeCtx].objects.get(self.object_id) if obj is not None: return LocationLink( diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 3657565..0cc3cb3 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -19,8 +19,6 @@ import typing as T -from ..decompiler import escape_quote -from .attributes import BaseAttribute from .common import * from .contexts import ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type @@ -119,7 +117,7 @@ def _get_docs(gir, name): return gir_type.doc -class A11yProperty(BaseAttribute): +class A11yProperty(AstNode): grammar = Statement( UseIdent("name"), ":", diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 312750a..32b3486 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -19,7 +19,6 @@ from .common import * -from .contexts import ValueTypeCtx from .gobject_object import ObjectContent, validate_parent_type from .values import StringValue diff --git a/blueprintcompiler/language/gtk_list_item_factory.py b/blueprintcompiler/language/gtk_list_item_factory.py index 3309c08..c9e1399 100644 --- a/blueprintcompiler/language/gtk_list_item_factory.py +++ b/blueprintcompiler/language/gtk_list_item_factory.py @@ -50,7 +50,7 @@ class ExtListItemFactory(AstNode): else: return self.root.gir.get_type("ListItem", "Gtk") - @validate("template") + @validate("id") def container_is_builder_list(self): validate_parent_type( self, @@ -59,7 +59,7 @@ class ExtListItemFactory(AstNode): "sub-templates", ) - @validate("template") + @validate("id") def unique_in_parent(self): self.validate_unique_in_parent("Duplicate template block") @@ -76,7 +76,7 @@ class ExtListItemFactory(AstNode): f"Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here" ) - @validate("template") + @validate("id") def type_name_upgrade(self): if self.type_name is None: raise UpgradeWarning( @@ -103,10 +103,7 @@ class ExtListItemFactory(AstNode): @property def action_widgets(self): - """ - The sub-template shouldn't have it`s own actions this is - just hear to satisfy XmlOutput._emit_object_or_template - """ + # The sub-template shouldn't have its own actions, this is just here to satisfy XmlOutput._emit_object_or_template return None @docs("id") diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index 3060bea..2d4bcf6 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -59,14 +59,8 @@ class GtkDirective(AstNode): @property def gir_namespace(self): - # validate the GTK version first to make sure the more specific error - # message is emitted - self.gtk_version() - if self.tokens["version"] is not None: - return gir.get_namespace("Gtk", self.tokens["version"]) - else: - # For better error handling, just assume it's 4.0 - return gir.get_namespace("Gtk", "4.0") + # For better error handling, just assume it's 4.0 + return gir.get_namespace("Gtk", "4.0") @docs() def ref_docs(self): @@ -90,7 +84,7 @@ class Import(AstNode): @validate("namespace", "version") def namespace_exists(self): - gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) + gir.get_namespace(self.namespace, self.version) @validate() def unused(self): @@ -106,7 +100,7 @@ class Import(AstNode): @property def gir_namespace(self): try: - return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) + return gir.get_namespace(self.namespace, self.version) except CompileError: return None diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 5e43834..420f6ef 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -366,12 +366,13 @@ class XmlOutput(OutputFormat): elif isinstance(extension, ExtScaleMarks): xml.start_tag("marks") - for mark in extension.children: + for mark in extension.marks: + label = mark.label.child if mark.label is not None else None xml.start_tag( "mark", value=mark.value, position=mark.position, - **self._translated_string_attrs(mark.label and mark.label.child), + **self._translated_string_attrs(label), ) if mark.label is not None: xml.put_text(mark.label.string) diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index ca87a49..ea91e03 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -40,7 +40,9 @@ class XmlEmitter: self._tag_stack = [] self._needs_newline = False - def start_tag(self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None]): + def start_tag( + self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None, float] + ): self._indent() self.result += f"<{tag}" for key, val in attrs.items(): diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index fff6e4a..ae062fb 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -95,19 +95,11 @@ class ParseGroup: try: return self.ast_type(self, children, self.keys, incomplete=self.incomplete) - except TypeError as e: + except TypeError: # pragma: no cover raise CompilerBugError( f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace." ) - def __str__(self): - result = str(self.ast_type.__name__) - result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n" - result += "\n".join( - [str(child) for children in self.children.values() for child in children] - ) - return result.replace("\n", "\n ") - class ParseContext: """Contains the state of the parser.""" @@ -265,10 +257,6 @@ class ParseNode: """Convenience method for err().""" return self.err("Expected " + expect) - def warn(self, message) -> "Warning": - """Causes this ParseNode to emit a warning if it parses successfully.""" - return Warning(self, message) - class Err(ParseNode): """ParseNode that emits a compile error if it fails to parse.""" @@ -290,27 +278,6 @@ class Err(ParseNode): return True -class Warning(ParseNode): - """ParseNode that emits a compile warning if it parses successfully.""" - - def __init__(self, child, message: str): - self.child = to_parse_node(child) - self.message = message - - def _parse(self, ctx: ParseContext): - ctx.skip() - start_idx = ctx.index - if self.child.parse(ctx).succeeded(): - start_token = ctx.tokens[start_idx] - end_token = ctx.tokens[ctx.index] - ctx.warnings.append( - CompileWarning(self.message, start_token.start, end_token.end) - ) - return True - else: - return False - - class Fail(ParseNode): """ParseNode that emits a compile error if it parses successfully.""" diff --git a/tests/formatting/comment_in.blp b/tests/formatting/comment_in.blp new file mode 100644 index 0000000..32a907c --- /dev/null +++ b/tests/formatting/comment_in.blp @@ -0,0 +1,2 @@ +using Gtk 4.0; +//comment \ No newline at end of file diff --git a/tests/formatting/comment_out.blp b/tests/formatting/comment_out.blp new file mode 100644 index 0000000..d5dca95 --- /dev/null +++ b/tests/formatting/comment_out.blp @@ -0,0 +1,2 @@ +using Gtk 4.0; +// comment diff --git a/tests/sample_errors/float_to_int_assignment.blp b/tests/sample_errors/float_to_int_assignment.blp new file mode 100644 index 0000000..73b5dc4 --- /dev/null +++ b/tests/sample_errors/float_to_int_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Entry { + margin-bottom: 10.5; +} diff --git a/tests/sample_errors/float_to_int_assignment.err b/tests/sample_errors/float_to_int_assignment.err new file mode 100644 index 0000000..0e9dc41 --- /dev/null +++ b/tests/sample_errors/float_to_int_assignment.err @@ -0,0 +1 @@ +4,18,4,Cannot convert 10.5 to integer \ No newline at end of file diff --git a/tests/sample_errors/int_object.blp b/tests/sample_errors/int_object.blp new file mode 100644 index 0000000..35b2562 --- /dev/null +++ b/tests/sample_errors/int_object.blp @@ -0,0 +1,3 @@ +using Gtk 4.0; + +int {} diff --git a/tests/sample_errors/int_object.err b/tests/sample_errors/int_object.err new file mode 100644 index 0000000..221e6e6 --- /dev/null +++ b/tests/sample_errors/int_object.err @@ -0,0 +1 @@ +3,1,3,int is not a class \ No newline at end of file diff --git a/tests/sample_errors/menu_assignment.blp b/tests/sample_errors/menu_assignment.blp new file mode 100644 index 0000000..9188d8a --- /dev/null +++ b/tests/sample_errors/menu_assignment.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Overlay { + child: my_menu; +} + +menu my_menu {} diff --git a/tests/sample_errors/menu_assignment.err b/tests/sample_errors/menu_assignment.err new file mode 100644 index 0000000..fb3187f --- /dev/null +++ b/tests/sample_errors/menu_assignment.err @@ -0,0 +1 @@ +4,10,7,Cannot assign Gio.Menu to Gtk.Widget \ No newline at end of file diff --git a/tests/sample_errors/string_to_num_assignment.blp b/tests/sample_errors/string_to_num_assignment.blp new file mode 100644 index 0000000..22e8fba --- /dev/null +++ b/tests/sample_errors/string_to_num_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Entry { + margin-bottom: "10"; +} diff --git a/tests/sample_errors/string_to_num_assignment.err b/tests/sample_errors/string_to_num_assignment.err new file mode 100644 index 0000000..98a6160 --- /dev/null +++ b/tests/sample_errors/string_to_num_assignment.err @@ -0,0 +1 @@ +4,18,4,Cannot convert string to number \ No newline at end of file diff --git a/tests/sample_errors/string_to_object_assignment.blp b/tests/sample_errors/string_to_object_assignment.blp new file mode 100644 index 0000000..0c070ba --- /dev/null +++ b/tests/sample_errors/string_to_object_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Button { + child: "Click me"; +} diff --git a/tests/sample_errors/string_to_object_assignment.err b/tests/sample_errors/string_to_object_assignment.err new file mode 100644 index 0000000..f9492af --- /dev/null +++ b/tests/sample_errors/string_to_object_assignment.err @@ -0,0 +1 @@ +4,10,10,Cannot convert string to Gtk.Widget \ No newline at end of file diff --git a/tests/sample_errors/string_to_type_assignment.blp b/tests/sample_errors/string_to_type_assignment.blp new file mode 100644 index 0000000..90f531b --- /dev/null +++ b/tests/sample_errors/string_to_type_assignment.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; +using Gio 2.0; + +Gio.ListStore { + item-type: "Button"; +} diff --git a/tests/sample_errors/string_to_type_assignment.err b/tests/sample_errors/string_to_type_assignment.err new file mode 100644 index 0000000..adb9eb0 --- /dev/null +++ b/tests/sample_errors/string_to_type_assignment.err @@ -0,0 +1 @@ +5,14,8,Cannot convert string to GType \ No newline at end of file diff --git a/tests/sample_errors/translated_assignment.blp b/tests/sample_errors/translated_assignment.blp new file mode 100644 index 0000000..fa8fa9d --- /dev/null +++ b/tests/sample_errors/translated_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Button { + child: _("Click me"); +} diff --git a/tests/sample_errors/translated_assignment.err b/tests/sample_errors/translated_assignment.err new file mode 100644 index 0000000..78f6b96 --- /dev/null +++ b/tests/sample_errors/translated_assignment.err @@ -0,0 +1 @@ +4,10,13,Cannot convert translated string to Gtk.Widget \ No newline at end of file diff --git a/tests/sample_errors/typeof_assignment.blp b/tests/sample_errors/typeof_assignment.blp new file mode 100644 index 0000000..a20d92c --- /dev/null +++ b/tests/sample_errors/typeof_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Button { + label: typeof 6 diff --git a/tests/samples/property_binding_dec.blp b/tests/samples/property_binding_dec.blp deleted file mode 100644 index 39e4458..0000000 --- a/tests/samples/property_binding_dec.blp +++ /dev/null @@ -1,11 +0,0 @@ -using Gtk 4.0; - -Box { - visible: bind box2.visible inverted; - orientation: bind box2.orientation; - spacing: bind box2.spacing no-sync-create; -} - -Box box2 { - spacing: 6; -} diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 4cf9ac8..af4f9f5 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -46,3 +46,4 @@ class TestFormatter(unittest.TestCase): self.assert_format_test("in2.blp", "out.blp") self.assert_format_test("correct1.blp", "correct1.blp") self.assert_format_test("string_in.blp", "string_out.blp") + self.assert_format_test("comment_in.blp", "comment_out.blp") diff --git a/tests/test_samples.py b/tests/test_samples.py index fe90774..5eb1538 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -28,6 +28,7 @@ gi.require_version("Gtk", "4.0") from gi.repository import Gtk from blueprintcompiler import decompiler, parser, tokenizer, utils +from blueprintcompiler.ast_utils import AstNode from blueprintcompiler.completions import complete from blueprintcompiler.errors import ( CompileError, @@ -61,11 +62,14 @@ class TestSamples(unittest.TestCase): except: pass - def assert_ast_doesnt_crash(self, text, tokens, ast): + def assert_ast_doesnt_crash(self, text, tokens, ast: AstNode): + lsp = LanguageServer() for i in range(len(text)): ast.get_docs(i) for i in range(len(text)): - list(complete(LanguageServer(), ast, tokens, i)) + list(complete(lsp, ast, tokens, i)) + for i in range(len(text)): + ast.get_reference(i) ast.get_document_symbols() def assert_sample(self, name, skip_run=False): From a6d57cebecaa148a8076521746d7850fc6c872b9 Mon Sep 17 00:00:00 2001 From: James Westman Date: Mon, 9 Dec 2024 20:29:08 -0600 Subject: [PATCH 21/41] language: Add not-swapped flag for signals This is needed because GtkBuilder defaults to swapped when you specify the object attribute. --- blueprintcompiler/language/gobject_signal.py | 41 ++++++++++++++++--- blueprintcompiler/outputs/xml/__init__.py | 2 +- docs/reference/objects.rst | 5 ++- .../sample_errors/signal_exclusive_flags.blp | 5 +++ .../sample_errors/signal_exclusive_flags.err | 1 + .../signal_unnecessary_flags.blp | 6 +++ .../signal_unnecessary_flags.err | 2 + tests/samples/signal_not_swapped.blp | 5 +++ tests/samples/signal_not_swapped.ui | 12 ++++++ tests/test_samples.py | 1 + 10 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 tests/sample_errors/signal_exclusive_flags.blp create mode 100644 tests/sample_errors/signal_exclusive_flags.err create mode 100644 tests/sample_errors/signal_unnecessary_flags.blp create mode 100644 tests/sample_errors/signal_unnecessary_flags.err create mode 100644 tests/samples/signal_not_swapped.blp create mode 100644 tests/samples/signal_not_swapped.ui diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 0e0332e..b052e3c 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -27,6 +27,7 @@ from .gtkbuilder_template import Template class SignalFlag(AstNode): grammar = AnyOf( UseExact("flag", "swapped"), + UseExact("flag", "not-swapped"), UseExact("flag", "after"), ) @@ -40,6 +41,27 @@ class SignalFlag(AstNode): f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag ) + @validate() + def swapped_exclusive(self): + if self.flag in ["swapped", "not-swapped"]: + self.validate_unique_in_parent( + "'swapped' and 'not-swapped' flags cannot be used together", + lambda x: x.flag in ["swapped", "not-swapped"], + ) + + @validate() + def swapped_unnecessary(self): + if self.flag == "not-swapped" and self.parent.object_id is None: + raise CompileWarning( + "'not-swapped' is the default for handlers that do not specify an object", + actions=[CodeAction("Remove 'not-swapped' flag", "")], + ) + elif self.flag == "swapped" and self.parent.object_id is not None: + raise CompileWarning( + "'swapped' is the default for handlers that specify an object", + actions=[CodeAction("Remove 'swapped' flag", "")], + ) + @docs() def ref_docs(self): return get_docs_section("Syntax Signal") @@ -92,9 +114,17 @@ class Signal(AstNode): def flags(self) -> T.List[SignalFlag]: return self.children[SignalFlag] + # Returns True if the "swapped" flag is present, False if "not-swapped" is present, and None if neither are present. + # GtkBuilder's default if swapped is not specified is to not swap the arguments if no object is specified, and to + # swap them if an object is specified. @property - def is_swapped(self) -> bool: - return any(x.flag == "swapped" for x in self.flags) + def is_swapped(self) -> T.Optional[bool]: + for flag in self.flags: + if flag.flag == "swapped": + return True + elif flag.flag == "not-swapped": + return False + return None @property def is_after(self) -> bool: @@ -194,15 +224,16 @@ class Signal(AstNode): @decompiler("signal") -def decompile_signal( - ctx, gir, name, handler, swapped="false", after="false", object=None -): +def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None): object_name = object or "" name = name.replace("_", "-") line = f"{name} => ${handler}({object_name})" if decompile.truthy(swapped): line += " swapped" + elif swapped is not None: + line += " not-swapped" + if decompile.truthy(after): line += " after" diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 420f6ef..a21b6fb 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -169,7 +169,7 @@ class XmlOutput(OutputFormat): "signal", name=name, handler=signal.handler, - swapped=signal.is_swapped or None, + swapped=signal.is_swapped, after=signal.is_after or None, object=( self._object_id(signal, signal.object_id) if signal.object_id else None diff --git a/docs/reference/objects.rst b/docs/reference/objects.rst index 699db49..09f5af8 100644 --- a/docs/reference/objects.rst +++ b/docs/reference/objects.rst @@ -91,7 +91,7 @@ Signal Handlers .. rst-class:: grammar-block Signal = `> ('::' `>)? '=>' '$' `> '(' `>? ')' (SignalFlag)* ';' - SignalFlag = 'after' | 'swapped' + SignalFlag = 'after' | 'swapped' | 'not-swapped' Signals are one way to respond to user input (another is `actions `_, which use the `action-name property `_). @@ -99,6 +99,8 @@ Signals provide a handle for your code to listen to events in the UI. The handle Optionally, you can provide an object ID to use when connecting the signal. +The ``swapped`` flag is used to swap the order of the object and userdata arguments in C applications. If an object argument is specified, then this is the default behavior, so the ``not-swapped`` flag can be used to prevent the swap. + Example ~~~~~~~ @@ -108,7 +110,6 @@ Example clicked => $on_button_clicked(); } - .. _Syntax Child: Children diff --git a/tests/sample_errors/signal_exclusive_flags.blp b/tests/sample_errors/signal_exclusive_flags.blp new file mode 100644 index 0000000..6432965 --- /dev/null +++ b/tests/sample_errors/signal_exclusive_flags.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +$MyObject obj { + signal1 => $handler() swapped not-swapped; +} diff --git a/tests/sample_errors/signal_exclusive_flags.err b/tests/sample_errors/signal_exclusive_flags.err new file mode 100644 index 0000000..5176510 --- /dev/null +++ b/tests/sample_errors/signal_exclusive_flags.err @@ -0,0 +1 @@ +4,33,11,'swapped' and 'not-swapped' flags cannot be used together \ No newline at end of file diff --git a/tests/sample_errors/signal_unnecessary_flags.blp b/tests/sample_errors/signal_unnecessary_flags.blp new file mode 100644 index 0000000..ed95a9b --- /dev/null +++ b/tests/sample_errors/signal_unnecessary_flags.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; + +$MyObject obj { + signal1 => $handler() not-swapped; + signal2 => $handler(obj) swapped; +} diff --git a/tests/sample_errors/signal_unnecessary_flags.err b/tests/sample_errors/signal_unnecessary_flags.err new file mode 100644 index 0000000..7586085 --- /dev/null +++ b/tests/sample_errors/signal_unnecessary_flags.err @@ -0,0 +1,2 @@ +4,25,11,'not-swapped' is the default for handlers that do not specify an object +5,28,7,'swapped' is the default for handlers that specify an object \ No newline at end of file diff --git a/tests/samples/signal_not_swapped.blp b/tests/samples/signal_not_swapped.blp new file mode 100644 index 0000000..835ab17 --- /dev/null +++ b/tests/samples/signal_not_swapped.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Button obj { + clicked => $handler(obj) not-swapped; +} \ No newline at end of file diff --git a/tests/samples/signal_not_swapped.ui b/tests/samples/signal_not_swapped.ui new file mode 100644 index 0000000..c9dcd8e --- /dev/null +++ b/tests/samples/signal_not_swapped.ui @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 5eb1538..866488e 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -200,6 +200,7 @@ class TestSamples(unittest.TestCase): "expr_closure_args", "parseable", "signal", + "signal_not_swapped", "template", "template_binding", "template_binding_extern", From d6f4b88d35eb6a897eb40741a92f639cecbd0acc Mon Sep 17 00:00:00 2001 From: James Westman Date: Wed, 25 Dec 2024 10:31:35 -0600 Subject: [PATCH 22/41] lsp: Fix crash on incomplete detailed signal --- blueprintcompiler/language/gobject_signal.py | 3 ++- tests/sample_errors/incomplete_signal.blp | 5 +++++ tests/sample_errors/incomplete_signal.err | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 tests/sample_errors/incomplete_signal.blp create mode 100644 tests/sample_errors/incomplete_signal.err diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index b052e3c..9c27b97 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -143,12 +143,13 @@ class Signal(AstNode): @property def document_symbol(self) -> DocumentSymbol: + detail = self.ranges["detail_start", "detail_end"] return DocumentSymbol( self.full_name, SymbolKind.Event, self.range, self.group.tokens["name"].range, - self.ranges["detail_start", "detail_end"].text, + detail.text if detail is not None else None, ) def get_reference(self, idx: int) -> T.Optional[LocationLink]: diff --git a/tests/sample_errors/incomplete_signal.blp b/tests/sample_errors/incomplete_signal.blp new file mode 100644 index 0000000..4ec693d --- /dev/null +++ b/tests/sample_errors/incomplete_signal.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + notify:: +} diff --git a/tests/sample_errors/incomplete_signal.err b/tests/sample_errors/incomplete_signal.err new file mode 100644 index 0000000..901ef3b --- /dev/null +++ b/tests/sample_errors/incomplete_signal.err @@ -0,0 +1,2 @@ +5,1,0,Expected a signal detail name +4,9,3,Unexpected tokens \ No newline at end of file From f3faf4b993c73de623eff17abf7e28cfa9664f6a Mon Sep 17 00:00:00 2001 From: Alexey Yerin Date: Fri, 3 Jan 2025 22:31:49 +0300 Subject: [PATCH 23/41] LSP: Handle shutdown commands This fixes the issue with terminal-based editor Helix which asks language servers to shut down when trying to close the editor. Since blueprint-compiler's server implementation didn't handle this request, Helix ended up waiting for a response until timing out after a few seconds and forcefully terminating the language server process. Besides fixing Helix, this patch should also make user-initiated server restarts more robust. --- blueprintcompiler/lsp.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 0659154..c4076b4 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -118,6 +118,7 @@ class LanguageServer: self.client_capabilities = {} self.client_supports_completion_choice = False self._open_files: T.Dict[str, OpenFile] = {} + self._exited = False def run(self): # Read tags from gir files. During normal compilation these are @@ -125,7 +126,7 @@ class LanguageServer: xml_reader.PARSE_GIR.add("doc") try: - while True: + while not self._exited: line = "" content_len = -1 while content_len == -1 or (line != "\n" and line != "\r\n"): @@ -221,6 +222,14 @@ class LanguageServer: }, ) + @command("shutdown") + def shutdown(self, id, params): + self._send_response(id, None) + + @command("exit") + def exit(self, id, params): + self._exited = True + @command("textDocument/didOpen") def didOpen(self, id, params): doc = params.get("textDocument") From 55e5095fbafa8096b3d8cb9624017467465cea9c Mon Sep 17 00:00:00 2001 From: James Westman Date: Fri, 3 Jan 2025 18:56:24 -0600 Subject: [PATCH 24/41] values: Don't allow translated strings in arrays Gtk.Builder has no way to translate individual strings in a string array, so don't allow it in the syntax. --- blueprintcompiler/language/values.py | 8 ++++++++ tests/sample_errors/translated_string_array.blp | 7 +++++++ tests/sample_errors/translated_string_array.err | 1 + 3 files changed, 16 insertions(+) create mode 100644 tests/sample_errors/translated_string_array.blp create mode 100644 tests/sample_errors/translated_string_array.err diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 63cf4fc..5f47e54 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -452,6 +452,14 @@ class ArrayValue(AstNode): range=quoted_literal.range, ) ) + elif isinstance(value.child, Translated): + errors.append( + CompileError( + "Arrays can't contain translated strings", + range=value.child.range, + ) + ) + if len(errors) > 0: raise MultipleErrors(errors) diff --git a/tests/sample_errors/translated_string_array.blp b/tests/sample_errors/translated_string_array.blp new file mode 100644 index 0000000..451d900 --- /dev/null +++ b/tests/sample_errors/translated_string_array.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +StringList { + strings: [ + _("Test") + ]; +} diff --git a/tests/sample_errors/translated_string_array.err b/tests/sample_errors/translated_string_array.err new file mode 100644 index 0000000..0beb7e5 --- /dev/null +++ b/tests/sample_errors/translated_string_array.err @@ -0,0 +1 @@ +5,5,9,Arrays can't contain translated strings \ No newline at end of file From b9f58aeab58e83f8285b310b9e329ca2f7a06b32 Mon Sep 17 00:00:00 2001 From: Alexey Yerin Date: Sat, 4 Jan 2025 10:20:54 +0300 Subject: [PATCH 25/41] Formatter: Add trailing commas in lists --- blueprintcompiler/formatter.py | 4 +++- tests/formatting/lists_in.blp | 21 +++++++++++++++++++++ tests/formatting/lists_out.blp | 31 +++++++++++++++++++++++++++++++ tests/formatting/out.blp | 2 +- tests/samples/string_array.blp | 2 +- tests/samples/style.blp | 2 +- tests/test_formatter.py | 1 + 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/formatting/lists_in.blp create mode 100644 tests/formatting/lists_out.blp diff --git a/blueprintcompiler/formatter.py b/blueprintcompiler/formatter.py index 35da5d2..60d87b4 100644 --- a/blueprintcompiler/formatter.py +++ b/blueprintcompiler/formatter.py @@ -146,8 +146,10 @@ def format(data, tab_size=2, insert_space=True): is_child_type = False elif str_item in CLOSING_TOKENS: - if str_item == "]" and last_not_whitespace != ",": + if str_item == "]" and str(last_not_whitespace) != "[": current_line = current_line[:-1] + if str(last_not_whitespace) != ",": + current_line += "," commit_current_line() current_line = "]" elif str(last_not_whitespace) in OPENING_TOKENS: diff --git a/tests/formatting/lists_in.blp b/tests/formatting/lists_in.blp new file mode 100644 index 0000000..66b37a2 --- /dev/null +++ b/tests/formatting/lists_in.blp @@ -0,0 +1,21 @@ +using Gtk 4.0; + +Box { + styles [] +} + +Box { + styles ["a"] +} + +Box { + styles ["a",] +} + +Box { + styles ["a", "b"] +} + +Box { + styles ["a", "b",] +} diff --git a/tests/formatting/lists_out.blp b/tests/formatting/lists_out.blp new file mode 100644 index 0000000..7f1fe4a --- /dev/null +++ b/tests/formatting/lists_out.blp @@ -0,0 +1,31 @@ +using Gtk 4.0; + +Box { + styles [] +} + +Box { + styles [ + "a", + ] +} + +Box { + styles [ + "a", + ] +} + +Box { + styles [ + "a", + "b", + ] +} + +Box { + styles [ + "a", + "b", + ] +} diff --git a/tests/formatting/out.blp b/tests/formatting/out.blp index 9d9a8b4..b84c25f 100644 --- a/tests/formatting/out.blp +++ b/tests/formatting/out.blp @@ -11,7 +11,7 @@ Overlay { notify::icon-name => $on_icon_name_changed(label) swapped; styles [ - "destructive" + "destructive", ] } diff --git a/tests/samples/string_array.blp b/tests/samples/string_array.blp index 2542d17..85a68fe 100644 --- a/tests/samples/string_array.blp +++ b/tests/samples/string_array.blp @@ -5,6 +5,6 @@ AboutDialog about { authors: [ "Jane doe ", - "Jhonny D " + "Jhonny D ", ]; } diff --git a/tests/samples/style.blp b/tests/samples/style.blp index fe20f52..63720de 100644 --- a/tests/samples/style.blp +++ b/tests/samples/style.blp @@ -3,6 +3,6 @@ using Gtk 4.0; Label { styles [ "class-1", - "class-2" + "class-2", ] } diff --git a/tests/test_formatter.py b/tests/test_formatter.py index af4f9f5..a2cb60f 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -47,3 +47,4 @@ class TestFormatter(unittest.TestCase): self.assert_format_test("correct1.blp", "correct1.blp") self.assert_format_test("string_in.blp", "string_out.blp") self.assert_format_test("comment_in.blp", "comment_out.blp") + self.assert_format_test("lists_in.blp", "lists_out.blp") From 8c6f8760f7bb28132589aaa79207318fe538a19a Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 24 Dec 2024 12:54:23 -0600 Subject: [PATCH 26/41] language: Add expression literals Add expression literals, so you can set properties of type Gtk.Expression. --- blueprintcompiler/completions.py | 9 +++++ blueprintcompiler/language/__init__.py | 1 + blueprintcompiler/language/contexts.py | 6 +++ blueprintcompiler/language/expression.py | 30 ++++++++++++++ blueprintcompiler/language/gobject_object.py | 13 ++++++- .../language/gobject_property.py | 7 ++-- blueprintcompiler/language/values.py | 39 ++++++++++++++++++- blueprintcompiler/outputs/xml/__init__.py | 14 ++++--- docs/reference/expressions.rst | 39 +++++++++++++++---- docs/reference/objects.rst | 2 +- tests/sample_errors/expr_item_not_cast.blp | 5 +++ tests/sample_errors/expr_item_not_cast.err | 1 + tests/sample_errors/expr_value_assignment.blp | 5 +++ tests/sample_errors/expr_value_assignment.err | 1 + .../sample_errors/expr_value_closure_arg.blp | 5 +++ .../sample_errors/expr_value_closure_arg.err | 1 + tests/sample_errors/expr_value_item.blp | 5 +++ tests/sample_errors/expr_value_item.err | 1 + tests/samples/bind_expr_prop.blp | 9 +++++ tests/samples/bind_expr_prop.ui | 17 ++++++++ tests/samples/bind_literal.blp | 5 +++ tests/samples/bind_literal.ui | 14 +++++++ tests/samples/expr_value.blp | 5 +++ tests/samples/expr_value.ui | 14 +++++++ tests/samples/expr_value_closure.blp | 5 +++ tests/samples/expr_value_closure.ui | 16 ++++++++ tests/samples/expr_value_literal.blp | 5 +++ tests/samples/expr_value_literal.ui | 14 +++++++ tests/test_samples.py | 1 + 29 files changed, 268 insertions(+), 21 deletions(-) create mode 100644 tests/sample_errors/expr_item_not_cast.blp create mode 100644 tests/sample_errors/expr_item_not_cast.err create mode 100644 tests/sample_errors/expr_value_assignment.blp create mode 100644 tests/sample_errors/expr_value_assignment.err create mode 100644 tests/sample_errors/expr_value_closure_arg.blp create mode 100644 tests/sample_errors/expr_value_closure_arg.err create mode 100644 tests/sample_errors/expr_value_item.blp create mode 100644 tests/sample_errors/expr_value_item.err create mode 100644 tests/samples/bind_expr_prop.blp create mode 100644 tests/samples/bind_expr_prop.ui create mode 100644 tests/samples/bind_literal.blp create mode 100644 tests/samples/bind_literal.ui create mode 100644 tests/samples/expr_value.blp create mode 100644 tests/samples/expr_value.ui create mode 100644 tests/samples/expr_value_closure.blp create mode 100644 tests/samples/expr_value_closure.ui create mode 100644 tests/samples/expr_value_literal.blp create mode 100644 tests/samples/expr_value_literal.ui diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 5d36739..b10ec3e 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -177,6 +177,15 @@ def property_completer(lsp, ast_node, match_variables): docs=prop.doc, detail=prop.detail, ) + 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, + ) else: yield Completion( prop_name, diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index e797eaa..5eb2b60 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -41,6 +41,7 @@ from .types import ClassName from .ui import UI from .values import ( ArrayValue, + ExprValue, Flag, Flags, IdentLiteral, diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index c5e97b3..6e26048 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -79,3 +79,9 @@ class ScopeCtx: for child in node.children: if child.context[ScopeCtx] is self: yield from self._iter_recursive(child) + + +@dataclass +class ExprValueCtx: + """Indicates that the context is an expression literal, where the + "item" keyword may be used.""" diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index f305035..e0b4246 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -81,6 +81,16 @@ class LiteralExpr(ExprBase): or self.root.is_legacy_template(self.literal.value.ident) ) + @property + def is_this(self) -> bool: + from .values import IdentLiteral + + return ( + not self.is_object + and isinstance(self.literal.value, IdentLiteral) + and self.literal.value.ident == "item" + ) + @property def literal(self): from .values import Literal @@ -91,6 +101,15 @@ class LiteralExpr(ExprBase): def type(self) -> T.Optional[GirType]: return self.literal.value.type + @validate() + def item_validations(self): + if self.is_this: + if not isinstance(self.rhs, CastExpr): + raise CompileError('"item" must be cast to its object type') + + if not isinstance(self.rhs.rhs, LookupOp): + raise CompileError('"item" can only be used for looking up properties') + class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] @@ -285,6 +304,9 @@ expr.children = [ def decompile_lookup( ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str ): + if ctx.parent_node is not None and ctx.parent_node.tag == "property": + ctx.print("expr ") + if t := ctx.type_by_cname(type): type = decompile.full_name(t) else: @@ -304,6 +326,8 @@ def decompile_lookup( if constant is not None: if constant == ctx.template_class: ctx.print("template." + name) + elif constant == "": + ctx.print("item as <" + type + ">." + name) else: ctx.print(constant + "." + name) return @@ -318,6 +342,9 @@ def decompile_lookup( def decompile_constant( ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None ): + if ctx.parent_node is not None and ctx.parent_node.tag == "property": + ctx.print("expr ") + if type is None: if cdata == ctx.template_class: ctx.print("template") @@ -330,6 +357,9 @@ def decompile_constant( @decompiler("closure", skip_children=True) def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str): + if ctx.parent_node is not None and ctx.parent_node.tag == "property": + ctx.print("expr ") + if t := ctx.type_by_cname(type): type = decompile.full_name(t) else: diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 54cb297..1def15b 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -28,7 +28,18 @@ from .common import * from .response_id import ExtResponse from .types import ClassName, ConcreteClassName -RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"} +RESERVED_IDS = { + "this", + "self", + "template", + "true", + "false", + "null", + "none", + "item", + "expr", + "typeof", +} class ObjectContent(AstNode): diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index b553909..50a7512 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -21,13 +21,12 @@ from .binding import Binding from .common import * from .contexts import ValueTypeCtx -from .gtkbuilder_template import Template -from .values import ArrayValue, ObjectValue, Value +from .values import ArrayValue, ExprValue, ObjectValue, Value class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value, ArrayValue) + UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) ) @property @@ -35,7 +34,7 @@ class Property(AstNode): return self.tokens["name"] @property - def value(self) -> T.Union[Binding, ObjectValue, Value, ArrayValue]: + def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]: return self.children[0] @property diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 5f47e54..5556d99 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -23,7 +23,8 @@ from blueprintcompiler.gir import ArrayType from blueprintcompiler.lsp_utils import SemanticToken from .common import * -from .contexts import ScopeCtx, ValueTypeCtx +from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx +from .expression import Expression from .gobject_object import Object from .types import TypeName @@ -319,7 +320,12 @@ class IdentLiteral(AstNode): if self.ident == "null": if not self.context[ValueTypeCtx].allow_null: raise CompileError("null is not permitted here") - else: + elif self.ident == "item": + if not self.context[ExprValueCtx]: + raise CompileError( + '"item" can only be used in an expression literal' + ) + elif self.ident not in ["true", "false"]: raise CompileError( f"Could not find object with ID {self.ident}", did_you_mean=( @@ -407,6 +413,35 @@ class ObjectValue(AstNode): ) +class ExprValue(AstNode): + grammar = [Keyword("expr"), Expression] + + @property + def expression(self) -> Expression: + return self.children[Expression][0] + + @validate("expr") + def validate_for_type(self) -> None: + expected_type = self.parent.context[ValueTypeCtx].value_type + expr_type = self.root.gir.get_type("Expression", "Gtk") + if expected_type is not None and not expected_type.assignable_to(expr_type): + raise CompileError( + f"Cannot convert Gtk.Expression to {expected_type.full_name}" + ) + + @docs("expr") + def ref_docs(self): + return get_docs_section("Syntax ExprValue") + + @context(ExprValueCtx) + def expr_literal(self): + return ExprValueCtx() + + @context(ValueTypeCtx) + def value_type(self): + return ValueTypeCtx(None, must_infer_type=True) + + class Value(AstNode): grammar = AnyOf(Translated, Flags, Literal) diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index a21b6fb..5c03761 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -134,6 +134,11 @@ class XmlOutput(OutputFormat): self._emit_expression(value.expression, xml) xml.end_tag() + elif isinstance(value, ExprValue): + xml.start_tag("property", **props) + self._emit_expression(value.expression, xml) + xml.end_tag() + elif isinstance(value, ObjectValue): xml.start_tag("property", **props) self._emit_object(value.object, xml) @@ -218,12 +223,6 @@ class XmlOutput(OutputFormat): xml.put_text( "|".join([str(flag.value or flag.name) for flag in value.child.flags]) ) - elif isinstance(value.child, Translated): - raise CompilerBugError("translated values must be handled in the parent") - elif isinstance(value.child, TypeLiteral): - xml.put_text(value.child.type_name.glib_type_name) - elif isinstance(value.child, ObjectValue): - self._emit_object(value.child.object, xml) else: raise CompilerBugError() @@ -245,6 +244,9 @@ class XmlOutput(OutputFormat): raise CompilerBugError() def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): + if expr.is_this: + return + if expr.is_object: xml.start_tag("constant") else: diff --git a/docs/reference/expressions.rst b/docs/reference/expressions.rst index 8688ff0..3d523d1 100644 --- a/docs/reference/expressions.rst +++ b/docs/reference/expressions.rst @@ -42,8 +42,8 @@ Expressions are composed of property lookups and/or closures. Property lookups a .. _Syntax LookupExpression: -Lookup Expressions ------------------- +Lookups +------- .. rst-class:: grammar-block @@ -56,8 +56,8 @@ The type of a property expression is the type of the property it refers to. .. _Syntax ClosureExpression: -Closure Expressions -------------------- +Closures +-------- .. rst-class:: grammar-block @@ -72,8 +72,8 @@ Blueprint doesn't know the closure's return type, so closure expressions must be .. _Syntax CastExpression: -Cast Expressions ----------------- +Casts +----- .. rst-class:: grammar-block @@ -81,7 +81,32 @@ Cast Expressions Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types. +Example +~~~~~~~ + .. code-block:: blueprint // Cast the result of the closure so blueprint knows it's a string - label: bind $my_closure() as \ No newline at end of file + label: bind $format_bytes(template.file-size) as + +.. _Syntax ExprValue: + +Expression Values +----------------- + +.. rst-class:: grammar-block + + ExprValue = 'expr' :ref:`Expression` + +Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter `_ has an ``expression`` property of type `Gtk.Expression `_. This expression is evaluated for every item in a list model to determine whether the item should be filtered. + +To define an expression for such a property, use ``expr`` instead of ``bind``. Inside the expression, you can use the ``item`` keyword to refer to the item being evaluated. You must cast the item to the correct type using the ``as`` keyword, and you can only use ``item`` in a property lookup--you may not pass it to a closure. + +Example +~~~~~~~ + +.. code-block:: blueprint + + BoolFilter { + expression: expr item as <$UserAccount>.active; + } diff --git a/docs/reference/objects.rst b/docs/reference/objects.rst index 09f5af8..6f76da6 100644 --- a/docs/reference/objects.rst +++ b/docs/reference/objects.rst @@ -58,7 +58,7 @@ Properties .. rst-class:: grammar-block - Property = `> ':' ( :ref:`Binding` | :ref:`ObjectValue` | :ref:`Value` ) ';' + Property = `> ':' ( :ref:`Binding` | :ref:`ExprValue` | :ref:`ObjectValue` | :ref:`Value` ) ';' Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container. diff --git a/tests/sample_errors/expr_item_not_cast.blp b/tests/sample_errors/expr_item_not_cast.blp new file mode 100644 index 0000000..76a1d89 --- /dev/null +++ b/tests/sample_errors/expr_item_not_cast.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +BoolFilter { + expression: expr item.visible; +} diff --git a/tests/sample_errors/expr_item_not_cast.err b/tests/sample_errors/expr_item_not_cast.err new file mode 100644 index 0000000..f6cf7d4 --- /dev/null +++ b/tests/sample_errors/expr_item_not_cast.err @@ -0,0 +1 @@ +4,20,4,"item" must be cast to its object type \ No newline at end of file diff --git a/tests/sample_errors/expr_value_assignment.blp b/tests/sample_errors/expr_value_assignment.blp new file mode 100644 index 0000000..51d778f --- /dev/null +++ b/tests/sample_errors/expr_value_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: expr 1; +} diff --git a/tests/sample_errors/expr_value_assignment.err b/tests/sample_errors/expr_value_assignment.err new file mode 100644 index 0000000..1c7092a --- /dev/null +++ b/tests/sample_errors/expr_value_assignment.err @@ -0,0 +1 @@ +4,10,4,Cannot convert Gtk.Expression to string \ No newline at end of file diff --git a/tests/sample_errors/expr_value_closure_arg.blp b/tests/sample_errors/expr_value_closure_arg.blp new file mode 100644 index 0000000..7f828c4 --- /dev/null +++ b/tests/sample_errors/expr_value_closure_arg.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +BoolFilter { + expression: expr $closure(item as ) as ; +} diff --git a/tests/sample_errors/expr_value_closure_arg.err b/tests/sample_errors/expr_value_closure_arg.err new file mode 100644 index 0000000..b9e19f8 --- /dev/null +++ b/tests/sample_errors/expr_value_closure_arg.err @@ -0,0 +1 @@ +4,29,4,"item" can only be used for looking up properties \ No newline at end of file diff --git a/tests/sample_errors/expr_value_item.blp b/tests/sample_errors/expr_value_item.blp new file mode 100644 index 0000000..141c806 --- /dev/null +++ b/tests/sample_errors/expr_value_item.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +BoolFilter { + expression: expr item as