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 .ast_utils import AstNode
from .completions_utils import * from .completions_utils import *
from .language.types import ClassName 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 .parser import SKIP_TOKENS
from .tokenizer import Token, TokenType from .tokenizer import Token, TokenType
@ -31,13 +31,18 @@ Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
def _complete( 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]: ) -> T.Iterator[Completion]:
for child in ast_node.children: for child in ast_node.children:
if child.group.start <= idx and ( if child.group.start <= idx and (
idx < child.group.end or (idx == child.group.end and child.incomplete) 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 return
prev_tokens: T.List[Token] = [] prev_tokens: T.List[Token] = []
@ -50,7 +55,7 @@ def _complete(
token_idx -= 1 token_idx -= 1
for completer in ast_node.completers: 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( def complete(
@ -62,35 +67,121 @@ def complete(
if token.start < idx <= token.end: if token.start < idx <= token.end:
token_idx = i 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 # 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 idx = tokens[token_idx].start
token_idx -= 1 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]) @completer([language.GtkDirective])
def using_gtk(lsp, ast_node, match_variables): def using_gtk(_ctx: CompletionContext):
yield Completion( yield Completion(
"using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n" "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( @completer(
applies_in=[language.UI, language.ObjectContent, language.Template], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def namespace(lsp, ast_node, match_variables): def namespace(ctx: CompletionContext):
yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") 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: if ns.gir_namespace is not None:
yield Completion( yield Completion(
ns.gir_namespace.name, ns.gir_namespace.name,
CompletionItemKind.Module, CompletionItemKind.Module,
text=ns.gir_namespace.name + ".", text=ns.gir_namespace.name + ".",
sort_text=get_sort_key(
CompletionPriority.NAMESPACE, ns.gir_namespace.name
),
) )
yield from _available_namespace_completions(ctx)
@completer( @completer(
applies_in=[language.UI, language.ObjectContent, language.Template], applies_in=[language.UI, language.ObjectContent, language.Template],
@ -99,14 +190,19 @@ def namespace(lsp, ast_node, match_variables):
[(TokenType.IDENT, None), (TokenType.OP, ".")], [(TokenType.IDENT, None), (TokenType.OP, ".")],
], ],
) )
def object_completer(lsp, ast_node, match_variables): def object_completer(ctx: CompletionContext):
ns = ast_node.root.gir.namespaces.get(match_variables[0]) ns = ctx.ast_node.root.gir.namespaces.get(ctx.match_variables[0])
if ns is not None: if ns is not None:
for c in ns.classes.values(): for c in ns.classes.values():
snippet = c.name
if str(ctx.next_token) != "{":
snippet += " {\n $0\n}"
yield Completion( yield Completion(
c.name, c.name,
CompletionItemKind.Class, CompletionItemKind.Class,
snippet=f"{c.name} {{\n $0\n}}", sort_text=get_sort_key(CompletionPriority.CLASS, c.name),
snippet=snippet,
docs=c.doc, docs=c.doc,
detail=c.detail, detail=c.detail,
) )
@ -116,14 +212,19 @@ def object_completer(lsp, ast_node, match_variables):
applies_in=[language.UI, language.ObjectContent, language.Template], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def gtk_object_completer(lsp, ast_node, match_variables): def gtk_object_completer(ctx: CompletionContext):
ns = ast_node.root.gir.namespaces.get("Gtk") ns = ctx.ast_node.root.gir.namespaces.get("Gtk")
if ns is not None: if ns is not None:
for c in ns.classes.values(): for c in ns.classes.values():
snippet = c.name
if str(ctx.next_token) != "{":
snippet += " {\n $0\n}"
yield Completion( yield Completion(
c.name, c.name,
CompletionItemKind.Class, CompletionItemKind.Class,
snippet=f"{c.name} {{\n $0\n}}", sort_text=get_sort_key(CompletionPriority.CLASS, c.name),
snippet=snippet,
docs=c.doc, docs=c.doc,
detail=c.detail, detail=c.detail,
) )
@ -133,76 +234,42 @@ def gtk_object_completer(lsp, ast_node, match_variables):
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def property_completer(lsp, ast_node, match_variables): def property_completer(ctx: CompletionContext):
if ast_node.gir_class and hasattr(ast_node.gir_class, "properties"): assert isinstance(ctx.ast_node, language.ObjectContent)
for prop_name, prop in ast_node.gir_class.properties.items(): if ctx.ast_node.gir_class and hasattr(ctx.ast_node.gir_class, "properties"):
if ( for prop_name, prop in ctx.ast_node.gir_class.properties.items():
isinstance(prop.type, gir.BoolType) yield get_property_completion(
and lsp.client_supports_completion_choice
):
yield Completion(
prop_name, prop_name,
CompletionItemKind.Property, prop.type,
sort_text=f"0 {prop_name}", ctx,
snippet=f"{prop_name}: ${{1|true,false|}};", annotations.is_property_translated(prop),
docs=prop.doc, 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,
) )
@completer( @completer(
applies_in=[language.Property, language.A11yProperty], 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): def prop_value_completer(ctx: CompletionContext):
if (vt := ast_node.value_type) is not None: 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): if isinstance(vt.value_type, gir.Enumeration):
for name, member in vt.value_type.members.items(): for name, member in vt.value_type.members.items():
yield Completion( yield Completion(
@ -210,43 +277,150 @@ def prop_value_completer(lsp, ast_node, match_variables):
CompletionItemKind.EnumMember, CompletionItemKind.EnumMember,
docs=member.doc, docs=member.doc,
detail=member.detail, detail=member.detail,
sort_text=get_sort_key(CompletionPriority.ENUM_MEMBER, name),
) )
elif isinstance(vt.value_type, gir.BoolType): elif isinstance(vt.value_type, gir.BoolType):
yield Completion("true", CompletionItemKind.Constant) yield Completion(
yield Completion("false", CompletionItemKind.Constant) "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( @completer(
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def signal_completer(lsp, ast_node, match_variables): def signal_completer(ctx: CompletionContext):
if ast_node.gir_class and hasattr(ast_node.gir_class, "signals"): assert isinstance(ctx.ast_node, language.ObjectContent)
for signal_name, signal in ast_node.gir_class.signals.items():
if not isinstance(ast_node.parent, language.Object): 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" name = "on"
else: else:
name = "on_" + ( name = "on_" + (
ast_node.parent.children[ClassName][0].tokens["id"] ctx.ast_node.parent.children[ClassName][0].tokens["id"]
or ast_node.parent.children[ClassName][0] or ctx.ast_node.parent.children[ClassName][0]
.tokens["class_name"] .tokens["class_name"]
.lower() .lower()
) )
snippet = f"{signal_name} => \\$${{1:{name}_{signal_name.replace('-', '_')}}}()$0;"
yield Completion( yield Completion(
signal_name, signal_name,
CompletionItemKind.Event, CompletionItemKind.Event,
sort_text=f"1 {signal_name}", sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, signal_name),
snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;", snippet=snippet,
docs=signal.doc, docs=signal.doc,
detail=signal.detail, detail=signal.detail,
) )
@completer(applies_in=[language.UI], matches=new_statement_patterns) @completer(applies_in=[language.UI], matches=new_statement_patterns)
def template_completer(lsp, ast_node, match_variables): def template_completer(_ctx: CompletionContext):
yield Completion( yield Completion(
"template", "template",
CompletionItemKind.Snippet, CompletionItemKind.Snippet,
snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}", 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 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 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 = [ new_statement_patterns = [
[(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "{")],
[(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 completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None):
def decorator(func): def decorator(func: T.Callable[[CompletionContext], T.Generator[Completion]]):
def inner(prev_tokens: T.List[Token], ast_node, lsp): 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 # For completers that apply in ObjectContent nodes, we can further
# check that the object is the right class # check that the object is the right class
if applies_in_subclass is not None: if applies_in_subclass is not None:
type = ast_node.root.gir.get_type( parent_obj = ast_node
applies_in_subclass[1], applies_in_subclass[0] 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 return
any_match = len(matches) == 0 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: if not any_match:
return 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: for c in applies_in:
c.completers.append(inner) c.completers.append(inner)
return inner return inner
return decorator 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) n_carets += line.count("\t", col_num, col_num + n_carets)
line = line.replace("\t", " ") line = line.replace("\t", " ")
n_carets = max(n_carets, 1)
stream.write( stream.write(
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
at {filename} line {line_num} column {col_num}: 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_size_group import ExtSizeGroupWidgets
from .gtk_string_list import ExtStringListStrings from .gtk_string_list import ExtStringListStrings
from .gtk_styles import ExtStyles 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 .gtkbuilder_template import Template
from .imports import GtkDirective, Import from .imports import GtkDirective, Import
from .response_id import ExtResponse
from .types import ClassName from .types import ClassName
from .ui import UI from .ui import UI
from .values import ( from .values import (

View file

@ -140,21 +140,10 @@ class ExtAdwResponseDialog(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Adw", "MessageDialog"), applies_in_subclass=[("Adw", "AlertDialog"), ("Adw", "MessageDialog")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def complete_adw_message_dialog(lsp, ast_node, match_variables): def complete_adw_message_dialog(_ctx: CompletionContext):
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):
yield Completion( yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" "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): class Property(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) UseIdent("name"),
":",
AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue).expected(
"property value"
),
) )
@property @property

View file

@ -25,12 +25,13 @@ from .gobject_object import ObjectContent, validate_parent_type
from .values import Value 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> # from <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>
return { return {
"autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"),
"description": StringType(), "description": StringType(),
"has-popup": BoolType(), "has-popup": BoolType(),
"help-text": StringType(),
"key-shortcuts": StringType(), "key-shortcuts": StringType(),
"label": StringType(), "label": StringType(),
"level": IntType(), "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> # from <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>
widget = gir.get_type("Widget", "Gtk") widget = gir.get_type("Widget", "Gtk")
return { 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> # from <https://docs.gtk.org/gtk4/enum.AccessibleState.html>
return { return {
"busy": BoolType(), "busy": BoolType(),
@ -86,9 +87,24 @@ def get_state_types(gir):
"invalid": gir.get_type("AccessibleInvalidState", "Gtk"), "invalid": gir.get_type("AccessibleInvalidState", "Gtk"),
"pressed": gir.get_type("AccessibleTristate", "Gtk"), "pressed": gir.get_type("AccessibleTristate", "Gtk"),
"selected": BoolType(), "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): def get_types(gir):
return { return {
**get_property_types(gir), **get_property_types(gir),
@ -121,7 +137,9 @@ class A11yProperty(AstNode):
grammar = Statement( grammar = Statement(
UseIdent("name"), UseIdent("name"),
":", ":",
AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]), AnyOf(
Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]
).expected("value"),
) )
@property @property
@ -232,9 +250,12 @@ class ExtAccessibility(AstNode):
applies_in=[ObjectContent], applies_in=[ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def a11y_completer(lsp, ast_node, match_variables): def a11y_completer(_ctx: CompletionContext):
yield Completion( 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], applies_in=[ExtAccessibility],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def a11y_name_completer(lsp, ast_node, match_variables): def a11y_property_completer(ctx: CompletionContext):
for name, type in get_types(ast_node.root.gir).items(): for name, type in get_types(ctx.ast_node.root.gir).items():
yield Completion( yield get_property_completion(
name, name,
CompletionItemKind.Property, type,
docs=_get_docs(ast_node.root.gir, type.name), ctx,
name in TRANSLATED,
_get_docs(ctx.ast_node.root.gir, name),
) )

View file

@ -91,11 +91,16 @@ class ExtComboBoxItems(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "ComboBoxText"), applies_in_subclass=[("Gtk", "ComboBoxText")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def items_completer(lsp, ast_node, match_variables): def items_completer(_ctx: CompletionContext):
yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") yield Completion(
"items",
CompletionItemKind.Snippet,
snippet="items [$0]",
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "items"),
)
@decompiler("items", parent_type="Gtk.ComboBoxText") @decompiler("items", parent_type="Gtk.ComboBoxText")

View file

@ -98,15 +98,28 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix")
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "FileFilter"), applies_in_subclass=[("Gtk", "FileFilter")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def file_filter_completer(lsp, ast_node, match_variables): def file_filter_completer(_ctx: CompletionContext):
yield Completion( 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") @decompiler("mime-types")

View file

@ -90,11 +90,16 @@ class ExtLayout(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=[("Gtk", "Widget")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def layout_completer(lsp, ast_node, match_variables): def layout_completer(_ctx: CompletionContext):
yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") yield Completion(
"layout",
CompletionItemKind.Snippet,
snippet="layout {\n $0\n}",
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "layout"),
)
@decompiler("layout") @decompiler("layout")

View file

@ -243,7 +243,7 @@ from .ui import UI
applies_in=[UI], applies_in=[UI],
matches=new_statement_patterns, 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}") 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], applies_in=[Menu],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def menu_content_completer(lsp, ast_node, match_variables): def menu_content_completer(_ctx: CompletionContext):
yield Completion( 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( 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( yield Completion(
"item (shorthand)", "item (shorthand)",
CompletionItemKind.Snippet, CompletionItemKind.Snippet,
snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', 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(
yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') "label",
yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') 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") @decompiler("menu")

View file

@ -23,22 +23,20 @@ from .values import StringValue
class ExtScaleMark(AstNode): class ExtScaleMark(AstNode):
grammar = [ grammar = Statement(
Keyword("mark"), Keyword("mark"),
Match("(").expected(), Match("(").expected(),
[
Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))),
UseNumber("value"), UseNumber("value").expected("value"),
Optional( Optional(
[ [
",", ",",
UseIdent("position"), UseIdent("position").expected("position"),
Optional([",", StringValue]), Optional([",", to_parse_node(StringValue).expected("label")]),
] ]
), ),
], end=")",
Match(")").expected(), )
]
@property @property
def value(self) -> float: def value(self) -> float:
@ -134,20 +132,42 @@ class ExtScaleMarks(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Scale"), applies_in_subclass=[("Gtk", "Scale")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def complete_marks(lsp, ast_node, match_variables): def complete_marks(_ctx: CompletionContext):
yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") yield Completion(
"marks",
CompletionItemKind.Keyword,
snippet="marks [\n\t$0\n]",
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "marks"),
)
@completer( @completer(
applies_in=[ExtScaleMarks], applies_in=[ExtScaleMarks],
) )
def complete_mark(lsp, ast_node, match_variables): def complete_mark(_ctx: CompletionContext):
yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") 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") @decompiler("marks")
def decompile_marks( def decompile_marks(
ctx, ctx,

View file

@ -101,11 +101,16 @@ class ExtSizeGroupWidgets(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "SizeGroup"), applies_in_subclass=[("Gtk", "SizeGroup")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def size_group_completer(lsp, ast_node, match_variables): def size_group_completer(_ctx: CompletionContext):
yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") yield Completion(
"widgets",
CompletionItemKind.Snippet,
snippet="widgets [$0]",
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "widgets"),
)
@decompiler("widgets") @decompiler("widgets")

View file

@ -72,11 +72,16 @@ class ExtStringListStrings(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "StringList"), applies_in_subclass=[("Gtk", "StringList")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def strings_completer(lsp, ast_node, match_variables): def strings_completer(_ctx: CompletionContext):
yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") yield Completion(
"strings",
CompletionItemKind.Snippet,
snippet="strings [$0]",
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "strings"),
)
@decompiler("items", parent_type="Gtk.StringList") @decompiler("items", parent_type="Gtk.StringList")

View file

@ -77,11 +77,16 @@ class ExtStyles(AstNode):
@completer( @completer(
applies_in=[ObjectContent], applies_in=[ObjectContent],
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=[("Gtk", "Widget")],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def style_completer(lsp, ast_node, match_variables): def style_completer(_ctx: CompletionContext):
yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') yield Completion(
"styles",
CompletionItemKind.Keyword,
snippet='styles ["$0"]',
sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "styles"),
)
@decompiler("style") @decompiler("style")

View file

@ -31,7 +31,12 @@ ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [
class ChildInternal(AstNode): class ChildInternal(AstNode):
grammar = ["internal-child", UseIdent("internal_child")] grammar = [
"[",
"internal-child",
UseIdent("internal_child").expected("internal child name"),
Match("]").expected(),
]
@property @property
def internal_child(self) -> str: def internal_child(self) -> str:
@ -39,7 +44,7 @@ class ChildInternal(AstNode):
class ChildType(AstNode): class ChildType(AstNode):
grammar = UseIdent("child_type").expected("a child type") grammar = ["[", UseIdent("child_type").expected("a child type"), "]"]
@property @property
def child_type(self) -> str: def child_type(self) -> str:
@ -59,7 +64,7 @@ class ChildExtension(AstNode):
class ChildAnnotation(AstNode): class ChildAnnotation(AstNode):
grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] grammar = AnyOf(ChildInternal, ChildExtension, ChildType)
@property @property
def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]: 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")] ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")]
grammar = [ grammar = Statement(
"[",
Keyword("action"), Keyword("action"),
Keyword("response"), Keyword("response"),
"=", Match("=").expected(),
AnyOf( AnyOf(
UseIdent("response_id"), UseIdent("response_id"),
[ [
Optional(UseExact("sign", "-")), Optional(UseExact("sign", "-")),
UseNumber("response_id"), UseNumber("response_id"),
], ],
), ).expected("response ID"),
Optional([Keyword("default"), UseLiteral("is_default", True)]), Optional([Keyword("default"), UseLiteral("is_default", True)]),
] end="]",
)
@validate() @validate()
def parent_has_action_widgets(self) -> None: 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 and self.template.class_name.glib_type_name == id
) )
def import_code_action(self, ns: str, version: str) -> CodeAction: def import_range(self, ns: str):
if len(self.children[Import]): """Returns a range to insert a new import statement"""
pos = self.children[Import][-1].range.end
else:
pos = self.children[GtkDirective][0].range.end 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( return CodeAction(
f"Import {ns} {version}", f"Import {ns} {version}",
f"\nusing {ns} {version};", f"\nusing {ns} {version};",
Range(pos, pos, self.group.text), self.import_range(ns),
) )
@cached_property @cached_property

View file

@ -338,7 +338,14 @@ class IdentLiteral(AstNode):
raise CompileError( raise CompileError(
'"item" can only be used in an expression literal' '"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( raise CompileError(
f"Could not find object with ID {self.ident}", f"Could not find object with ID {self.ident}",
did_you_mean=( did_you_mean=(

View file

@ -87,6 +87,7 @@ class Completion:
text: T.Optional[str] = None text: T.Optional[str] = None
snippet: T.Optional[str] = None snippet: T.Optional[str] = None
detail: T.Optional[str] = None detail: T.Optional[str] = None
additional_text_edits: T.Optional[T.List["TextEdit"]] = None
def to_json(self, snippets: bool): def to_json(self, snippets: bool):
insert_text = self.text or self.label insert_text = self.text or self.label
@ -114,6 +115,11 @@ class Completion:
"insertText": insert_text, "insertText": insert_text,
"insertTextFormat": insert_text_format, "insertTextFormat": insert_text_format,
"detail": self.detail if self.detail else None, "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} 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 start_idx = ctx.index
inner_ctx = ctx.create_child() 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) ctx.apply_child(inner_ctx)
if ctx.index == start_idx: if ctx.index == start_idx:
return ParseResult.EMPTY return ParseResult.EMPTY
@ -269,11 +277,11 @@ class Err(ParseNode):
if self.child.parse(ctx).failed(): if self.child.parse(ctx).failed():
start_idx = ctx.start start_idx = ctx.start
while ctx.tokens[start_idx].type in SKIP_TOKENS: while ctx.tokens[start_idx].type in SKIP_TOKENS:
start_idx += 1 start_idx -= 1
start_token = ctx.tokens[start_idx] start_token = ctx.tokens[start_idx]
raise CompileError( 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 return True
@ -329,8 +337,9 @@ class Statement(ParseNode):
"""ParseNode that attempts to match all of its children in sequence. If any """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.""" 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.children = [to_parse_node(child) for child in children]
self.end = end
def _parse(self, ctx) -> bool: def _parse(self, ctx) -> bool:
for child in self.children: for child in self.children:
@ -340,11 +349,16 @@ class Statement(ParseNode):
except CompileError as e: except CompileError as e:
ctx.errors.append(e) ctx.errors.append(e)
ctx.set_group_incomplete() ctx.set_group_incomplete()
token = ctx.peek_token()
if str(token) == self.end:
ctx.next_token()
return True return True
token = ctx.peek_token() token = ctx.peek_token()
if str(token) != ";": if str(token) != self.end:
ctx.errors.append(CompileError("Expected `;`", token.range)) ctx.errors.append(CompileError(f"Expected `{self.end}`", token.range))
else: else:
ctx.next_token() ctx.next_token()
return True return True
@ -405,7 +419,6 @@ class Until(ParseNode):
ctx.skip_unexpected_token() ctx.skip_unexpected_token()
except CompileError as e: except CompileError as e:
ctx.errors.append(e) ctx.errors.append(e)
ctx.next_token()
return True 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]: def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]:
if idx == 0 or len(text) == 0: if idx == 0 or len(text) == 0:
return (0, 0) return (0, 0)
line_num = text.count("\n", 0, idx) + 1 line_num = text.count("\n", 0, idx - 1) + 1
col_num = idx - text.rfind("\n", 0, idx) - 1 col_num = idx - text.rfind("\n", 0, idx - 1) - 1
return (line_num - 1, col_num) 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,11,0,Expected a signal detail name
4,9,3,Unexpected tokens

View file

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

@ -64,11 +64,11 @@ class TestSamples(unittest.TestCase):
def assert_ast_doesnt_crash(self, text, tokens, ast: AstNode): def assert_ast_doesnt_crash(self, text, tokens, ast: AstNode):
lsp = LanguageServer() lsp = LanguageServer()
for i in range(len(text)): for i in range(len(text) + 1):
ast.get_docs(i) ast.get_docs(i)
for i in range(len(text)): for i in range(len(text) + 1):
list(complete(lsp, ast, tokens, i)) 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_reference(i)
ast.get_document_symbols() ast.get_document_symbols()