Compare commits

...

5 commits

Author SHA1 Message Date
James Westman
5e6a34bfbd Merge branch 'completion-improvements' into 'main'
Completion improvements

See merge request GNOME/blueprint-compiler!243
2025-05-03 16:38:05 +00:00
James Westman
d91cbef50c
completions: Add GtkScale mark positions 2025-05-03 10:14:10 -05:00
James Westman
d3c31447c9
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 10:10:06 -05:00
James Westman
69e05bba34
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 09:08:05 -05:00
James Westman
b7f176418f
completions: Add completions for response IDs 2025-05-03 08:46:28 -05:00
21 changed files with 165 additions and 68 deletions

View file

@ -76,9 +76,13 @@ def complete(
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
else:
while tokens[token_idx].type == TokenType.WHITESPACE:
idx = tokens[token_idx].start
token_idx -= 1
yield from _complete(lsp, ast_node, tokens, idx, token_idx, next_token)
@ -236,7 +240,7 @@ def property_completer(ctx: CompletionContext):
for prop_name, prop in ctx.ast_node.gir_class.properties.items():
yield get_property_completion(
prop_name,
prop,
prop.type,
ctx,
annotations.is_property_translated(prop),
prop.doc,
@ -245,7 +249,11 @@ def property_completer(ctx: CompletionContext):
@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(ctx: CompletionContext):
if isinstance(ctx.ast_node, language.Property):
@ -348,7 +356,7 @@ def signal_completer(ctx: CompletionContext):
.lower()
)
snippet = f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;"
snippet = f"{signal_name} => \\$${{1:{name}_{signal_name.replace('-', '_')}}}()$0;"
yield Completion(
signal_name,
@ -367,3 +375,52 @@ def template_completer(_ctx: CompletionContext):
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),
]
],
)
def complete_response_default(ctx: CompletionContext):
yield Completion(
"default",
kind=CompletionItemKind.Keyword,
)

View file

@ -68,10 +68,22 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None
# 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

View file

@ -111,6 +111,8 @@ class CompileError(PrintableError):
n_carets += line.count("\t", col_num, col_num + n_carets)
line = line.replace("\t", " ")
n_carets = max(n_carets, 1)
stream.write(
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
at {filename} line {line_num} column {col_num}:

View file

@ -34,9 +34,16 @@ 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 .response_id import ExtResponse
from .types import ClassName
from .ui import UI
from .values import (

View file

@ -140,7 +140,7 @@ 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(_ctx: CompletionContext):
@ -149,20 +149,6 @@ def complete_adw_message_dialog(_ctx: CompletionContext):
)
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Adw", "AlertDialog"),
matches=new_statement_patterns,
)
def complete_adw_alert_dialog(_ctx: CompletionContext):
yield Completion(
"responses",
CompletionItemKind.Keyword,
snippet="responses [\n\t$0\n]",
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "responses"),
)
@decompiler("responses")
def decompile_responses(ctx, gir):
ctx.print(f"responses [")

View file

@ -91,7 +91,7 @@ class ExtComboBoxItems(AstNode):
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "ComboBoxText"),
applies_in_subclass=[("Gtk", "ComboBoxText")],
matches=new_statement_patterns,
)
def items_completer(_ctx: CompletionContext):

View file

@ -98,7 +98,7 @@ 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(_ctx: CompletionContext):

View file

@ -90,7 +90,7 @@ class ExtLayout(AstNode):
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"),
applies_in_subclass=[("Gtk", "Widget")],
matches=new_statement_patterns,
)
def layout_completer(_ctx: CompletionContext):

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"),
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,7 +132,7 @@ class ExtScaleMarks(AstNode):
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Scale"),
applies_in_subclass=[("Gtk", "Scale")],
matches=new_statement_patterns,
)
def complete_marks(_ctx: CompletionContext):
@ -153,6 +151,23 @@ 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,7 +101,7 @@ 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(_ctx: CompletionContext):

View file

@ -72,7 +72,7 @@ class ExtStringListStrings(AstNode):
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "StringList"),
applies_in_subclass=[("Gtk", "StringList")],
matches=new_statement_patterns,
)
def strings_completer(_ctx: CompletionContext):

View file

@ -77,7 +77,7 @@ class ExtStyles(AstNode):
@completer(
applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"),
applies_in_subclass=[("Gtk", "Widget")],
matches=new_statement_patterns,
)
def style_completer(_ctx: CompletionContext):

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

@ -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,11 +277,11 @@ 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)
self.message, Range(start_token.end, start_token.end, 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,16 @@ 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:
ctx.errors.append(CompileError(f"Expected `{self.end}`", token.range))
else:
ctx.next_token()
return True
@ -405,7 +419,6 @@ class Until(ParseNode):
ctx.skip_unexpected_token()
except CompileError as e:
ctx.errors.append(e)
ctx.next_token()
return True

View file

@ -76,8 +76,8 @@ def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]:
def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
if idx == 0 or len(text) == 0:
return (0, 0)
line_num = text.count("\n", 0, idx) + 1
col_num = idx - text.rfind("\n", 0, idx) - 1
line_num = text.count("\n", 0, idx - 1) + 1
col_num = idx - text.rfind("\n", 0, idx - 1) - 1
return (line_num - 1, col_num)

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

@ -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
4,5,21,Attributes are not permitted at the top level of a menu

View file

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