diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 8f742e0..b0c5357 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -196,6 +196,13 @@ class AstNode: return None + def get_child_at(self, idx: int) -> "AstNode": + for child in self.children: + if idx in child.range: + return child.get_child_at(idx) + + return self + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: for child in self.children: yield from child.get_semantic_tokens() diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 65db3cd..55d719b 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -21,7 +21,16 @@ import typing as T from . import annotations, gir, language from .ast_utils import AstNode -from .completions_utils import * +from .completions_utils import ( + CompletionContext, + completers, + completer, + get_sort_key, + new_statement_patterns, + get_property_completion, + CompletionItemKind, + CompletionPriority, +) from .language.types import ClassName from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS @@ -38,13 +47,6 @@ def _complete( 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, next_token) - return - prev_tokens: T.List[Token] = [] # collect the 5 previous non-skipped tokens @@ -54,7 +56,7 @@ def _complete( prev_tokens.insert(0, token) token_idx -= 1 - for completer in ast_node.completers: + for completer in completers: yield from completer(prev_tokens, next_token, ast_node, lsp, idx) @@ -84,7 +86,17 @@ def complete( idx = tokens[token_idx].start token_idx -= 1 - yield from _complete(lsp, ast_node, tokens, idx, token_idx, next_token) + child_node = ast_node.get_child_at(idx) + # If the cursor is at the end of a node, completions should be for the next child of the parent, unless the node + # is incomplete. + while ( + child_node.range.end == idx + and not child_node.incomplete + and child_node.parent is not None + ): + child_node = child_node.parent + + yield from _complete(lsp, child_node, tokens, idx, token_idx, next_token) @completer([language.GtkDirective]) @@ -163,7 +175,13 @@ def _available_namespace_completions(ctx: CompletionContext): @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=new_statement_patterns, ) def namespace(ctx: CompletionContext): @@ -184,7 +202,13 @@ def namespace(ctx: CompletionContext): @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], @@ -195,7 +219,11 @@ def object_completer(ctx: CompletionContext): if ns is not None: for c in ns.classes.values(): snippet = c.name - if str(ctx.next_token) != "{": + if ( + str(ctx.next_token) != "{" + and not isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.BracketedTypeName) + ): snippet += " {\n $0\n}" yield Completion( @@ -209,7 +237,13 @@ def object_completer(ctx: CompletionContext): @completer( - applies_in=[language.UI, language.ObjectContent, language.Template], + applies_in=[ + language.UI, + language.ObjectContent, + language.Template, + language.TypeName, + language.BracketedTypeName, + ], matches=new_statement_patterns, ) def gtk_object_completer(ctx: CompletionContext): @@ -217,7 +251,11 @@ def gtk_object_completer(ctx: CompletionContext): if ns is not None: for c in ns.classes.values(): snippet = c.name - if str(ctx.next_token) != "{": + if ( + str(ctx.next_token) != "{" + and not isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.BracketedTypeName) + ): snippet += " {\n $0\n}" yield Completion( @@ -229,6 +267,17 @@ def gtk_object_completer(ctx: CompletionContext): detail=c.detail, ) + if isinstance(ctx.ast_node, language.BracketedTypeName) or ( + isinstance(ctx.ast_node, language.TypeName) + and not isinstance(ctx.ast_node, language.ClassName) + ): + for basic_type in gir.BASIC_TYPES: + yield Completion( + basic_type, + CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, basic_type), + ) + @completer( applies_in=[language.ObjectContent], diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 36399b1..d8bcbc2 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -57,14 +57,21 @@ new_statement_patterns = [ [(TokenType.PUNCTUATION, "}")], [(TokenType.PUNCTUATION, "]")], [(TokenType.PUNCTUATION, ";")], + [(TokenType.OP, "<")], ] +completers = [] + + def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def decorator(func: T.Callable[[CompletionContext], T.Generator[Completion]]): def inner( prev_tokens: T.List[Token], next_token: Token, ast_node, lsp, idx: int ): + if not any(isinstance(ast_node, rule) for rule in applies_in): + return + # For completers that apply in ObjectContent nodes, we can further # check that the object is the right class if applies_in_subclass is not None: @@ -118,8 +125,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None ) yield from func(context) - for c in applies_in: - c.completers.append(inner) + completers.append(inner) return inner return decorator diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 333f4ac..6392eb4 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -289,7 +289,7 @@ class TypeType(BasicType): return isinstance(other, TypeType) -_BASIC_TYPES = { +BASIC_TYPES = { "bool": BoolType, "string": StringType, "int": IntType, @@ -914,7 +914,7 @@ class Namespace(GirNode): def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: """Gets a type from this namespace by its C name.""" - for basic in _BASIC_TYPES.values(): + for basic in BASIC_TYPES.values(): if basic.glib_type_name == cname: return basic() @@ -1036,8 +1036,8 @@ class GirContext: return None def get_type(self, name: str, ns: str) -> T.Optional[GirType]: - if ns is None and name in _BASIC_TYPES: - return _BASIC_TYPES[name]() + if ns is None and name in BASIC_TYPES: + return BASIC_TYPES[name]() ns = ns or "Gtk" diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 7f59d96..88d7538 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -44,7 +44,7 @@ from .gtkbuilder_child import ( from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .response_id import ExtResponse -from .types import ClassName +from .types import BracketedTypeName, ClassName, TypeName from .ui import UI from .values import ( ArrayValue, diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index de6fbf1..910cd71 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -21,7 +21,7 @@ from ..decompiler import decompile_element from .common import * from .contexts import ScopeCtx, ValueTypeCtx -from .types import TypeName +from .types import BracketedTypeName, TypeName expr = Sequence() @@ -196,7 +196,7 @@ class CastExpr(InfixExpr): grammar = [ Keyword("as"), AnyOf( - ["<", TypeName, Match(">").expected()], + BracketedTypeName, [ UseExact("lparen", "("), TypeName, @@ -211,7 +211,13 @@ class CastExpr(InfixExpr): @property def type(self) -> T.Optional[GirType]: - return self.children[TypeName][0].gir_type + if len(self.children[BracketedTypeName]) == 1: + type_name = self.children[BracketedTypeName][0].type_name + return None if type_name is None else type_name.gir_type + elif len(self.children[TypeName]) == 1: + return self.children[TypeName][0].gir_type + else: + return None @validate() def cast_makes_sense(self): diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index fe45c4d..da41360 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -27,11 +27,11 @@ class TypeName(AstNode): [ UseIdent("namespace"), ".", - UseIdent("class_name"), + UseIdent("class_name").expected("class name"), ], [ AnyOf("$", [".", UseLiteral("old_extern", True)]), - UseIdent("class_name"), + UseIdent("class_name").expected("class name"), UseLiteral("extern", True), ], UseIdent("class_name"), @@ -47,7 +47,11 @@ class TypeName(AstNode): @validate("class_name") def type_exists(self): - if not self.tokens["extern"] and self.gir_ns is not None: + if ( + not self.tokens["extern"] + and self.gir_ns is not None + and self.tokens["class_name"] is not None + ): self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) @@ -182,3 +186,14 @@ class TemplateClassName(ClassName): self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) + + +class BracketedTypeName(AstNode): + grammar = Statement("<", to_parse_node(TypeName).expected("type name"), end=">") + + @property + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[TypeName]) == 0: + return None + + return self.children[TypeName][0] diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 7fa6bfa..2840337 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -26,7 +26,7 @@ from .common import * from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx from .expression import Expression from .gobject_object import Object -from .types import TypeName +from .types import BracketedTypeName, TypeName class Translated(AstNode): @@ -80,11 +80,7 @@ class TypeLiteral(AstNode): grammar = [ "typeof", AnyOf( - [ - "<", - to_parse_node(TypeName).expected("type name"), - Match(">").expected(), - ], + BracketedTypeName, [ UseExact("lparen", "("), to_parse_node(TypeName).expected("type name"), @@ -98,8 +94,13 @@ class TypeLiteral(AstNode): return gir.TypeType() @property - def type_name(self) -> TypeName: - return self.children[TypeName][0] + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[BracketedTypeName]) == 1: + return self.children[BracketedTypeName][0].type_name + elif len(self.children[TypeName]) == 1: + return self.children[TypeName][0] + else: + return None @validate() def validate_for_type(self) -> None: diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 15850f7..3f52ca5 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -209,6 +209,7 @@ class XmlOutput(OutputFormat): else: xml.put_text(self._object_id(value, value.ident)) elif isinstance(value, TypeLiteral): + assert value.type_name is not None xml.put_text(value.type_name.glib_type_name) else: if isinstance(value.value, float) and value.value == int(value.value): diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index 3924ee5..8bb4c66 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -280,9 +280,9 @@ class Err(ParseNode): start_idx -= 1 start_token = ctx.tokens[start_idx] - raise CompileError( - self.message, Range(start_token.end, start_token.end, ctx.text) - ) + position = start_token.start if ctx.start == start_idx else start_token.end + + raise CompileError(self.message, Range(position, position, ctx.text)) return True