diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b10ec3e..a4e86b9 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -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,117 @@ 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]: 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 +186,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 +208,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 +230,38 @@ 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, + ctx, + annotations.is_property_translated(prop), + prop.doc, + ) @completer( applies_in=[language.Property, language.A11yProperty], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) -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,41 +269,99 @@ 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, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index eccf125..bfca55a 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -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,8 +61,10 @@ 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: @@ -66,10 +97,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, + ) diff --git a/blueprintcompiler/formatter.py b/blueprintcompiler/formatter.py index 60d87b4..f438675 100644 --- a/blueprintcompiler/formatter.py +++ b/blueprintcompiler/formatter.py @@ -193,6 +193,9 @@ def format(data, tab_size=2, insert_space=True): elif prev_line_type in require_extra_newline: newlines = 2 + current_line = "\n".join( + [line.rstrip() for line in current_line.split("\n")] + ) commit_current_line(LineType.COMMENT, newlines_before=newlines) else: # pragma: no cover diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index d2680fd..c621df0 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -143,7 +143,7 @@ class ExtAdwResponseDialog(AstNode): applies_in_subclass=("Adw", "MessageDialog"), 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]" ) @@ -154,9 +154,12 @@ def complete_adw_message_dialog(lsp, ast_node, match_variables): applies_in_subclass=("Adw", "AlertDialog"), matches=new_statement_patterns, ) -def complete_adw_alert_dialog(lsp, ast_node, match_variables): +def complete_adw_alert_dialog(_ctx: CompletionContext): yield Completion( - "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" + "responses", + CompletionItemKind.Keyword, + snippet="responses [\n\t$0\n]", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, "responses"), ) diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index 50a7512..67f2555 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -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 diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 0cc3cb3..7f90b6e 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -25,7 +25,7 @@ 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 return { "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), @@ -50,7 +50,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 widget = gir.get_type("Widget", "Gtk") return { @@ -75,7 +75,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 return { "busy": BoolType(), @@ -89,6 +89,20 @@ def get_state_types(gir): } +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 +135,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 +248,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 +261,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), ) diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 32b3486..5a7a892 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -94,8 +94,13 @@ class ExtComboBoxItems(AstNode): 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") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index e84afc7..d0e53d2 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -101,12 +101,25 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") 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") diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8d3e37a..63bc0f6 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -93,8 +93,13 @@ class ExtLayout(AstNode): 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") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index c7ef5f2..ed7ede8 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -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") diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 1fd5ac3..21089a4 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -137,14 +137,19 @@ class ExtScaleMarks(AstNode): 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),") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 54d85e5..d30eef9 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -104,8 +104,13 @@ class ExtSizeGroupWidgets(AstNode): 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") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index a146f35..a4fa3b5 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -75,8 +75,13 @@ class ExtStringListStrings(AstNode): 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") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 8617522..7c9252c 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -80,8 +80,13 @@ class ExtStyles(AstNode): 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") diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index d55a22a..896c0f7 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -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 diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index b938181..2a4380a 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -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} diff --git a/tests/formatting/comment_in.blp b/tests/formatting/comment_in.blp index 32a907c..88b825a 100644 --- a/tests/formatting/comment_in.blp +++ b/tests/formatting/comment_in.blp @@ -1,2 +1,4 @@ using Gtk 4.0; -//comment \ No newline at end of file +//comment +// Trailing whitespace: +// diff --git a/tests/formatting/comment_out.blp b/tests/formatting/comment_out.blp index d5dca95..91e647a 100644 --- a/tests/formatting/comment_out.blp +++ b/tests/formatting/comment_out.blp @@ -1,2 +1,4 @@ using Gtk 4.0; // comment +// Trailing whitespace: +// diff --git a/tests/test_samples.py b/tests/test_samples.py index 1f56eb6..5253166 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -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()