Compare commits

...

35 commits

Author SHA1 Message Date
James Westman
72319b29c6
completions: Don't suggest "null" where not allowed 2025-05-03 14:44:13 -05:00
James Westman
67983aee2e
completions: Object names in signal handlers 2025-05-03 14:39:02 -05:00
James Westman
1205fc42ea
completions: Fix completions in identifiers 2025-05-03 14:27:45 -05:00
James Westman
e9206809d6
completions: Add types in typeof<> and as<> 2025-05-03 14:27:45 -05:00
James Westman
d5b2ee3589
completions: Add GtkScale mark positions 2025-05-03 14:27:45 -05:00
James Westman
8f3ae9a626
parser: Tweak parsing during error conditions
When an explicit parsing error is encountered and a CompileError raised,
apply the changes to the context state. This way, the rule that catches
the exception (e.g. Statement or Until) knows where the error occurred.

Also, changed "Expected" errors to be reported at the end of the
previous non-whitespace token.
2025-05-03 14:27:45 -05:00
James Westman
e5d6910626
completions: Fix generated signal handler name
A syntax error in the snippet caused the generated signal handler name
not to be used.
2025-05-03 14:27:44 -05:00
James Westman
b26433d865
completions: Add completions for response IDs 2025-05-03 14:27:44 -05:00
James Westman
866092ccf7
values: Don't allow assigning true/false to object
Fix a bug in the type checking code where it would not produce an error
if you assigned "true" or "false" to an object property.
2025-05-03 08:27:29 -05:00
James Westman
1e9b01bab9
gtk_a11y: Add new state and property 2025-05-03 08:27:29 -05:00
James Westman
b9910db849
completions: Improve accessibility properties 2025-05-03 08:27:29 -05:00
James Westman
d0394136cf
completions: Sort completion items 2025-05-03 08:27:29 -05:00
James Westman
860580e560
completions: Add object value completions
For object properties, add completions for named objects in the
blueprint and for matching classes in imported namespaces. Also add null
and bind.
2025-05-03 08:27:29 -05:00
James Westman
64b96137f5
completions: Add completer for import statements 2025-05-03 08:27:29 -05:00
James Westman
f5cef37db8
completions: Add translation-domain completer 2025-05-03 08:27:28 -05:00
James Westman
3d0593bc2b
completions: Complete available namespaces
Add completions for namespaces in the typelib path that can be imported.
Accepting the completion automatically adds an import statement.
2025-05-03 08:27:28 -05:00
James Westman
bf4d8579b6
lsp: Fix completions when editing existing item
Many completion snippets insert more than just the name. For example,
the object completer inserts the braces and places your cursor inside
them automatically, to save some typing. However, if you're changing the
class of an existing object, this isn't what you want. Changed so that
if the next token is '{', only the name is inserted.

Made similar changes to the property and signal completers.
2025-05-03 08:27:28 -05:00
James Westman
2e42dc6848
decompiler: Fix bug in signals with template object
If a signal handler had the template as its object, the decompiler would
output the class name instead of the 'template' keyword.
2025-05-03 07:46:34 -05:00
James Westman
a12d3f5c81
decompile: Fix bug in lookup tags
A lookup tag with no type attribute would crash the decompiler, even if
that was valid. This wasn't caught by the tests since blueprint never
generates such XML.

Also fixed a bug in the tests that caused decompiler-only tests not to
run.
2025-04-25 20:13:01 -05:00
James Westman
a83c7e936d
black: Update formatting 2025-04-25 18:32:33 -05:00
James Westman
3816f4fe8d
Add .doap file 2025-04-25 18:29:55 -05:00
James Westman
e9d61cb6f9
Update URLs after move to GNOME namespace on GitLab 2025-04-25 18:29:55 -05:00
James Westman
6a77bfee0a
tests: Fix typing 2025-04-19 13:27:20 -05:00
James Westman
f50b898e4c adw_breakpoint: Fix crash in language server
Fix a crash that happened when an AdwBreakpointSetter rule was
incomplete, such as when you're still typing it. Fixes #189.
2025-04-01 19:27:59 -05:00
Tom Greig
cc09f3d3bb Add tests for nested templates
Basically just a copy of the list_factory test, but with an extra copy
of the list factory inside it.
2025-03-30 10:27:11 +01:00
Tom Greig
f93d5d2acd Handle nested CDATA from nested templates
When putting CDATA into the output, any instances of ']]>' in the text
are replaced with ']]]]><![CDATA[>'.  This allows nested templates, e.g.
from list views inside list views to work properly.
2025-03-28 20:53:03 +00:00
Sertonix
394014429e Sort keys in collect-sections.py
This makes sure that the reference_docs.json file
is build reproducible.

