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/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/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/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/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 63cf4fc..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) @@ -452,6 +487,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/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") 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/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/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