diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1ec071e..6d373cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - pages build: - image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + image: registry.gitlab.gnome.org/gnome/blueprint-compiler stage: build script: - black --check --diff ./ tests @@ -33,7 +33,7 @@ build: path: coverage.xml fuzz: - image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + image: registry.gitlab.gnome.org/gnome/blueprint-compiler stage: build script: - meson _build diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 220c117..3ab4fa2 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -8,7 +8,7 @@ in the NEWS file. 3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. 4. Create a "Post-release version bump" commit. 5. Go to the Releases page in GitLab and create a new release from the tag. -6. Announce the release through relevant channels (Twitter, TWIG, etc.) +6. Announce the release through relevant channels (Mastodon, TWIG, etc.) ## Related projects diff --git a/NEWS.md b/NEWS.md index 389f82c..a12dab0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,35 @@ +# v0.16.0 + +## Added +- Added more "go to reference" implementations in the language server +- Added semantic token support for flag members in the language server +- Added property documentation to the hover tooltip for notify signals +- The language server now shows relevant sections of the reference documentation when hovering over keywords and symbols +- Added `not-swapped` flag to signal handlers, which may be needed for signal handlers that specify an object +- Added expression literals, which allow you to specify a Gtk.Expression property (as opposed to the existing expression support, which is for property bindings) + +## Changed +- The formatter adds trailing commas to lists (Alexey Yerin) +- The formatter removes trailing whitespace from comments (Alexey Yerin) +- Autocompleting a commonly translated property automatically adds the `_("")` syntax +- Marking a single-quoted string as translatable now generates a warning, since gettext does not recognize it when using the configuration recommended in the blueprint documentation + +## Fixed +- Added support for libgirepository-2.0 so that blueprint doesn't crash due to import conflicts on newer versions of PyGObject (Jordan Petridis) +- Fixed a bug when decompiling/porting files with enum values +- Fixed several issues where tests would fail with versions of GTK that added new deprecations +- Addressed a problem with the language server protocol in some editors (Luoyayu) +- Fixed an issue where the compiler would crash instead of reporting compiler errors +- Fixed a crash in the language server that occurred when a detailed signal (e.g. `notify::*`) was not complete +- The language server now properly implements the shutdown command, fixing support for some editors and improving robustness when restarting (Alexey Yerin) +- Marking a string in an array as translatable now generates an error, since it doesn't work +- + +## Documentation +- Added mention of `null` in the Literal Values section +- Add apps to Built with Blueprint section (Benedek Dévényi, Vladimir Vaskov) +- Corrected and updated many parts of the documentation + # v0.14.0 ## Added diff --git a/blueprint-compiler.doap b/blueprint-compiler.doap new file mode 100644 index 0000000..f3e4000 --- /dev/null +++ b/blueprint-compiler.doap @@ -0,0 +1,27 @@ + + + Blueprint + A modern language for creating GTK interfaces + Blueprint is a language and associated tooling for building user interfaces for GTK. + + Python + + + + + + + + James Westman + + jwestman + + + diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 8f742e0..b0c5357 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -196,6 +196,13 @@ class AstNode: return None + def get_child_at(self, idx: int) -> "AstNode": + for child in self.children: + if idx in child.range: + return child.get_child_at(idx) + + return self + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: for child in self.children: yield from child.get_semantic_tokens() diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b10ec3e..81dd03d 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -21,9 +21,20 @@ import typing as T from . import annotations, gir, language from .ast_utils import AstNode -from .completions_utils import * +from .completions_utils import ( + CompletionContext, + CompletionItemKind, + CompletionPriority, + completer, + completers, + get_object_id_completions, + get_property_completion, + get_sort_key, + new_statement_patterns, +) +from .language.contexts import ValueTypeCtx from .language.types import ClassName -from .lsp_utils import Completion, CompletionItemKind +from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS from .tokenizer import Token, TokenType @@ -31,15 +42,13 @@ Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] def _complete( - lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int + lsp, + ast_node: AstNode, + tokens: T.List[Token], + idx: int, + token_idx: int, + next_token: Token, ) -> T.Iterator[Completion]: - for child in ast_node.children: - if child.group.start <= idx and ( - idx < child.group.end or (idx == child.group.end and child.incomplete) - ): - yield from _complete(lsp, child, tokens, idx, token_idx) - return - prev_tokens: T.List[Token] = [] # collect the 5 previous non-skipped tokens @@ -49,8 +58,8 @@ def _complete( prev_tokens.insert(0, token) token_idx -= 1 - for completer in ast_node.completers: - yield from completer(prev_tokens, ast_node, lsp) + for completer in completers: + yield from completer(prev_tokens, next_token, ast_node, lsp, idx) def complete( @@ -62,147 +71,258 @@ def complete( if token.start < idx <= token.end: token_idx = i + if tokens[token_idx].type == TokenType.EOF: + next_token = tokens[token_idx] + else: + next_token_idx = token_idx + 1 + while tokens[next_token_idx].type == TokenType.WHITESPACE: + next_token_idx += 1 + next_token = tokens[next_token_idx] + # if the current token is an identifier or whitespace, move to the token before it - while tokens[token_idx].type in [TokenType.IDENT, TokenType.WHITESPACE]: + if tokens[token_idx].type == TokenType.IDENT: idx = tokens[token_idx].start token_idx -= 1 - yield from _complete(lsp, ast_node, tokens, idx, token_idx) + while tokens[token_idx].type == TokenType.WHITESPACE: + idx = tokens[token_idx].start + token_idx -= 1 + + child_node = ast_node.get_child_at(idx) + # If the cursor is at the end of a node, completions should be for the next child of the parent, unless the node + # is incomplete. + while ( + child_node.range.end == idx + and not child_node.incomplete + and child_node.parent is not None + ): + child_node = child_node.parent + + yield from _complete(lsp, child_node, tokens, idx, token_idx, next_token) @completer([language.GtkDirective]) -def using_gtk(lsp, ast_node, match_variables): +def using_gtk(_ctx: CompletionContext): yield Completion( "using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n" ) +@completer([language.UI]) +def using(ctx: CompletionContext): + imported_namespaces = set( + [import_.namespace for import_ in ctx.ast_node.root.using] + ) + + # Import statements must be before any content + for i in ctx.ast_node.root.children: + if not isinstance(i, language.GtkDirective) and not isinstance( + i, language.Import + ): + if ctx.index >= i.range.end: + return + + for ns, version in gir.get_available_namespaces(): + if ns not in imported_namespaces and ns != "Gtk": + yield Completion( + f"using {ns} {version}", + CompletionItemKind.Module, + text=f"using {ns} {version};", + sort_text=get_sort_key(CompletionPriority.NAMESPACE, ns), + ) + + +@completer([language.UI]) +def translation_domain(ctx: CompletionContext): + if ctx.ast_node.root.translation_domain is not None: + return + + # Translation domain must be after the import statements but before any content + for i in ctx.ast_node.root.children: + if isinstance(i, language.Import): + if ctx.index <= i.range.start: + return + elif not isinstance(i, language.GtkDirective): + if ctx.index >= i.range.end: + return + + yield Completion( + "translation-domain", + CompletionItemKind.Keyword, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "translation-domain"), + snippet='translation-domain "$0";', + docs=get_docs_section("Syntax TranslationDomain"), + ) + + +def _available_namespace_completions(ctx: CompletionContext): + imported_namespaces = set( + [import_.namespace for import_ in ctx.ast_node.root.using] + ) + + for ns, version in gir.get_available_namespaces(): + if ns not in imported_namespaces and ns != "Gtk": + yield Completion( + ns, + CompletionItemKind.Module, + text=ns + ".", + sort_text=get_sort_key(CompletionPriority.IMPORT_NAMESPACE, ns), + signature=f" using {ns} {version}", + additional_text_edits=[ + TextEdit( + ctx.ast_node.root.import_range(ns), f"\nusing {ns} {version};" + ) + ], + ) + + @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=new_statement_patterns, ) -def namespace(lsp, ast_node, match_variables): +def namespace(ctx: CompletionContext): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") - for ns in ast_node.root.children[language.Import]: + + for ns in ctx.ast_node.root.children[language.Import]: if ns.gir_namespace is not None: yield Completion( ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".", + sort_text=get_sort_key( + CompletionPriority.NAMESPACE, ns.gir_namespace.name + ), ) + yield from _available_namespace_completions(ctx) + @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], ], ) -def object_completer(lsp, ast_node, match_variables): - ns = ast_node.root.gir.namespaces.get(match_variables[0]) +def object_completer(ctx: CompletionContext): + ns = ctx.ast_node.root.gir.namespaces.get(ctx.match_variables[0]) if ns is not None: for c in ns.classes.values(): + snippet = c.name + if ( + str(ctx.next_token) != "{" + and not isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.BracketedTypeName) + ): + snippet += " {\n $0\n}" + yield Completion( c.name, CompletionItemKind.Class, - snippet=f"{c.name} {{\n $0\n}}", + sort_text=get_sort_key(CompletionPriority.CLASS, c.name), + snippet=snippet, docs=c.doc, detail=c.detail, ) @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=new_statement_patterns, ) -def gtk_object_completer(lsp, ast_node, match_variables): - ns = ast_node.root.gir.namespaces.get("Gtk") +def gtk_object_completer(ctx: CompletionContext): + ns = ctx.ast_node.root.gir.namespaces.get("Gtk") if ns is not None: for c in ns.classes.values(): + snippet = c.name + if ( + str(ctx.next_token) != "{" + and not isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.BracketedTypeName) + ): + snippet += " {\n $0\n}" + yield Completion( c.name, CompletionItemKind.Class, - snippet=f"{c.name} {{\n $0\n}}", + sort_text=get_sort_key(CompletionPriority.CLASS, c.name), + snippet=snippet, docs=c.doc, detail=c.detail, ) + if isinstance(ctx.ast_node, language.BracketedTypeName) or ( + isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.ClassName) + ): + for basic_type in gir.BASIC_TYPES: + yield Completion( + basic_type, + CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, basic_type), + ) + @completer( applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def property_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"): - for prop_name, prop in ast_node.gir_class.properties.items(): - if ( - isinstance(prop.type, gir.BoolType) - and lsp.client_supports_completion_choice - ): - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: ${{1|true,false|}};", - docs=prop.doc, - detail=prop.detail, - ) - elif isinstance(prop.type, gir.StringType): - snippet = ( - f'{prop_name}: _("$0");' - if annotations.is_property_translated(prop) - else f'{prop_name}: "$0";' - ) - - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=snippet, - docs=prop.doc, - detail=prop.detail, - ) - elif ( - isinstance(prop.type, gir.Enumeration) - and len(prop.type.members) <= 10 - and lsp.client_supports_completion_choice - ): - choices = ",".join(prop.type.members.keys()) - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: ${{1|{choices}|}};", - docs=prop.doc, - detail=prop.detail, - ) - 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, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: $0;", - docs=prop.doc, - detail=prop.detail, - ) +def property_completer(ctx: CompletionContext): + assert isinstance(ctx.ast_node, language.ObjectContent) + if ctx.ast_node.gir_class and hasattr(ctx.ast_node.gir_class, "properties"): + for prop_name, prop in ctx.ast_node.gir_class.properties.items(): + yield get_property_completion( + prop_name, + prop.type, + ctx, + annotations.is_property_translated(prop), + prop.doc, + ) @completer( applies_in=[language.Property, language.A11yProperty], - matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], + matches=[ + [(TokenType.IDENT, None), (TokenType.OP, ":")], + [(TokenType.PUNCTUATION, ",")], + [(TokenType.PUNCTUATION, "[")], + ], ) -def prop_value_completer(lsp, ast_node, match_variables): - if (vt := ast_node.value_type) is not None: +def prop_value_completer(ctx: CompletionContext): + if isinstance(ctx.ast_node, language.Property): + yield Completion( + "bind", + CompletionItemKind.Keyword, + snippet="bind $0", + docs=get_docs_section("Syntax Binding"), + sort_text=get_sort_key(CompletionPriority.KEYWORD, "bind"), + ) + + assert isinstance(ctx.ast_node, language.Property) or isinstance( + ctx.ast_node, language.A11yProperty + ) + + if (vt := ctx.ast_node.value_type) is not None: + assert isinstance(vt, ValueTypeCtx) + if isinstance(vt.value_type, gir.Enumeration): for name, member in vt.value_type.members.items(): yield Completion( @@ -210,43 +330,148 @@ def prop_value_completer(lsp, ast_node, match_variables): CompletionItemKind.EnumMember, docs=member.doc, detail=member.detail, + sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, name), ) elif isinstance(vt.value_type, gir.BoolType): - yield Completion("true", CompletionItemKind.Constant) - yield Completion("false", CompletionItemKind.Constant) + yield Completion( + "true", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, "true"), + ) + yield Completion( + "false", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, "false"), + ) + + elif isinstance(vt.value_type, gir.Class) or isinstance( + vt.value_type, gir.Interface + ): + if vt.allow_null: + yield Completion( + "null", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), + ) + + yield from get_object_id_completions(ctx, vt.value_type) + + if isinstance(ctx.ast_node, language.Property): + yield from _available_namespace_completions(ctx) + + for ns in ctx.ast_node.root.gir.namespaces.values(): + for c in ns.classes.values(): + if not c.abstract and c.assignable_to(vt.value_type): + name = ( + c.name if ns.name == "Gtk" else ns.name + "." + c.name + ) + snippet = name + if str(ctx.next_token) != "{": + snippet += " {\n $0\n}" + yield Completion( + name, + CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, name), + snippet=snippet, + detail=c.detail, + docs=c.doc, + ) @completer( applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def signal_completer(lsp, ast_node, match_variables): - if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"): - for signal_name, signal in ast_node.gir_class.signals.items(): - if not isinstance(ast_node.parent, language.Object): - name = "on" +def signal_completer(ctx: CompletionContext): + assert isinstance(ctx.ast_node, language.ObjectContent) + + if ctx.ast_node.gir_class and hasattr(ctx.ast_node.gir_class, "signals"): + for signal_name, signal in ctx.ast_node.gir_class.signals.items(): + if str(ctx.next_token) == "=>": + snippet = signal_name else: - name = "on_" + ( - ast_node.parent.children[ClassName][0].tokens["id"] - or ast_node.parent.children[ClassName][0] - .tokens["class_name"] - .lower() - ) + if not isinstance(ctx.ast_node.parent, language.Object): + name = "on" + else: + name = "on_" + ( + ctx.ast_node.parent.children[ClassName][0].tokens["id"] + or ctx.ast_node.parent.children[ClassName][0] + .tokens["class_name"] + .lower() + ) + + snippet = f"{signal_name} => \\$${{1:{name}_{signal_name.replace('-', '_')}}}()$0;" + yield Completion( signal_name, CompletionItemKind.Event, - sort_text=f"1 {signal_name}", - snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, signal_name), + snippet=snippet, docs=signal.doc, detail=signal.detail, ) @completer(applies_in=[language.UI], matches=new_statement_patterns) -def template_completer(lsp, ast_node, match_variables): +def template_completer(_ctx: CompletionContext): yield Completion( "template", CompletionItemKind.Snippet, snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}", ) + + +@completer( + applies_in=[language.ObjectContent, language.ChildType], + matches=[[(TokenType.PUNCTUATION, "[")]], + applies_in_subclass=[("Gtk", "Dialog"), ("Gtk", "InfoBar")], +) +def response_id_completer(ctx: CompletionContext): + yield Completion( + "action", + CompletionItemKind.Snippet, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "action"), + snippet="action response=$0", + ) + + +@completer( + [language.ChildAnnotation, language.ExtResponse], + [[(TokenType.IDENT, "action"), (TokenType.IDENT, "response"), (TokenType.OP, "=")]], +) +def complete_response_id(ctx: CompletionContext): + gir = ctx.ast_node.root.gir + response_type = gir.get_type("ResponseType", "Gtk") + yield from [ + Completion( + name, + kind=CompletionItemKind.EnumMember, + docs=member.doc, + ) + for name, member in response_type.members.items() + ] + + +@completer( + [language.ChildAnnotation, language.ExtResponse], + [ + [ + (TokenType.IDENT, "action"), + (TokenType.IDENT, "response"), + (TokenType.OP, "="), + (TokenType.IDENT, None), + ], + [ + (TokenType.IDENT, "action"), + (TokenType.IDENT, "response"), + (TokenType.OP, "="), + (TokenType.NUMBER, None), + ], + ], +) +def complete_response_default(ctx: CompletionContext): + yield Completion( + "default", + kind=CompletionItemKind.Keyword, + ) diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index eccf125..970d429 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -19,28 +19,78 @@ import typing as T +from dataclasses import dataclass +from enum import Enum -from .lsp_utils import Completion +from . import gir, language +from .ast_utils import AstNode +from .lsp_utils import Completion, CompletionItemKind from .tokenizer import Token, TokenType + +class CompletionPriority(Enum): + ENUM_MEMBER = "00" + NAMED_OBJECT = "01" + OBJECT_MEMBER = "02" + CLASS = "03" + NAMESPACE = "04" + KEYWORD = "05" + # An available namespace that hasn't been imported yet + IMPORT_NAMESPACE = "99" + + +def get_sort_key(priority: CompletionPriority, name: str): + return f"{priority.value} {name}" + + +@dataclass +class CompletionContext: + client_supports_completion_choice: bool + ast_node: AstNode + match_variables: T.List[str] + next_token: Token + index: int + + new_statement_patterns = [ [(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "}")], [(TokenType.PUNCTUATION, "]")], [(TokenType.PUNCTUATION, ";")], + [(TokenType.OP, "<")], ] +completers = [] + + def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): - def decorator(func): - def inner(prev_tokens: T.List[Token], ast_node, lsp): + def decorator(func: T.Callable[[CompletionContext], T.Generator[Completion]]): + def inner( + prev_tokens: T.List[Token], next_token: Token, ast_node, lsp, idx: int + ): + if not any(isinstance(ast_node, rule) for rule in applies_in): + return + # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: - type = ast_node.root.gir.get_type( - applies_in_subclass[1], applies_in_subclass[0] - ) - if not ast_node.gir_class or not ast_node.gir_class.assignable_to(type): + parent_obj = ast_node + while parent_obj is not None and not hasattr(parent_obj, "gir_class"): + parent_obj = parent_obj.parent + + if ( + parent_obj is None + or not parent_obj.gir_class + or not any( + [ + parent_obj.gir_class.assignable_to( + parent_obj.root.gir.get_type(c[1], c[0]) + ) + for c in applies_in_subclass + ] + ) + ): return any_match = len(matches) == 0 @@ -66,10 +116,65 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None if not any_match: return - yield from func(lsp, ast_node, match_variables) + context = CompletionContext( + client_supports_completion_choice=lsp.client_supports_completion_choice, + ast_node=ast_node, + match_variables=match_variables, + next_token=next_token, + index=idx, + ) + yield from func(context) - for c in applies_in: - c.completers.append(inner) + completers.append(inner) return inner return decorator + + +def get_property_completion( + name: str, + type: gir.GirType, + ctx: CompletionContext, + translated: bool, + doc: str, +) -> Completion: + if str(ctx.next_token) == ":": + snippet = name + elif isinstance(type, gir.BoolType) and ctx.client_supports_completion_choice: + snippet = f"{name}: ${{1|true,false|}};" + elif isinstance(type, gir.StringType): + snippet = f'{name}: _("$0");' if translated else f'{name}: "$0";' + elif ( + isinstance(type, gir.Enumeration) + and len(type.members) <= 10 + and ctx.client_supports_completion_choice + ): + choices = ",".join(type.members.keys()) + snippet = f"{name}: ${{1|{choices}|}};" + elif type.full_name == "Gtk.Expression": + snippet = f"{name}: expr $0;" + else: + snippet = f"{name}: $0;" + + return Completion( + name, + CompletionItemKind.Property, + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, name), + snippet=snippet, + docs=doc, + ) + + +def get_object_id_completions( + ctx: CompletionContext, value_type: T.Optional[gir.GirType] = None +): + for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items(): + if value_type is None or ( + obj.gir_class is not None and obj.gir_class.assignable_to(value_type) + ): + yield Completion( + id, + CompletionItemKind.Variable, + signature=" " + obj.signature, + sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), + ) diff --git a/blueprintcompiler/decompiler.py b/blueprintcompiler/decompiler.py index de6c06f..850b6d8 100644 --- a/blueprintcompiler/decompiler.py +++ b/blueprintcompiler/decompiler.py @@ -255,7 +255,11 @@ def decompile_element( ctx._node_stack.append(xml) ctx.start_block() - gir = decompiler(*args, **kwargs) + + try: + gir = decompiler(*args, **kwargs) + except TypeError as e: + raise UnsupportedError(tag=xml.tag) if not decompiler._skip_children: for child in xml.children: @@ -266,8 +270,6 @@ def decompile_element( except UnsupportedError as e: raise e - except TypeError as e: - raise UnsupportedError(tag=xml.tag) def decompile(data: str) -> str: diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index 1e7297c..f5d2e06 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -92,29 +92,38 @@ class CompileError(PrintableError): def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: assert self.range is not None - line_num, col_num = utils.idx_to_pos(self.range.start + 1, code) - end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code) - line = code.splitlines(True)[line_num] if code != "" else "" + def format_line(range: Range): + line_num, col_num = utils.idx_to_pos(range.start, code) + end_line_num, end_col_num = utils.idx_to_pos(range.end, code) + line = code.splitlines(True)[line_num] if code != "" else "" - # Display 1-based line numbers - line_num += 1 - end_line_num += 1 + # Display 1-based line numbers + line_num += 1 + end_line_num += 1 + col_num += 1 + end_col_num += 1 - n_spaces = col_num - 1 - n_carets = ( - (end_col_num - col_num) - if line_num == end_line_num - else (len(line) - n_spaces - 1) - ) + n_spaces = col_num - 1 + n_carets = ( + (end_col_num - col_num) + if line_num == end_line_num + else (len(line) - n_spaces - 1) + ) - n_spaces += line.count("\t", 0, col_num) - n_carets += line.count("\t", col_num, col_num + n_carets) - line = line.replace("\t", " ") + n_spaces += line.count("\t", 0, col_num) + n_carets += line.count("\t", col_num, col_num + n_carets) + line = line.replace("\t", " ") + + n_carets = max(n_carets, 1) + + return line_num, col_num, line.rstrip(), (" " * n_spaces) + ("^" * n_carets) + + line_num, col_num, line, carets = format_line(self.range) stream.write( f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n""" +{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n""" ) for hint in self.hints: @@ -139,14 +148,12 @@ at {filename} line {line_num} column {col_num}: ) for ref in self.references: - line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code) - line = code.splitlines(True)[line_num] - line_num += 1 + line_num, col_num, line, carets = format_line(ref.range) stream.write( f"""{Colors.FAINT}note: {ref.message}: at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" +{Colors.FAINT}{line_num :>4} |{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n""" ) stream.write("\n") @@ -219,7 +226,7 @@ def report_bug(): # pragma: no cover f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** The blueprint-compiler program has crashed. Please report the above stacktrace, along with the input file(s) if possible, on GitLab: -{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue +{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue {Colors.CLEAR}""" ) diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 333f4ac..6392eb4 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -289,7 +289,7 @@ class TypeType(BasicType): return isinstance(other, TypeType) -_BASIC_TYPES = { +BASIC_TYPES = { "bool": BoolType, "string": StringType, "int": IntType, @@ -914,7 +914,7 @@ class Namespace(GirNode): def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: """Gets a type from this namespace by its C name.""" - for basic in _BASIC_TYPES.values(): + for basic in BASIC_TYPES.values(): if basic.glib_type_name == cname: return basic() @@ -1036,8 +1036,8 @@ class GirContext: return None def get_type(self, name: str, ns: str) -> T.Optional[GirType]: - if ns is None and name in _BASIC_TYPES: - return _BASIC_TYPES[name]() + if ns is None and name in BASIC_TYPES: + return BASIC_TYPES[name]() ns = ns or "Gtk" diff --git a/blueprintcompiler/interactive_port.py b/blueprintcompiler/interactive_port.py index 0c37885..12dd485 100644 --- a/blueprintcompiler/interactive_port.py +++ b/blueprintcompiler/interactive_port.py @@ -71,7 +71,7 @@ def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: print( f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: -{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" +{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" ) return CouldNotPort("does not compile") @@ -136,7 +136,7 @@ def step1(): wrap.write( f"""[wrap-git] directory = blueprint-compiler -url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git revision = {VERSION} depth = 1 diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 5eb2b60..88d7538 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -34,10 +34,17 @@ from .gtk_scale import ExtScaleMarks from .gtk_size_group import ExtSizeGroupWidgets from .gtk_string_list import ExtStringListStrings from .gtk_styles import ExtStyles -from .gtkbuilder_child import Child, ChildExtension, ChildInternal, ChildType +from .gtkbuilder_child import ( + Child, + ChildAnnotation, + ChildExtension, + ChildInternal, + ChildType, +) from .gtkbuilder_template import Template from .imports import GtkDirective, Import -from .types import ClassName +from .response_id import ExtResponse +from .types import BracketedTypeName, ClassName, TypeName from .ui import UI from .values import ( ArrayValue, diff --git a/blueprintcompiler/language/adw_breakpoint.py b/blueprintcompiler/language/adw_breakpoint.py index 4ad5b24..3d2c10d 100644 --- a/blueprintcompiler/language/adw_breakpoint.py +++ b/blueprintcompiler/language/adw_breakpoint.py @@ -81,8 +81,8 @@ class AdwBreakpointSetter(AstNode): return self.tokens["property"] @property - def value(self) -> Value: - return self.children[Value][0] + def value(self) -> T.Optional[Value]: + return self.children[Value][0] if len(self.children[Value]) > 0 else None @property def gir_class(self) -> T.Optional[GirType]: @@ -106,7 +106,10 @@ class AdwBreakpointSetter(AstNode): return None @property - def document_symbol(self) -> DocumentSymbol: + def document_symbol(self) -> T.Optional[DocumentSymbol]: + if self.value is None: + return None + return DocumentSymbol( f"{self.object_id}.{self.property_name}", SymbolKind.Property, diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index d2680fd..b1b43a4 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -140,21 +140,10 @@ class ExtAdwResponseDialog(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Adw", "MessageDialog"), + applies_in_subclass=[("Adw", "AlertDialog"), ("Adw", "MessageDialog")], matches=new_statement_patterns, ) -def complete_adw_message_dialog(lsp, ast_node, match_variables): - yield Completion( - "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" - ) - - -@completer( - applies_in=[ObjectContent], - applies_in_subclass=("Adw", "AlertDialog"), - matches=new_statement_patterns, -) -def complete_adw_alert_dialog(lsp, ast_node, match_variables): +def complete_adw_message_dialog(_ctx: CompletionContext): yield Completion( "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" ) diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 1cc1b3b..9bd04a5 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -34,6 +34,7 @@ from ..errors import ( CompileError, CompileWarning, DeprecatedWarning, + ErrorReference, MultipleErrors, UnusedWarning, UpgradeWarning, diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 6e26048..38d84f4 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -48,7 +48,7 @@ class ScopeCtx: return self.node @cached_property - def objects(self) -> T.Dict[str, Object]: + def objects(self) -> T.Dict[str, AstNode]: return { obj.tokens["id"]: obj for obj in self._iter_recursive(self.node) @@ -58,7 +58,7 @@ class ScopeCtx: def validate_unique_ids(self) -> None: from .gtk_list_item_factory import ExtListItemFactory - passed = {} + passed: T.Dict[str, AstNode] = {} for obj in self._iter_recursive(self.node): if obj.tokens["id"] is None: continue @@ -71,10 +71,16 @@ class ScopeCtx: raise CompileError( f"Duplicate object ID '{obj.tokens['id']}'", token.range, + references=[ + ErrorReference( + passed[obj.tokens["id"]].group.tokens["id"].range, + "previous declaration was here", + ) + ], ) passed[obj.tokens["id"]] = obj - def _iter_recursive(self, node: AstNode): + def _iter_recursive(self, node: AstNode) -> T.Generator[AstNode, T.Any, None]: yield node for child in node.children: if child.context[ScopeCtx] is self: diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index e0b4246..910cd71 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -21,7 +21,7 @@ from ..decompiler import decompile_element from .common import * from .contexts import ScopeCtx, ValueTypeCtx -from .types import TypeName +from .types import BracketedTypeName, TypeName expr = Sequence() @@ -196,7 +196,7 @@ class CastExpr(InfixExpr): grammar = [ Keyword("as"), AnyOf( - ["<", TypeName, Match(">").expected()], + BracketedTypeName, [ UseExact("lparen", "("), TypeName, @@ -211,7 +211,13 @@ class CastExpr(InfixExpr): @property def type(self) -> T.Optional[GirType]: - return self.children[TypeName][0].gir_type + if len(self.children[BracketedTypeName]) == 1: + type_name = self.children[BracketedTypeName][0].type_name + return None if type_name is None else type_name.gir_type + elif len(self.children[TypeName]) == 1: + return self.children[TypeName][0].gir_type + else: + return None @validate() def cast_makes_sense(self): @@ -302,12 +308,18 @@ expr.children = [ @decompiler("lookup", skip_children=True, cdata=True) def decompile_lookup( - ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str + ctx: DecompileCtx, + gir: gir.GirContext, + cdata: str, + name: str, + type: T.Optional[str] = None, ): if ctx.parent_node is not None and ctx.parent_node.tag == "property": ctx.print("expr ") - if t := ctx.type_by_cname(type): + if type is None: + type = "" + elif t := ctx.type_by_cname(type): type = decompile.full_name(t) else: type = "$" + type @@ -327,7 +339,7 @@ def decompile_lookup( if constant == ctx.template_class: ctx.print("template." + name) elif constant == "": - ctx.print("item as <" + type + ">." + name) + ctx.print(f"item as <{type}>.{name}") else: ctx.print(constant + "." + name) return diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 50a7512..67f2555 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -26,7 +26,11 @@ from .values import ArrayValue, ExprValue, ObjectValue, Value class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) + UseIdent("name"), + ":", + AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue).expected( + "property value" + ), ) @property diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 9c27b97..b6afb09 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -225,8 +225,14 @@ class Signal(AstNode): @decompiler("signal") -def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", object=None): +def decompile_signal( + ctx: DecompileCtx, gir, name, handler, swapped=None, after="false", object=None +): object_name = object or "" + + if object_name == ctx.template_class: + object_name = "template" + name = name.replace("_", "-") line = f"{name} => ${handler}({object_name})" @@ -241,3 +247,11 @@ def decompile_signal(ctx, gir, name, handler, swapped=None, after="false", objec line += ";" ctx.print(line) return gir + + +@completer( + [Signal], + [[(TokenType.PUNCTUATION, "(")]], +) +def signal_object_completer(ctx: CompletionContext): + yield from get_object_id_completions(ctx) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 0cc3cb3..2a895ef 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -25,12 +25,13 @@ from .gobject_object import ObjectContent, validate_parent_type from .values import Value -def get_property_types(gir): +def get_property_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: # from return { "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), "description": StringType(), "has-popup": BoolType(), + "help-text": StringType(), "key-shortcuts": StringType(), "label": StringType(), "level": IntType(), @@ -50,7 +51,7 @@ def get_property_types(gir): } -def get_relation_types(gir): +def get_relation_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: # from widget = gir.get_type("Widget", "Gtk") return { @@ -75,7 +76,7 @@ def get_relation_types(gir): } -def get_state_types(gir): +def get_state_types(gir: gir.GirContext) -> T.Dict[str, T.Optional[GirType]]: # from return { "busy": BoolType(), @@ -86,9 +87,24 @@ def get_state_types(gir): "invalid": gir.get_type("AccessibleInvalidState", "Gtk"), "pressed": gir.get_type("AccessibleTristate", "Gtk"), "selected": BoolType(), + "visited": BoolType(), } +TRANSLATED = set( + [ + "description", + "help-text", + "label", + "placeholder", + "role-description", + "value-text", + "col-index-text", + "row-index-text", + ] +) + + def get_types(gir): return { **get_property_types(gir), @@ -121,7 +137,9 @@ class A11yProperty(AstNode): grammar = Statement( UseIdent("name"), ":", - AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]), + AnyOf( + Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"] + ).expected("value"), ) @property @@ -232,9 +250,12 @@ class ExtAccessibility(AstNode): applies_in=[ObjectContent], matches=new_statement_patterns, ) -def a11y_completer(lsp, ast_node, match_variables): +def a11y_completer(_ctx: CompletionContext): yield Completion( - "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" + "accessibility", + CompletionItemKind.Snippet, + snippet="accessibility {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "accessibility"), ) @@ -242,12 +263,14 @@ def a11y_completer(lsp, ast_node, match_variables): applies_in=[ExtAccessibility], matches=new_statement_patterns, ) -def a11y_name_completer(lsp, ast_node, match_variables): - for name, type in get_types(ast_node.root.gir).items(): - yield Completion( +def a11y_property_completer(ctx: CompletionContext): + for name, type in get_types(ctx.ast_node.root.gir).items(): + yield get_property_completion( name, - CompletionItemKind.Property, - docs=_get_docs(ast_node.root.gir, type.name), + type, + ctx, + name in TRANSLATED, + _get_docs(ctx.ast_node.root.gir, name), ) diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 32b3486..aa1fe1d 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -91,11 +91,16 @@ class ExtComboBoxItems(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "ComboBoxText"), + applies_in_subclass=[("Gtk", "ComboBoxText")], matches=new_statement_patterns, ) -def items_completer(lsp, ast_node, match_variables): - yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") +def items_completer(_ctx: CompletionContext): + yield Completion( + "items", + CompletionItemKind.Snippet, + snippet="items [$0]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "items"), + ) @decompiler("items", parent_type="Gtk.ComboBoxText") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index e84afc7..36e7da4 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -98,15 +98,28 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "FileFilter"), + applies_in_subclass=[("Gtk", "FileFilter")], matches=new_statement_patterns, ) -def file_filter_completer(lsp, ast_node, match_variables): +def file_filter_completer(_ctx: CompletionContext): yield Completion( - "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' + "mime-types", + CompletionItemKind.Snippet, + snippet='mime-types ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "mime-types"), + ) + yield Completion( + "patterns", + CompletionItemKind.Snippet, + snippet='patterns ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "patterns"), + ) + yield Completion( + "suffixes", + CompletionItemKind.Snippet, + snippet='suffixes ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "suffixes"), ) - yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]') - yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]') @decompiler("mime-types") diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8d3e37a..8dd3458 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -90,11 +90,16 @@ class ExtLayout(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "Widget"), + applies_in_subclass=[("Gtk", "Widget")], matches=new_statement_patterns, ) -def layout_completer(lsp, ast_node, match_variables): - yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") +def layout_completer(_ctx: CompletionContext): + yield Completion( + "layout", + CompletionItemKind.Snippet, + snippet="layout {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "layout"), + ) @decompiler("layout") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index c7ef5f2..ed7ede8 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -243,7 +243,7 @@ from .ui import UI applies_in=[UI], matches=new_statement_patterns, ) -def menu_completer(lsp, ast_node, match_variables): +def menu_completer(_ctx: CompletionContext): yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") @@ -251,23 +251,50 @@ def menu_completer(lsp, ast_node, match_variables): applies_in=[Menu], matches=new_statement_patterns, ) -def menu_content_completer(lsp, ast_node, match_variables): +def menu_content_completer(_ctx: CompletionContext): yield Completion( - "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" + "submenu", + CompletionItemKind.Snippet, + snippet="submenu {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.CLASS, "1 submenu"), ) yield Completion( - "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" + "section", + CompletionItemKind.Snippet, + snippet="section {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.CLASS, "1 section"), + ) + yield Completion( + "item", + CompletionItemKind.Snippet, + snippet="item {\n $0\n}", + sort_text=get_sort_key(CompletionPriority.CLASS, "1 item"), ) - yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}") yield Completion( "item (shorthand)", CompletionItemKind.Snippet, snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', + sort_text=get_sort_key(CompletionPriority.CLASS, "0 item (shorthand)"), ) - yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;") - yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') - yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') + yield Completion( + "label", + CompletionItemKind.Snippet, + snippet="label: $0;", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "label"), + ) + yield Completion( + "action", + CompletionItemKind.Snippet, + snippet='action: "$0";', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "action"), + ) + yield Completion( + "icon", + CompletionItemKind.Snippet, + snippet='icon: "$0";', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "icon"), + ) @decompiler("menu") diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 1fd5ac3..5dc49d8 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -23,22 +23,20 @@ from .values import StringValue class ExtScaleMark(AstNode): - grammar = [ + grammar = Statement( Keyword("mark"), Match("(").expected(), - [ - Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), - UseNumber("value"), - Optional( - [ - ",", - UseIdent("position"), - Optional([",", StringValue]), - ] - ), - ], - Match(")").expected(), - ] + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value").expected("value"), + Optional( + [ + ",", + UseIdent("position").expected("position"), + Optional([",", to_parse_node(StringValue).expected("label")]), + ] + ), + end=")", + ) @property def value(self) -> float: @@ -134,20 +132,42 @@ class ExtScaleMarks(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "Scale"), + applies_in_subclass=[("Gtk", "Scale")], matches=new_statement_patterns, ) -def complete_marks(lsp, ast_node, match_variables): - yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") +def complete_marks(_ctx: CompletionContext): + yield Completion( + "marks", + CompletionItemKind.Keyword, + snippet="marks [\n\t$0\n]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "marks"), + ) @completer( applies_in=[ExtScaleMarks], ) -def complete_mark(lsp, ast_node, match_variables): +def complete_mark(_ctx: CompletionContext): yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") +@completer( + applies_in=[ExtScaleMark], + matches=[[(TokenType.NUMBER, None), (TokenType.PUNCTUATION, ",")]], +) +def complete_mark_position(ctx: CompletionContext): + gir = ctx.ast_node.root.gir + response_type = gir.get_type("PositionType", "Gtk") + yield from [ + Completion( + name, + kind=CompletionItemKind.EnumMember, + docs=member.doc, + ) + for name, member in response_type.members.items() + ] + + @decompiler("marks") def decompile_marks( ctx, diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 54d85e5..e7a6a35 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -101,11 +101,16 @@ class ExtSizeGroupWidgets(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "SizeGroup"), + applies_in_subclass=[("Gtk", "SizeGroup")], matches=new_statement_patterns, ) -def size_group_completer(lsp, ast_node, match_variables): - yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") +def size_group_completer(_ctx: CompletionContext): + yield Completion( + "widgets", + CompletionItemKind.Snippet, + snippet="widgets [$0]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "widgets"), + ) @decompiler("widgets") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index a146f35..4d15d32 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -72,11 +72,16 @@ class ExtStringListStrings(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "StringList"), + applies_in_subclass=[("Gtk", "StringList")], matches=new_statement_patterns, ) -def strings_completer(lsp, ast_node, match_variables): - yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") +def strings_completer(_ctx: CompletionContext): + yield Completion( + "strings", + CompletionItemKind.Snippet, + snippet="strings [$0]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "strings"), + ) @decompiler("items", parent_type="Gtk.StringList") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 8617522..0836073 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -77,11 +77,16 @@ class ExtStyles(AstNode): @completer( applies_in=[ObjectContent], - applies_in_subclass=("Gtk", "Widget"), + applies_in_subclass=[("Gtk", "Widget")], matches=new_statement_patterns, ) -def style_completer(lsp, ast_node, match_variables): - yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') +def style_completer(_ctx: CompletionContext): + yield Completion( + "styles", + CompletionItemKind.Keyword, + snippet='styles ["$0"]', + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "styles"), + ) @decompiler("style") diff --git a/blueprintcompiler/language/gtkbuilder_child.py b/blueprintcompiler/language/gtkbuilder_child.py index bee551c..0eb8f04 100644 --- a/blueprintcompiler/language/gtkbuilder_child.py +++ b/blueprintcompiler/language/gtkbuilder_child.py @@ -31,7 +31,12 @@ ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ class ChildInternal(AstNode): - grammar = ["internal-child", UseIdent("internal_child")] + grammar = [ + "[", + "internal-child", + UseIdent("internal_child").expected("internal child name"), + Match("]").expected(), + ] @property def internal_child(self) -> str: @@ -39,7 +44,7 @@ class ChildInternal(AstNode): class ChildType(AstNode): - grammar = UseIdent("child_type").expected("a child type") + grammar = ["[", UseIdent("child_type").expected("a child type"), "]"] @property def child_type(self) -> str: @@ -59,7 +64,7 @@ class ChildExtension(AstNode): class ChildAnnotation(AstNode): - grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] + grammar = AnyOf(ChildInternal, ChildExtension, ChildType) @property def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]: diff --git a/blueprintcompiler/language/response_id.py b/blueprintcompiler/language/response_id.py index 939f71f..83843ed 100644 --- a/blueprintcompiler/language/response_id.py +++ b/blueprintcompiler/language/response_id.py @@ -28,19 +28,21 @@ class ExtResponse(AstNode): ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] - grammar = [ + grammar = Statement( + "[", Keyword("action"), Keyword("response"), - "=", + Match("=").expected(), AnyOf( UseIdent("response_id"), [ Optional(UseExact("sign", "-")), UseNumber("response_id"), ], - ), + ).expected("response ID"), Optional([Keyword("default"), UseLiteral("is_default", True)]), - ] + end="]", + ) @validate() def parent_has_action_widgets(self) -> None: diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index fe45c4d..da41360 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -27,11 +27,11 @@ class TypeName(AstNode): [ UseIdent("namespace"), ".", - UseIdent("class_name"), + UseIdent("class_name").expected("class name"), ], [ AnyOf("$", [".", UseLiteral("old_extern", True)]), - UseIdent("class_name"), + UseIdent("class_name").expected("class name"), UseLiteral("extern", True), ], UseIdent("class_name"), @@ -47,7 +47,11 @@ class TypeName(AstNode): @validate("class_name") def type_exists(self): - if not self.tokens["extern"] and self.gir_ns is not None: + if ( + not self.tokens["extern"] + and self.gir_ns is not None + and self.tokens["class_name"] is not None + ): self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) @@ -182,3 +186,14 @@ class TemplateClassName(ClassName): self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) + + +class BracketedTypeName(AstNode): + grammar = Statement("<", to_parse_node(TypeName).expected("type name"), end=">") + + @property + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[TypeName]) == 0: + return None + + return self.children[TypeName][0] diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index d55a22a..896c0f7 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -110,16 +110,22 @@ class UI(AstNode): and self.template.class_name.glib_type_name == id ) - def import_code_action(self, ns: str, version: str) -> CodeAction: - if len(self.children[Import]): - pos = self.children[Import][-1].range.end - else: - pos = self.children[GtkDirective][0].range.end + def import_range(self, ns: str): + """Returns a range to insert a new import statement""" + pos = self.children[GtkDirective][0].range.end + # try to insert alphabetically + for import_ in self.children[Import]: + if ns.lower() > import_.namespace.lower(): + pos = import_.range.end + + return Range(pos, pos, self.group.text) + + def import_code_action(self, ns: str, version: str) -> CodeAction: return CodeAction( f"Import {ns} {version}", f"\nusing {ns} {version};", - Range(pos, pos, self.group.text), + self.import_range(ns), ) @cached_property diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 5556d99..2840337 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -26,7 +26,7 @@ from .common import * from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx from .expression import Expression from .gobject_object import Object -from .types import TypeName +from .types import BracketedTypeName, TypeName class Translated(AstNode): @@ -58,6 +58,19 @@ class Translated(AstNode): f"Cannot convert translated string to {expected_type.full_name}" ) + @validate("context") + def context_double_quoted(self): + if self.translate_context is None: + return + + if not str(self.group.tokens["context"]).startswith('"'): + raise CompileWarning("gettext may not recognize single-quoted strings") + + @validate("string") + def string_double_quoted(self): + if not str(self.group.tokens["string"]).startswith('"'): + raise CompileWarning("gettext may not recognize single-quoted strings") + @docs() def ref_docs(self): return get_docs_section("Syntax Translated") @@ -67,11 +80,7 @@ class TypeLiteral(AstNode): grammar = [ "typeof", AnyOf( - [ - "<", - to_parse_node(TypeName).expected("type name"), - Match(">").expected(), - ], + BracketedTypeName, [ UseExact("lparen", "("), to_parse_node(TypeName).expected("type name"), @@ -85,8 +94,13 @@ class TypeLiteral(AstNode): return gir.TypeType() @property - def type_name(self) -> TypeName: - return self.children[TypeName][0] + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[BracketedTypeName]) == 1: + return self.children[BracketedTypeName][0].type_name + elif len(self.children[TypeName]) == 1: + return self.children[TypeName][0] + else: + return None @validate() def validate_for_type(self) -> None: @@ -212,12 +226,12 @@ 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 elif member := type.members.get(self.name): - return member.value + return member.nick else: return None @@ -325,7 +339,14 @@ class IdentLiteral(AstNode): raise CompileError( '"item" can only be used in an expression literal' ) - elif self.ident not in ["true", "false"]: + elif self.ident in ["true", "false"]: + if expected_type is not None and not isinstance( + expected_type, gir.BoolType + ): + raise CompileError( + f"Cannot assign boolean to {expected_type.full_name}" + ) + else: raise CompileError( f"Could not find object with ID {self.ident}", did_you_mean=( diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index b938181..2a4380a 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -87,6 +87,7 @@ class Completion: text: T.Optional[str] = None snippet: T.Optional[str] = None detail: T.Optional[str] = None + additional_text_edits: T.Optional[T.List["TextEdit"]] = None def to_json(self, snippets: bool): insert_text = self.text or self.label @@ -114,6 +115,11 @@ class Completion: "insertText": insert_text, "insertTextFormat": insert_text_format, "detail": self.detail if self.detail else None, + "additionalTextEdits": ( + [edit.to_json() for edit in self.additional_text_edits] + if self.additional_text_edits + else None + ), } return {k: v for k, v in result.items() if v is not None} diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 5c03761..3f52ca5 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -209,6 +209,7 @@ class XmlOutput(OutputFormat): else: xml.put_text(self._object_id(value, value.ident)) elif isinstance(value, TypeLiteral): + assert value.type_name is not None xml.put_text(value.type_name.glib_type_name) else: if isinstance(value.value, float) and value.value == int(value.value): @@ -308,6 +309,9 @@ class XmlOutput(OutputFormat): elif isinstance(extension, AdwBreakpointSetters): for setter in extension.setters: + if setter.value is None: + continue + attrs = {} if isinstance(setter.value.child, Translated): diff --git a/blueprintcompiler/outputs/xml/xml_emitter.py b/blueprintcompiler/outputs/xml/xml_emitter.py index ea91e03..d34eff4 100644 --- a/blueprintcompiler/outputs/xml/xml_emitter.py +++ b/blueprintcompiler/outputs/xml/xml_emitter.py @@ -73,6 +73,7 @@ class XmlEmitter: self._needs_newline = False def put_cdata(self, text: str): + text = text.replace("]]>", "]]]]>") self.result += f"" self._needs_newline = False diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index ae062fb..8bb4c66 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -17,7 +17,7 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -""" Utilities for parsing an AST from a token stream. """ +"""Utilities for parsing an AST from a token stream.""" import typing as T from enum import Enum @@ -235,7 +235,15 @@ class ParseNode: start_idx = ctx.index inner_ctx = ctx.create_child() - if self._parse(inner_ctx): + try: + result = self._parse(inner_ctx) + except Exception as e: + # If an exception occurs, there's an explicit error, not just a rule that didn't match. Apply the context + # state so that whichever rule handles the exception (e.g. a Statement) knows where the error occurred. + ctx.apply_child(inner_ctx) + raise e + + if result: ctx.apply_child(inner_ctx) if ctx.index == start_idx: return ParseResult.EMPTY @@ -269,12 +277,12 @@ class Err(ParseNode): if self.child.parse(ctx).failed(): start_idx = ctx.start while ctx.tokens[start_idx].type in SKIP_TOKENS: - start_idx += 1 + start_idx -= 1 start_token = ctx.tokens[start_idx] - raise CompileError( - self.message, Range(start_token.start, start_token.start, ctx.text) - ) + position = start_token.start if ctx.start == start_idx else start_token.end + + raise CompileError(self.message, Range(position, position, ctx.text)) return True @@ -329,8 +337,9 @@ class Statement(ParseNode): """ParseNode that attempts to match all of its children in sequence. If any child raises an error, the error will be logged but parsing will continue.""" - def __init__(self, *children): + def __init__(self, *children, end: str = ";"): self.children = [to_parse_node(child) for child in children] + self.end = end def _parse(self, ctx) -> bool: for child in self.children: @@ -340,11 +349,29 @@ class Statement(ParseNode): except CompileError as e: ctx.errors.append(e) ctx.set_group_incomplete() + + token = ctx.peek_token() + if str(token) == self.end: + ctx.next_token() + return True token = ctx.peek_token() - if str(token) != ";": - ctx.errors.append(CompileError("Expected `;`", token.range)) + if str(token) != self.end: + start_idx = ctx.index - 1 + while ctx.tokens[start_idx].type in SKIP_TOKENS: + start_idx -= 1 + start_token = ctx.tokens[start_idx] + + position = ( + start_token.start if ctx.index - 1 == start_idx else start_token.end + ) + + ctx.errors.append( + CompileError( + f"Expected `{self.end}`", Range(position, position, ctx.text) + ) + ) else: ctx.next_token() return True @@ -405,7 +432,6 @@ class Until(ParseNode): ctx.skip_unexpected_token() except CompileError as e: ctx.errors.append(e) - ctx.next_token() return True diff --git a/docs/collect-sections.py b/docs/collect-sections.py index e6227e7..a2dd004 100755 --- a/docs/collect-sections.py +++ b/docs/collect-sections.py @@ -9,7 +9,7 @@ from pathlib import Path __all__ = ["get_docs_section"] -DOCS_ROOT = "https://jwestman.pages.gitlab.gnome.org/blueprint-compiler" +DOCS_ROOT = "https://gnome.pages.gitlab.gnome.org/blueprint-compiler" sections: dict[str, "Section"] = {} @@ -132,5 +132,8 @@ if __name__ == "__main__": # print the sections to a json file with open(outfile, "w") as f: json.dump( - {name: section.to_json() for name, section in sections.items()}, f, indent=2 + {name: section.to_json() for name, section in sections.items()}, + f, + indent=2, + sort_keys=True, ) diff --git a/docs/flatpak.rst b/docs/flatpak.rst index 0071d2f..8081c8d 100644 --- a/docs/flatpak.rst +++ b/docs/flatpak.rst @@ -16,8 +16,8 @@ a module in your flatpak manifest: "sources": [ { "type": "git", - "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", - "tag": "v0.14.0" + "url": "https://gitlab.gnome.org/GNOME/blueprint-compiler", + "tag": "v0.16.0" } ] } diff --git a/docs/index.rst b/docs/index.rst index 34b942c..6cd130f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces. using Gtk 4.0; - template MyAppWindow : ApplicationWindow { + template $MyAppWindow: ApplicationWindow { default-width: 600; default-height: 300; title: _("Hello, Blueprint!"); @@ -35,7 +35,7 @@ Blueprint is a markup language and compiler for GTK 4 user interfaces. HeaderBar {} Label { - label: bind MyAppWindow.main_text; + label: bind template.main_text; } } @@ -59,7 +59,7 @@ Features Links ----- -- `Source code `_ +- `Source code `_ - `Workbench `_ lets you try, preview and export Blueprint - `GNOME Builder `_ provides builtin support - `Vim syntax highlighting plugin by thetek42 `_ diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index 0961d14..2fd5dbb 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -10,7 +10,7 @@ Properties are the main way to set values on objects, but they are limited by th Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() `_ for internal details. - Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. + Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. .. rst-class:: grammar-block diff --git a/docs/setup.rst b/docs/setup.rst index 839f8f6..914c753 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -8,7 +8,7 @@ Setting up Blueprint on a new or existing project Using the porting tool ~~~~~~~~~~~~~~~~~~~~~~ -Clone `blueprint-compiler `_ +Clone `blueprint-compiler `_ from source. You can install it using ``meson _build`` and ``ninja -C _build install``, or you can leave it uninstalled. @@ -29,7 +29,7 @@ blueprint-compiler works as a meson subproject. [wrap-git] directory = blueprint-compiler - url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git + url = https://gitlab.gnome.org/GNOME/blueprint-compiler.git revision = main depth = 1 diff --git a/docs/translations.rst b/docs/translations.rst index 7ebf929..7af2099 100644 --- a/docs/translations.rst +++ b/docs/translations.rst @@ -24,6 +24,8 @@ If you're using Meson's `i18n module - 1|4 + is-service|handles-open 1 diff --git a/tests/samples/issue_187.ui b/tests/samples/issue_187.ui new file mode 100644 index 0000000..941a00f --- /dev/null +++ b/tests/samples/issue_187.ui @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/tests/samples/issue_187_dec.blp b/tests/samples/issue_187_dec.blp new file mode 100644 index 0000000..30b997c --- /dev/null +++ b/tests/samples/issue_187_dec.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template ListItem { + child: Label { + label: bind template.item as <$RecentObject>.filename; + }; +} diff --git a/tests/samples/list_factory_nested.blp b/tests/samples/list_factory_nested.blp new file mode 100644 index 0000000..86a59b3 --- /dev/null +++ b/tests/samples/list_factory_nested.blp @@ -0,0 +1,17 @@ +using Gtk 4.0; + +Gtk.ListView { + factory: Gtk.BuilderListItemFactory list_item_factory { + template ListItem { + child: Gtk.ListView { + factory: Gtk.BuilderListItemFactory list_item_factory { + template ListItem { + child: Gtk.Label { + label: bind template.item as <$MyObject>.name; + }; + } + }; + }; + } + }; +} diff --git a/tests/samples/list_factory_nested.ui b/tests/samples/list_factory_nested.ui new file mode 100644 index 0000000..44cdb2b --- /dev/null +++ b/tests/samples/list_factory_nested.ui @@ -0,0 +1,44 @@ + + + + + + + + + + +]]> + + + + diff --git a/tests/samples/list_factory_nested_dec.blp b/tests/samples/list_factory_nested_dec.blp new file mode 100644 index 0000000..755491c --- /dev/null +++ b/tests/samples/list_factory_nested_dec.blp @@ -0,0 +1,18 @@ +using Gtk 4.0; + +ListView { + factory: BuilderListItemFactory list_item_factory { + template ListItem { + child: ListView { + factory: BuilderListItemFactory list_item_factory { + template ListItem { + child: Label { + label: bind template.item as <$MyObject>.name; + }; + } + }; + }; + } + }; +} + diff --git a/tests/samples/signal_template_object.blp b/tests/samples/signal_template_object.blp new file mode 100644 index 0000000..16dd5a0 --- /dev/null +++ b/tests/samples/signal_template_object.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template $MyTemplate { + Button { + clicked => $my_signal_handler(template); + } +} diff --git a/tests/samples/signal_template_object.ui b/tests/samples/signal_template_object.ui new file mode 100644 index 0000000..c9a680a --- /dev/null +++ b/tests/samples/signal_template_object.ui @@ -0,0 +1,16 @@ + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 1f56eb6..7d32ecb 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -64,11 +64,11 @@ class TestSamples(unittest.TestCase): def assert_ast_doesnt_crash(self, text, tokens, ast: AstNode): lsp = LanguageServer() - for i in range(len(text)): + for i in range(len(text) + 1): ast.get_docs(i) - for i in range(len(text)): + for i in range(len(text) + 1): list(complete(lsp, ast, tokens, i)) - for i in range(len(text)): + for i in range(len(text) + 1): ast.get_reference(i) ast.get_document_symbols() @@ -143,9 +143,9 @@ class TestSamples(unittest.TestCase): ] def error_str(error: CompileError): - line, col = utils.idx_to_pos(error.range.start + 1, blueprint) + line, col = utils.idx_to_pos(error.range.start, blueprint) len = error.range.length - return ",".join([str(line + 1), str(col), str(len), error.message]) + return ",".join([str(line + 1), str(col + 1), str(len), error.message]) actual = "\n".join([error_str(error) for error in errors]) @@ -181,11 +181,7 @@ class TestSamples(unittest.TestCase): def test_samples(self): # list the samples directory - samples = [ - f.stem - for f in Path(__file__).parent.glob("samples/*.blp") - if not f.stem.endswith("_dec") - ] + samples = [f.stem for f in Path(__file__).parent.glob("samples/*.blp")] samples.sort() for sample in samples: REQUIRE_ADW_1_4 = ["adw_breakpoint"] @@ -202,6 +198,7 @@ class TestSamples(unittest.TestCase): "parseable", "signal", "signal_not_swapped", + "signal_template_object", "template", "template_binding", "template_binding_extern", @@ -215,7 +212,7 @@ class TestSamples(unittest.TestCase): ] # Decompiler-only tests - SKIP_COMPILE = ["issue_177", "translator_comments"] + SKIP_COMPILE = ["issue_177", "issue_187", "translator_comments"] SKIP_DECOMPILE = [ # Comments are not preserved in either direction @@ -228,7 +225,7 @@ class TestSamples(unittest.TestCase): continue with self.subTest(sample): - if sample not in SKIP_COMPILE: + if sample not in SKIP_COMPILE and not sample.endswith("_dec"): self.assert_sample(sample, skip_run=sample in SKIP_RUN) with self.subTest("decompile/" + sample): diff --git a/tests/test_tokenizer.py b/tests/test_tokenizer.py index 2bca595..ad5f828 100644 --- a/tests/test_tokenizer.py +++ b/tests/test_tokenizer.py @@ -25,7 +25,7 @@ from blueprintcompiler.tokenizer import Token, TokenType, tokenize class TestTokenizer(unittest.TestCase): - def assert_tokenize(self, string: str, expect: [Token]): + def assert_tokenize(self, string: str, expect: list[Token]): try: tokens = tokenize(string) self.assertEqual(len(tokens), len(expect))