Ref https://reproducible-builds.org/
2025-03-24 22:58:43 +00:00
Chris Mayo
a4e0c3701b docs: Update overview example using format and compile 2025-03-21 01:01:24 +00:00
kotontrion
c1fbcef6d0 Merge branch blueprint-compiler:main into main 2025-03-02 15:26:42 +00:00
James Westman
404ae76787
Update MAINTENANCE.md 2025-01-17 17:25:21 -06:00
James Westman
04ef0944db
Release v0.16.0 2025-01-17 17:04:52 -06:00
James Westman
aa13c8f5af
Warn about single-quoted translated strings
gettext only recognizes double quoted strings
2025-01-05 14:27:59 -06:00
kotontrion
e07da3c339 flags: use nick instead of name 2024-12-18 17:46:26 +00:00
kotontrion
2ae41020ab Fix flag return value type 2024-12-18 17:46:26 +00:00
kotontrion
f48b840cfa compile: fix flag values
gtk builder does not support combining interger values with | in flags
properties, so the short names are used instead.
2024-12-18 17:46:26 +00:00
65 changed files with 1078 additions and 307 deletions

View file

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

View file

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

32
NEWS.md
View file

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

27
blueprint-compiler.doap Normal file
View file

@ -0,0 +1,27 @@
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#">
<name xml:lang="en">Blueprint</name>
<shortdesc xml:lang="en">A modern language for creating GTK interfaces</shortdesc>
<description xml:lang="en">Blueprint is a language and associated tooling for building user interfaces for GTK.</description>
<category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
<programming-language>Python</programming-language>
<homepage
rdf:resource="https://gnome.gitlab.gnome.org/blueprint-compiler/" />
<download-page
rdf:resource="https://gitlab.gnome.org/GNOME/blueprint-compiler/-/releases" />
<bug-database
rdf:resource="https://gitlab.gnome.org/GNOME/blueprint-compiler/issues" />
<maintainer>
<foaf:Person>
<foaf:name>James Westman</foaf:name>
<foaf:mbox rdf:resource="mailto:james@jwestman.net" />
<gnome:userid>jwestman</gnome:userid>
</foaf:Person>
</maintainer>
</Project>

View file

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

View file

@ -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(
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,
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,
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):
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:
if not isinstance(ctx.ast_node.parent, language.Object):
name = "on"
else:
name = "on_" + (
ast_node.parent.children[ClassName][0].tokens["id"]
or ast_node.parent.children[ClassName][0]
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,
)

View file

@ -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]
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])
)
if not ast_node.gir_class or not ast_node.gir_class.assignable_to(type):
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),
)

View file

