diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index e030961..8d005ca 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -137,10 +137,10 @@ def prop_value_completer(ast_node, match_variables): def signal_completer(ast_node, match_variables): if ast_node.gir_class: for signal in ast_node.gir_class.signals: - if not isinstance(ast_node.parent, language.Object): - name = "on" + if isinstance(ast_node.parent, language.Object): + name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.children[language.types.ClassName][0].tokens["class_name"].lower()) else: - name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower()) + name = "on" yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 6234077..022b69f 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -487,7 +487,7 @@ class GirContext: ) - def validate_class(self, name: str, ns: str): + def validate_type(self, name: str, ns: str): """ Raises an exception if there is a problem looking up the given class (it doesn't exist, it isn't a class, etc.) """ @@ -498,12 +498,7 @@ class GirContext: if type is None: raise CompileError( - f"Namespace {ns} does not contain a class called {name}", - did_you_mean=(name, self.namespaces[ns].classes.keys()), - ) - elif not isinstance(type, Class): - raise CompileError( - f"{ns}.{name} is not a class", + f"Namespace {ns} does not contain a type called {name}", did_you_mean=(name, self.namespaces[ns].classes.keys()), ) diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index feeb301..3f87faf 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -16,6 +16,7 @@ from .gtk_string_list import Strings from .gtk_styles import Styles from .gtkbuilder_child import Child from .gtkbuilder_template import Template +from .lambdas import Lambda from .imports import GtkDirective, Import from .ui import UI from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue @@ -47,4 +48,5 @@ VALUE_HOOKS.children = [ FlagsValue, IdentValue, LiteralValue, + Lambda, ] diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 03cb428..489f7ef 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -17,7 +17,6 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later - from .. import gir from ..ast_utils import AstNode, validate, docs from ..errors import CompileError, MultipleErrors @@ -27,10 +26,29 @@ from ..decompiler import DecompileCtx, decompiler from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * -from ..parser_utils import * from ..xml_emitter import XmlEmitter OBJECT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf() VALUE_HOOKS = AnyOf() + + +class Scope: + def get_variables(self) -> T.Iterator[str]: + yield from self.get_objects().keys() + + def get_objects(self) -> T.Dict[str, T.Any]: + raise NotImplementedError() + + @property + def this_name(self) -> T.Optional[str]: + return None + + @property + def this_type(self) -> T.Optional[str]: + return None + + @property + def this_type_glib_name(self) -> T.Optional[str]: + return None diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index a684b39..b51068e 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -41,7 +41,14 @@ class InfixExpr(AstNode): class IdentExpr(AstNode): grammar = UseIdent("ident") + @property + def is_this(self): + return self.parent_by_type(Scope).this_name == self.tokens["ident"] + def emit_xml(self, xml: XmlEmitter): + if self.is_this: + raise CompilerBugError() + xml.start_tag("constant") xml.put_text(self.tokens["ident"]) xml.end_tag() @@ -51,9 +58,13 @@ class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] def emit_xml(self, xml: XmlEmitter): - xml.start_tag("lookup", name=self.tokens["property"]) - self.lhs.emit_xml(xml) - xml.end_tag() + if isinstance(self.lhs, IdentExpr) and self.lhs.is_this: + xml.put_self_closing("lookup", name=self.tokens["property"], type=self.parent_by_type(Scope).this_type) + else: + xml.start_tag("lookup", name=self.tokens["property"]) + self.lhs.emit_xml(xml) + xml.end_tag() + expr.children = [ diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index e23e0d2..45c3f40 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -21,6 +21,7 @@ import typing as T from functools import cached_property +from .types import ConcreteClassName, ClassName from .common import * from .response_id import ResponseId @@ -46,29 +47,11 @@ class ObjectContent(AstNode): class Object(AstNode): grammar: T.Any = [ - class_name, + ConcreteClassName, Optional(UseIdent("id")), ObjectContent, ] - @validate("namespace") - def gir_ns_exists(self): - if not self.tokens["ignore_gir"]: - self.root.gir.validate_ns(self.tokens["namespace"]) - - @validate("class_name") - def gir_class_exists(self): - if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None: - self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"]) - - @validate("namespace", "class_name") - def not_abstract(self): - if self.gir_class is not None and self.gir_class.abstract: - raise CompileError( - f"{self.gir_class.full_name} can't be instantiated because it's abstract", - hints=[f"did you mean to use a subclass of {self.gir_class.full_name}?"] - ) - @property def gir_ns(self): if not self.tokens["ignore_gir"]: @@ -76,9 +59,11 @@ class Object(AstNode): @property def gir_class(self): - if self.tokens["class_name"] and not self.tokens["ignore_gir"]: - return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) - + class_names = self.children[ClassName] + if len(class_names) == 0: + return None + else: + return class_names[0].gir_type @docs("namespace") def namespace_docs(self): @@ -109,7 +94,7 @@ class Object(AstNode): from .gtkbuilder_child import Child xml.start_tag("object", **{ - "class": self.gir_class or self.tokens["class_name"], + "class": self.children[ClassName][0].glib_type_name, "id": self.tokens["id"], }) for child in self.children: diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 92dff71..cb0f689 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .types import ClassName from .gobject_object import Object, ObjectContent from .common import * @@ -27,8 +28,8 @@ class Template(Object): "template", UseIdent("name").expected("template class name"), Optional([ - Match(":"), - class_name.expected("parent class"), + ":", + to_parse_node(ClassName).expected("parent class"), ]), ObjectContent, ] @@ -38,10 +39,14 @@ class Template(Object): pass # does not apply to templates def emit_xml(self, xml: XmlEmitter): + parent = None + if len(self.children[ClassName]): + parent = self.children[ClassName][0].glib_type_name + xml.start_tag( "template", **{"class": self.tokens["name"]}, - parent=self.gir_class or self.tokens["class_name"] + parent=parent, ) for child in self.children: child.emit_xml(xml) diff --git a/blueprintcompiler/language/lambdas.py b/blueprintcompiler/language/lambdas.py new file mode 100644 index 0000000..0eef4d3 --- /dev/null +++ b/blueprintcompiler/language/lambdas.py @@ -0,0 +1,53 @@ +# lambdas.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 .types import TypeName +from .expression import Expr +from .values import Value +from .common import * + + +class Lambda(Value, Scope): + grammar = [ + "(", + TypeName, + UseIdent("argument"), + ")", + "=>", + Expr, + ] + + def emit_xml(self, xml: XmlEmitter): + for child in self.children: + child.emit_xml(xml) + + def get_objects(self): + return { + **self.parent.parent_by_type(Scope).get_objects(), + self.tokens["argument"]: None, + } + + @property + def this_name(self) -> str: + return self.tokens["argument"] + + @property + def this_type(self) -> str: + return self.children[TypeName][0].gir_type diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py new file mode 100644 index 0000000..ad48c3e --- /dev/null +++ b/blueprintcompiler/language/types.py @@ -0,0 +1,96 @@ +# types.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 + + +import typing as T +from .common import * + + +class TypeName(AstNode): + grammar = AnyOf( + [ + UseIdent("namespace"), + ".", + UseIdent("class_name"), + ], + [ + ".", + UseIdent("class_name"), + UseLiteral("ignore_gir", True), + ], + UseIdent("class_name"), + ) + + @validate("class_name") + def type_exists(self): + if not self.tokens["ignore_gir"] and self.gir_ns is not None: + self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"]) + + @validate("namespace") + def gir_ns_exists(self): + if not self.tokens["ignore_gir"]: + self.root.gir.validate_ns(self.tokens["namespace"]) + + @property + def gir_ns(self): + if not self.tokens["ignore_gir"]: + return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") + + @property + def gir_type(self) -> T.Optional[gir.Class]: + if self.tokens["class_name"] and not self.tokens["ignore_gir"]: + return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"]) + return None + + @property + def glib_type_name(self) -> str: + if gir_type := self.gir_type: + return gir_type.glib_type_name + else: + return self.tokens["class_name"] + + @docs("namespace") + def namespace_docs(self): + if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): + return ns.doc + + @docs("class_name") + def class_docs(self): + if self.gir_type: + return self.gir_type.doc + + def emit_xml(self, xml: XmlEmitter): + pass + + +class ClassName(TypeName): + @validate("class_name") + def gir_type_is_class(self): + if self.gir_type is not None and not isinstance(self.gir_type, gir.Class): + raise CompileError(f"{self.gir_type.full_name} is not a class") + + +class ConcreteClassName(ClassName): + @validate("namespace", "class_name") + def not_abstract(self): + if self.gir_type is not None and self.gir_type.abstract: + raise CompileError( + f"{self.gir_type.full_name} can't be instantiated because it's abstract", + hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"] + ) diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index a41e585..ffcbc47 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -24,7 +24,7 @@ from .gtkbuilder_template import Template from .common import * -class UI(AstNode): +class UI(AstNode, Scope): """ The AST node for the entire file """ grammar = [ @@ -63,6 +63,10 @@ class UI(AstNode): return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } + def get_objects(self): + return self.objects_by_id + + @validate() def gir_errors(self): # make sure gir is loaded diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 6064481..7c62ae6 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -20,7 +20,6 @@ from .errors import MultipleErrors, PrintableError from .parse_tree import * -from .parser_utils import * from .tokenizer import TokenType from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI diff --git a/blueprintcompiler/parser_utils.py b/blueprintcompiler/parser_utils.py deleted file mode 100644 index af951fe..0000000 --- a/blueprintcompiler/parser_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -# parser_utils.py -# -# Copyright 2021 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 .parse_tree import * - - -class_name = AnyOf( - [ - UseIdent("namespace"), - ".", - UseIdent("class_name"), - ], - [ - ".", - UseIdent("class_name"), - UseLiteral("ignore_gir", True), - ], - UseIdent("class_name"), -) diff --git a/tests/sample_errors/class_dne.err b/tests/sample_errors/class_dne.err index 573baf3..57d74f8 100644 --- a/tests/sample_errors/class_dne.err +++ b/tests/sample_errors/class_dne.err @@ -1 +1 @@ -3,29,13,Namespace Gtk does not contain a class called NotARealClass +3,29,13,Namespace Gtk does not contain a type called NotARealClass diff --git a/tests/samples/lambda.blp b/tests/samples/lambda.blp new file mode 100644 index 0000000..1db7003 --- /dev/null +++ b/tests/samples/lambda.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; + +Gtk.BoolFilter filter { + expression: (Gtk.Label object) => object.visible; +} + +Gtk.BoolFilter { + expression: (Gtk.Label object) => filter.invert; +} diff --git a/tests/samples/lambda.ui b/tests/samples/lambda.ui new file mode 100644 index 0000000..177449b --- /dev/null +++ b/tests/samples/lambda.ui @@ -0,0 +1,16 @@ + + + + + + + + + + + + filter + + + + diff --git a/tests/samples/template_custom_parent.blp b/tests/samples/template_custom_parent.blp new file mode 100644 index 0000000..be5ce3d --- /dev/null +++ b/tests/samples/template_custom_parent.blp @@ -0,0 +1,3 @@ +using Gtk 4.0; + +template MyWidget : .MyBaseWidget {} diff --git a/tests/samples/template_custom_parent.ui b/tests/samples/template_custom_parent.ui new file mode 100644 index 0000000..914e1b1 --- /dev/null +++ b/tests/samples/template_custom_parent.ui @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 203b07f..d8a3abb 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -141,6 +141,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("flags") self.assert_sample("id_prop") self.assert_sample("inline_menu") + self.assert_sample("lambda") self.assert_sample("layout") self.assert_sample("menu") self.assert_sample("numbers") @@ -153,6 +154,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("strings") self.assert_sample("style") self.assert_sample("template") + self.assert_sample("template_custom_parent") self.assert_sample("template_no_parent") self.assert_sample("translated") self.assert_sample("uint")