Compare commits

...

5 commits

Author SHA1 Message Date
James Westman
8c6f8760f7 language: Add expression literals
Add expression literals, so you can set properties of type
Gtk.Expression.
2025-01-04 17:09:57 +00:00
Alexey Yerin
b9f58aeab5 Formatter: Add trailing commas in lists 2025-01-04 16:29:15 +00:00
James Westman
55e5095fba
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.
2025-01-03 18:56:24 -06:00
Alexey Yerin
f3faf4b993 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.
2025-01-03 22:49:36 +03:00
James Westman
d6f4b88d35
lsp: Fix crash on incomplete detailed signal 2024-12-25 10:31:35 -06:00
42 changed files with 362 additions and 27 deletions

View file

@ -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,

View file

@ -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:

View file

@ -41,6 +41,7 @@ from .types import ClassName
from .ui import UI
from .values import (
ArrayValue,
ExprValue,
Flag,
Flags,
IdentLiteral,

View file

@ -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."""

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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]:

View file

@ -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)

View file

@ -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 <doc> 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")

View file

@ -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:

View file

@ -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 <string>
label: bind $format_bytes(template.file-size) as <string>
.. _Syntax ExprValue:
Expression Values
-----------------
.. rst-class:: grammar-block
ExprValue = 'expr' :ref:`Expression<Syntax Expression>`
Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter <https://docs.gtk.org/gtk4/class.BoolFilter.html>`_ has an ``expression`` property of type `Gtk.Expression <https://docs.gtk.org/gtk4/class.Expression.html>`_. 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;
}

View file

@ -58,7 +58,7 @@ Properties
.. rst-class:: grammar-block
Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`Binding<Syntax Binding>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';'
Property = <name::ref:`IDENT<Syntax IDENT>`> ':' ( :ref:`Binding<Syntax Binding>` | :ref:`ExprValue<Syntax ExprValue>` | :ref:`ObjectValue<Syntax ObjectValue>` | :ref:`Value<Syntax Value>` ) ';'
Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container.

View file

@ -0,0 +1,21 @@
using Gtk 4.0;
Box {
styles []
}
Box {
styles ["a"]
}
Box {
styles ["a",]
}
Box {
styles ["a", "b"]
}
Box {
styles ["a", "b",]
}

View file

@ -0,0 +1,31 @@
using Gtk 4.0;
Box {
styles []
}
Box {
styles [
"a",
]
}
Box {
styles [
"a",
]
}
Box {
styles [
"a",
"b",
]
}
Box {
styles [
"a",
"b",
]
}

View file

@ -11,7 +11,7 @@ Overlay {
notify::icon-name => $on_icon_name_changed(label) swapped;
styles [
"destructive"
"destructive",
]
}

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr item.visible;
}

View file

@ -0,0 +1 @@
4,20,4,"item" must be cast to its object type

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: expr 1;
}

View file

@ -0,0 +1 @@
4,10,4,Cannot convert Gtk.Expression to string

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr $closure(item as <Entry>) as <bool>;
}

View file

@ -0,0 +1 @@
4,29,4,"item" can only be used for looking up properties

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr item as <Label>;
}

View file

@ -0,0 +1 @@
4,20,4,"item" can only be used for looking up properties

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
notify::
}

View file

@ -0,0 +1,2 @@
5,1,0,Expected a signal detail name
4,9,3,Unexpected tokens

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
StringList {
strings: [
_("Test")
];
}

View file

@ -0,0 +1 @@
5,5,9,Arrays can't contain translated strings

View file

@ -0,0 +1,9 @@
using Gtk 4.0;
BoolFilter filter1 {
expression: expr true;
}
BoolFilter filter2 {
expression: bind filter1.expression;
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkBoolFilter" id="filter1">
<property name="expression">
<constant type="gboolean">true</constant>
</property>
</object>
<object class="GtkBoolFilter" id="filter2">
<property name="expression" bind-source="filter1" bind-property="expression" bind-flags="sync-create"/>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: bind "Hello, world!";
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkLabel">
<binding name="label">
<constant type="gchararray">Hello, world!</constant>
</binding>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr item as <Entry>.visible;
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkBoolFilter">
<property name="expression">
<lookup name="visible" type="GtkEntry"></lookup>
</property>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr $closure(item as <Entry>.visible) as <bool>;
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkBoolFilter">
<property name="expression">
<closure function="closure" type="gboolean">
<lookup name="visible" type="GtkEntry"></lookup>
</closure>
</property>
</object>
</interface>

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
BoolFilter {
expression: expr true;
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkBoolFilter">
<property name="expression">
<constant type="gboolean">true</constant>
</property>
</object>
</interface>

View file

@ -5,6 +5,6 @@ AboutDialog about {
authors: [
"Jane doe <jane-doe@email.com>",
"Jhonny D <jd@email.com>"
"Jhonny D <jd@email.com>",
];
}

View file

@ -3,6 +3,6 @@ using Gtk 4.0;
Label {
styles [
"class-1",
"class-2"
"class-2",
]
}

View file

@ -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")

View file

@ -198,6 +198,7 @@ class TestSamples(unittest.TestCase):
"adw_breakpoint_template",
"expr_closure",
"expr_closure_args",
"expr_value_closure",
"parseable",
"signal",
"signal_not_swapped",