@ -255,7 +255,11 @@ def decompile_element(
ctx._node_stack.append(xml)
ctx.start_block()
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:

View file

@ -92,13 +92,16 @@ 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)
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
col_num += 1
end_col_num += 1
n_spaces = col_num - 1
n_carets = (
@ -111,10 +114,16 @@ class CompileError(PrintableError):
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}"""
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,6 +34,7 @@ from ..errors import (
CompileError,
CompileWarning,
DeprecatedWarning,
ErrorReference,
MultipleErrors,
UnusedWarning,
UpgradeWarning,

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>
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 <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>
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 <https://docs.gtk.org/gtk4/enum.AccessibleState.html>
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),
)

View file

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

View file

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

View file

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

View file

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

View file

@ -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"),
UseNumber("value").expected("value"),
Optional(
[
",",
UseIdent("position"),
Optional([",", StringValue]),
UseIdent("position").expected("position"),
Optional([",", to_parse_node(StringValue).expected("label")]),
]
),
],
Match(")").expected(),
]
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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -73,6 +73,7 @@ class XmlEmitter:
self._needs_newline = False
def put_cdata(self, text: str):
text = text.replace("]]>", "]]]]><![CDATA[>")
self.result += f"<![CDATA[{text}]]>"
self._needs_newline = False

View file

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

View file

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

View file

@ -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"
}
]
}

View file

@ -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 <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
- `Source code <https://gitlab.gnome.org/GNOME/blueprint-compiler>`_
- `Workbench <https://github.com/sonnyp/Workbench>`_ lets you try, preview and export Blueprint
- `GNOME Builder <https://developer.gnome.org/documentation/introduction/builder.html>`_ provides builtin support
- `Vim syntax highlighting plugin by thetek42 <https://github.com/thetek42/vim-blueprint-syntax>`_

View file

@ -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() <https://docs.gtk.org/gtk4/vfunc.Buildable.custom_tag_start.html>`_ 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 <https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues>`_.
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 <https://gitlab.gnome.org/GNOME/blueprint-compiler/-/issues>`_.
.. rst-class:: grammar-block

View file

@ -8,7 +8,7 @@ Setting up Blueprint on a new or existing project
Using the porting tool
~~~~~~~~~~~~~~~~~~~~~~
Clone `blueprint-compiler <https://gitlab.gnome.org/jwestman/blueprint-compiler>`_
Clone `blueprint-compiler <https://gitlab.gnome.org/GNOME/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

View file

@ -24,6 +24,8 @@ If you're using Meson's `i18n module <https://mesonbuild.com/i18n-module.html#i1
i18n.gettext('package name', preset: 'glib')
You must use double quotes for the translated strings in order for gettext to recognize them. Newer versions of blueprint will warn you if you use single quotes.
Contexts
--------

View file

@ -1,5 +1,5 @@
project('blueprint-compiler',
version: '0.14.0',
version: '0.16.0',
)
prefix = get_option('prefix')

View file

@ -1 +1 @@
4,6,22,Action widget must have ID
4,5,24,Action widget must have ID

View file

@ -1 +1 @@
4,6,18,Gtk.Box doesn't have action widgets
4,5,20,Gtk.Box doesn't have action widgets

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Button {
child: false;
}

View file

@ -0,0 +1 @@
4,10,5,Cannot assign boolean to Gtk.Widget

View file

@ -1 +1 @@
1,0,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)
1,1,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)

View file

@ -1 +1 @@
6,1,1,Expected `;`
5,4,0,Expected `;`

View file

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

View file

@ -1,2 +1 @@
4,5,21,Attributes are not permitted at the top level of a menu
4,16,10,Unexpected tokens

View file

@ -1 +1 @@
1,11,0,Expected a version number for GTK
1,10,0,Expected a version number for GTK

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: _('Hello, World!');
}

View file

@ -0,0 +1 @@
4,12,15,gettext may not recognize single-quoted strings

View file

@ -7,7 +7,7 @@ corresponding .blp file and regenerate this file with blueprint-compiler.
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GApplication">
<property name="flags">1|4</property>
<property name="flags">is-service|handles-open</property>
</object>
<object class="GtkEventControllerScroll">
<property name="flags">1</property>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="GtkListItem">
<property name="child">
<object class="GtkLabel">
<binding name="label">
<lookup type="RecentObject" name="filename">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</property>
</template>
</interface>

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
template ListItem {
child: Label {
label: bind template.item as <$RecentObject>.filename;
};
}

View file

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

View file

@ -0,0 +1,44 @@
<?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="GtkListView">
<property name="factory">
<object class="GtkBuilderListItemFactory" id="list_item_factory">
<property name="bytes"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="child">
<object class="GtkListView">
<property name="factory">
<object class="GtkBuilderListItemFactory" id="list_item_factory">
<property name="bytes"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="child">
<object class="GtkLabel">
<binding name="label">
<lookup name="name" type="MyObject">
<lookup name="item" type="GtkListItem">
<constant>GtkListItem</constant>
</lookup>
</lookup>
</binding>
</object>
</property>
</template>
</interface>]]]]><![CDATA[></property>
</object>
</property>
</object>
</property>
</template>
</interface>]]></property>
</object>
</property>
</object>
</interface>

View file

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

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
template $MyTemplate {
Button {
clicked => $my_signal_handler(template);
}
}

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"/>
<template class="MyTemplate">
<child>
<object class="GtkButton">
<signal name="clicked" handler="my_signal_handler" object="MyTemplate"/>
</object>
</child>
</template>
</interface>

View file

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

View file

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