Merge branch 'completion-improvements' into 'main'

Completion improvements

See merge request GNOME/blueprint-compiler!243
This commit is contained in:
James Westman 2025-05-03 16:38:05 +00:00
commit 5e6a34bfbd
30 changed files with 620 additions and 209 deletions

View file

@ -23,7 +23,7 @@ from . import annotations, gir, language
from .ast_utils import AstNode
from .completions_utils import *
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,13 +31,18 @@ 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)
yield from _complete(lsp, child, tokens, idx, token_idx, next_token)
return
prev_tokens: T.List[Token] = []
@ -50,7 +55,7 @@ def _complete(
token_idx -= 1
for completer in ast_node.completers:
yield from completer(prev_tokens, ast_node, lsp)
yield from completer(prev_tokens, next_token, ast_node, lsp, idx)
def complete(
@ -62,35 +67,121 @@ 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
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)
yield from _complete(lsp, ast_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],
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],
@ -99,14 +190,19 @@ def namespace(lsp, ast_node, match_variables):
[(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) != "{":
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,
)
@ -116,14 +212,19 @@ def object_completer(lsp, ast_node, match_variables):
applies_in=[language.UI, language.ObjectContent, language.Template],
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) != "{":
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,
)
@ -133,76 +234,42 @@ def gtk_object_completer(lsp, ast_node, match_variables):
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:
if isinstance(vt.value_type, gir.Enumeration):
for name, member in vt.value_type.members.items():
yield Completion(
@ -210,43 +277,150 @@ 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
):
yield Completion(
"null",
CompletionItemKind.Constant,
sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"),
)
for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items():
if obj.gir_class is not None and obj.gir_class.assignable_to(
vt.value_type
):
yield Completion(
id,
CompletionItemKind.Variable,
signature=" " + obj.signature,
sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id),
)
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),
]
],
)
def complete_response_default(ctx: CompletionContext):
yield Completion(
"default",
kind=CompletionItemKind.Keyword,
)

View file

@ -19,10 +19,39 @@
import typing as T
from dataclasses import dataclass
from enum import Enum
from .lsp_utils import Completion
from . import gir
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, "}")],
@ -32,15 +61,29 @@ new_statement_patterns = [
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
):
# 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 +109,51 @@ 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)
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,
)

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

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

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

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

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

View file

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

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

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

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