diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index b0c5357..8f742e0 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -196,13 +196,6 @@ 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 81dd03d..5964f31 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -21,18 +21,7 @@ import typing as T from . import annotations, gir, language from .ast_utils import AstNode -from .completions_utils import ( - CompletionContext, - CompletionItemKind, - CompletionPriority, - completer, - completers, - get_object_id_completions, - get_property_completion, - get_sort_key, - new_statement_patterns, -) -from .language.contexts import ValueTypeCtx +from .completions_utils import * from .language.types import ClassName from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS @@ -49,6 +38,13 @@ 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 @@ -58,7 +54,7 @@ def _complete( prev_tokens.insert(0, token) token_idx -= 1 - for completer in completers: + for completer in ast_node.completers: yield from completer(prev_tokens, next_token, ast_node, lsp, idx) @@ -83,22 +79,12 @@ def complete( 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 - while tokens[token_idx].type == TokenType.WHITESPACE: - idx = tokens[token_idx].start - token_idx -= 1 - - child_node = ast_node.get_child_at(idx) - # If the cursor is at the end of a node, completions should be for the next child of the parent, unless the node - # is incomplete. - while ( - child_node.range.end == idx - and not child_node.incomplete - and child_node.parent is not None - ): - child_node = child_node.parent - - yield from _complete(lsp, child_node, tokens, idx, token_idx, next_token) + yield from _complete(lsp, ast_node, tokens, idx, token_idx, next_token) @completer([language.GtkDirective]) @@ -177,13 +163,7 @@ def _available_namespace_completions(ctx: CompletionContext): @completer( - applies_in=[ - language.UI, - language.ObjectContent, - language.Template, - language.TypeName, - language.BracketedTypeName, - ], + applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) def namespace(ctx: CompletionContext): @@ -204,13 +184,7 @@ def namespace(ctx: CompletionContext): @completer( - applies_in=[ - language.UI, - language.ObjectContent, - language.Template, - language.TypeName, - language.BracketedTypeName, - ], + applies_in=[language.UI, language.ObjectContent, language.Template], matches=[ [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], [(TokenType.IDENT, None), (TokenType.OP, ".")], @@ -221,11 +195,7 @@ def object_completer(ctx: CompletionContext): if ns is not None: for c in ns.classes.values(): snippet = c.name - if ( - str(ctx.next_token) != "{" - and not isinstance(ctx.ast_node, language.TypeName) - and not isinstance(ctx.ast_node, language.BracketedTypeName) - ): + if str(ctx.next_token) != "{": snippet += " {\n $0\n}" yield Completion( @@ -239,13 +209,7 @@ def object_completer(ctx: CompletionContext): @completer( - applies_in=[ - language.UI, - language.ObjectContent, - language.Template, - language.TypeName, - language.BracketedTypeName, - ], + applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) def gtk_object_completer(ctx: CompletionContext): @@ -253,11 +217,7 @@ 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) != "{" - and not isinstance(ctx.ast_node, language.TypeName) - and not isinstance(ctx.ast_node, language.BracketedTypeName) - ): + if str(ctx.next_token) != "{": snippet += " {\n $0\n}" yield Completion( @@ -269,17 +229,6 @@ 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], @@ -321,8 +270,6 @@ def prop_value_completer(ctx: CompletionContext): ) if (vt := ctx.ast_node.value_type) is not None: - assert isinstance(vt, ValueTypeCtx) - if isinstance(vt.value_type, gir.Enumeration): for name, member in vt.value_type.members.items(): yield Completion( @@ -348,14 +295,22 @@ def prop_value_completer(ctx: CompletionContext): elif isinstance(vt.value_type, gir.Class) or isinstance( vt.value_type, gir.Interface ): - if vt.allow_null: - yield Completion( - "null", - CompletionItemKind.Constant, - sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), - ) + yield Completion( + "null", + CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), + ) - yield from get_object_id_completions(ctx, vt.value_type) + 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) @@ -461,13 +416,7 @@ def complete_response_id(ctx: CompletionContext): (TokenType.IDENT, "response"), (TokenType.OP, "="), (TokenType.IDENT, None), - ], - [ - (TokenType.IDENT, "action"), - (TokenType.IDENT, "response"), - (TokenType.OP, "="), - (TokenType.NUMBER, None), - ], + ] ], ) def complete_response_default(ctx: CompletionContext): diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 970d429..36399b1 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -22,7 +22,7 @@ import typing as T from dataclasses import dataclass from enum import Enum -from . import gir, language +from . import gir from .ast_utils import AstNode from .lsp_utils import Completion, CompletionItemKind from .tokenizer import Token, TokenType @@ -57,21 +57,14 @@ 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: @@ -125,7 +118,8 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None ) yield from func(context) - completers.append(inner) + for c in applies_in: + c.completers.append(inner) return inner return decorator @@ -163,18 +157,3 @@ def get_property_completion( snippet=snippet, docs=doc, ) - - -def get_object_id_completions( - ctx: CompletionContext, value_type: T.Optional[gir.GirType] = None -): - for id, obj in ctx.ast_node.root.context[language.ScopeCtx].objects.items(): - if value_type is None or ( - obj.gir_class is not None and obj.gir_class.assignable_to(value_type) - ): - yield Completion( - id, - CompletionItemKind.Variable, - signature=" " + obj.signature, - sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), - ) diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index f5d2e06..9836c85 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -92,38 +92,31 @@ class CompileError(PrintableError): def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: assert self.range is not None - def format_line(range: Range): - line_num, col_num = utils.idx_to_pos(range.start, code) - end_line_num, end_col_num = utils.idx_to_pos(range.end, code) - line = code.splitlines(True)[line_num] if code != "" else "" + line_num, col_num = utils.idx_to_pos(self.range.start + 1, code) + end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code) + line = code.splitlines(True)[line_num] if code != "" else "" - # Display 1-based line numbers - line_num += 1 - end_line_num += 1 - col_num += 1 - end_col_num += 1 + # Display 1-based line numbers + line_num += 1 + end_line_num += 1 - n_spaces = col_num - 1 - n_carets = ( - (end_col_num - col_num) - if line_num == end_line_num - else (len(line) - n_spaces - 1) - ) + n_spaces = col_num - 1 + n_carets = ( + (end_col_num - col_num) + if line_num == end_line_num + else (len(line) - n_spaces - 1) + ) - n_spaces += line.count("\t", 0, col_num) - n_carets += line.count("\t", col_num, col_num + n_carets) - line = line.replace("\t", " ") + n_spaces += line.count("\t", 0, col_num) + n_carets += line.count("\t", col_num, col_num + n_carets) + line = line.replace("\t", " ") - n_carets = max(n_carets, 1) - - return line_num, col_num, line.rstrip(), (" " * n_spaces) + ("^" * n_carets) - - line_num, col_num, line, carets = format_line(self.range) + 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}: -{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n""" +{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n""" ) for hint in self.hints: @@ -148,12 +141,14 @@ at {filename} line {line_num} column {col_num}: ) for ref in self.references: - line_num, col_num, line, carets = format_line(ref.range) + line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code) + line = code.splitlines(True)[line_num] + line_num += 1 stream.write( f"""{Colors.FAINT}note: {ref.message}: at {filename} line {line_num} column {col_num}: -{Colors.FAINT}{line_num :>4} |{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n""" +{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" ) stream.write("\n") diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 6392eb4..333f4ac 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 88d7538..7f59d96 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 BracketedTypeName, ClassName, TypeName +from .types import ClassName from .ui import UI from .values import ( ArrayValue, diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 9bd04a5..1cc1b3b 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -34,7 +34,6 @@ from ..errors import ( CompileError, CompileWarning, DeprecatedWarning, - ErrorReference, MultipleErrors, UnusedWarning, UpgradeWarning, diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index 38d84f4..6e26048 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -48,7 +48,7 @@ class ScopeCtx: return self.node @cached_property - def objects(self) -> T.Dict[str, AstNode]: + def objects(self) -> T.Dict[str, Object]: return { obj.tokens["id"]: obj for obj in self._iter_recursive(self.node) @@ -58,7 +58,7 @@ class ScopeCtx: def validate_unique_ids(self) -> None: from .gtk_list_item_factory import ExtListItemFactory - passed: T.Dict[str, AstNode] = {} + passed = {} for obj in self._iter_recursive(self.node): if obj.tokens["id"] is None: continue @@ -71,16 +71,10 @@ class ScopeCtx: raise CompileError( f"Duplicate object ID '{obj.tokens['id']}'", token.range, - references=[ - ErrorReference( - passed[obj.tokens["id"]].group.tokens["id"].range, - "previous declaration was here", - ) - ], ) passed[obj.tokens["id"]] = obj - def _iter_recursive(self, node: AstNode) -> T.Generator[AstNode, T.Any, None]: + def _iter_recursive(self, node: AstNode): yield node for child in node.children: if child.context[ScopeCtx] is self: diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 910cd71..de6fbf1 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 BracketedTypeName, TypeName +from .types import TypeName expr = Sequence() @@ -196,7 +196,7 @@ class CastExpr(InfixExpr): grammar = [ Keyword("as"), AnyOf( - BracketedTypeName, + ["<", TypeName, Match(">").expected()], [ UseExact("lparen", "("), TypeName, @@ -211,13 +211,7 @@ class CastExpr(InfixExpr): @property def type(self) -> T.Optional[GirType]: - if len(self.children[BracketedTypeName]) == 1: - type_name = self.children[BracketedTypeName][0].type_name - return None if type_name is None else type_name.gir_type - elif len(self.children[TypeName]) == 1: - return self.children[TypeName][0].gir_type - else: - return None + return self.children[TypeName][0].gir_type @validate() def cast_makes_sense(self): diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index b6afb09..3b4235f 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -247,11 +247,3 @@ def decompile_signal( line += ";" ctx.print(line) return gir - - -@completer( - [Signal], - [[(TokenType.PUNCTUATION, "(")]], -) -def signal_object_completer(ctx: CompletionContext): - yield from get_object_id_completions(ctx) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index da41360..fe45c4d 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -27,11 +27,11 @@ class TypeName(AstNode): [ UseIdent("namespace"), ".", - UseIdent("class_name").expected("class name"), + UseIdent("class_name"), ], [ AnyOf("$", [".", UseLiteral("old_extern", True)]), - UseIdent("class_name").expected("class name"), + UseIdent("class_name"), UseLiteral("extern", True), ], UseIdent("class_name"), @@ -47,11 +47,7 @@ class TypeName(AstNode): @validate("class_name") def type_exists(self): - if ( - not self.tokens["extern"] - and self.gir_ns is not None - and self.tokens["class_name"] is not None - ): + if not self.tokens["extern"] and self.gir_ns is not None: self.root.gir.validate_type( self.tokens["class_name"], self.tokens["namespace"] ) @@ -186,14 +182,3 @@ 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 2840337..7fa6bfa 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 BracketedTypeName, TypeName +from .types import TypeName class Translated(AstNode): @@ -80,7 +80,11 @@ class TypeLiteral(AstNode): grammar = [ "typeof", AnyOf( - BracketedTypeName, + [ + "<", + to_parse_node(TypeName).expected("type name"), + Match(">").expected(), + ], [ UseExact("lparen", "("), to_parse_node(TypeName).expected("type name"), @@ -94,13 +98,8 @@ class TypeLiteral(AstNode): return gir.TypeType() @property - 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 + def type_name(self) -> TypeName: + return self.children[TypeName][0] @validate() def validate_for_type(self) -> None: diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 3f52ca5..15850f7 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -209,7 +209,6 @@ 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 8bb4c66..9bdbef1 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] - position = start_token.start if ctx.start == start_idx else start_token.end - - raise CompileError(self.message, Range(position, position, ctx.text)) + raise CompileError( + self.message, Range(start_token.end, start_token.end, ctx.text) + ) return True @@ -358,20 +358,7 @@ class Statement(ParseNode): token = ctx.peek_token() if str(token) != self.end: - start_idx = ctx.index - 1 - while ctx.tokens[start_idx].type in SKIP_TOKENS: - start_idx -= 1 - start_token = ctx.tokens[start_idx] - - position = ( - start_token.start if ctx.index - 1 == start_idx else start_token.end - ) - - ctx.errors.append( - CompileError( - f"Expected `{self.end}`", Range(position, position, ctx.text) - ) - ) + ctx.errors.append(CompileError(f"Expected `{self.end}`", token.range)) else: ctx.next_token() return True diff --git a/blueprintcompiler/utils.py b/blueprintcompiler/utils.py index ea8102e..de6d493 100644 --- a/blueprintcompiler/utils.py +++ b/blueprintcompiler/utils.py @@ -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) diff --git a/tests/sample_errors/empty.err b/tests/sample_errors/empty.err index b30f437..854962f 100644 --- a/tests/sample_errors/empty.err +++ b/tests/sample_errors/empty.err @@ -1 +1 @@ -1,1,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`) \ No newline at end of file +1,0,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`) \ No newline at end of file diff --git a/tests/sample_errors/expected_semicolon.err b/tests/sample_errors/expected_semicolon.err index a1b2a36..bfabc9a 100644 --- a/tests/sample_errors/expected_semicolon.err +++ b/tests/sample_errors/expected_semicolon.err @@ -1 +1 @@ -5,4,0,Expected `;` \ No newline at end of file +6,1,1,Expected `;` \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index 7d32ecb..9cd5baf 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -143,9 +143,9 @@ class TestSamples(unittest.TestCase): ] def error_str(error: CompileError): - line, col = utils.idx_to_pos(error.range.start, blueprint) + line, col = utils.idx_to_pos(error.range.start + 1, blueprint) len = error.range.length - return ",".join([str(line + 1), str(col + 1), str(len), error.message]) + return ",".join([str(line + 1), str(col), str(len), error.message]) actual = "\n".join([error_str(error) for error in errors])