From 3ba85d1e473c0ecafd7b323fb70232595c10038f Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 13:45:47 -0600 Subject: [PATCH] lsp: Fix completions when editing existing item Many completion snippets insert more than just the name. For example, the object completer inserts the braces and places your cursor inside them automatically, to save some typing. However, if you're changing the class of an existing object, this isn't what you want. Changed so that if the next token is '{', only the name is inserted. Made similar changes to the property and signal completers. --- blueprintcompiler/completions.py | 158 +++++++++--------- blueprintcompiler/completions_utils.py | 23 ++- .../language/adw_response_dialog.py | 4 +- blueprintcompiler/language/gtk_a11y.py | 8 +- .../language/gtk_combo_box_text.py | 2 +- blueprintcompiler/language/gtk_file_filter.py | 2 +- blueprintcompiler/language/gtk_layout.py | 2 +- blueprintcompiler/language/gtk_menu.py | 4 +- blueprintcompiler/language/gtk_scale.py | 4 +- blueprintcompiler/language/gtk_size_group.py | 2 +- blueprintcompiler/language/gtk_string_list.py | 2 +- blueprintcompiler/language/gtk_styles.py | 2 +- tests/test_samples.py | 6 +- 13 files changed, 122 insertions(+), 97 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b10ec3e..f1c17b6 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -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) def complete( @@ -62,16 +67,24 @@ 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" ) @@ -81,9 +94,9 @@ def using_gtk(lsp, ast_node, match_variables): 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, @@ -99,14 +112,18 @@ 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}}", + snippet=snippet, docs=c.doc, detail=c.detail, ) @@ -116,14 +133,18 @@ 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}}", + snippet=snippet, docs=c.doc, detail=c.detail, ) @@ -133,76 +154,55 @@ 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 ( +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(): + if str(ctx.next_token) == ":": + snippet = prop_name + elif ( isinstance(prop.type, gir.BoolType) - and lsp.client_supports_completion_choice + and ctx.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, - ) + snippet = f"{prop_name}: ${{1|true,false|}};" 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 + and ctx.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, - ) + snippet = f"{prop_name}: ${{1|{choices}|}};" 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, - ) + snippet = f"{prop_name}: expr $0;" else: - yield Completion( - prop_name, - CompletionItemKind.Property, - sort_text=f"0 {prop_name}", - snippet=f"{prop_name}: $0;", - docs=prop.doc, - detail=prop.detail, - ) + snippet = f"{prop_name}: $0;" + + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=snippet, + docs=prop.doc, + detail=prop.detail, + ) @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): + 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( @@ -221,30 +221,38 @@ def prop_value_completer(lsp, ast_node, match_variables): 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;", + 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..effb152 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -19,10 +19,21 @@ import typing as T +from dataclasses import dataclass +from .ast_utils import AstNode from .lsp_utils import Completion from .tokenizer import Token, TokenType + +@dataclass +class CompletionContext: + client_supports_completion_choice: bool + ast_node: AstNode + match_variables: T.List[str] + next_token: Token + + new_statement_patterns = [ [(TokenType.PUNCTUATION, "{")], [(TokenType.PUNCTUATION, "}")], @@ -32,8 +43,8 @@ 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): # 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,7 +77,13 @@ 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, + ) + yield from func(context) for c in applies_in: c.completers.append(inner) diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index d2680fd..516c69a 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,7 +154,7 @@ 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]" ) diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 0cc3cb3..417eaec 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -232,7 +232,7 @@ 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}" ) @@ -242,12 +242,12 @@ 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(): +def a11y_name_completer(ctx: CompletionContext): + for name, type in get_types(ctx.ast_node.root.gir).items(): yield Completion( name, CompletionItemKind.Property, - docs=_get_docs(ast_node.root.gir, type.name), + docs=_get_docs(ctx.ast_node.root.gir, type.name), ) diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 32b3486..e162cca 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -94,7 +94,7 @@ class ExtComboBoxItems(AstNode): applies_in_subclass=("Gtk", "ComboBoxText"), 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]") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index e84afc7..754130a 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -101,7 +101,7 @@ 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"]' ) diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index 8d3e37a..901c40d 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -93,7 +93,7 @@ class ExtLayout(AstNode): applies_in_subclass=("Gtk", "Widget"), 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}") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index c7ef5f2..d5bf4fb 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,7 +251,7 @@ 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}" ) diff --git a/blueprintcompiler/language/gtk_scale.py b/blueprintcompiler/language/gtk_scale.py index 1fd5ac3..2615a67 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -137,14 +137,14 @@ class ExtScaleMarks(AstNode): applies_in_subclass=("Gtk", "Scale"), 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]") @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..3505a06 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -104,7 +104,7 @@ class ExtSizeGroupWidgets(AstNode): applies_in_subclass=("Gtk", "SizeGroup"), 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]") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index a146f35..0e6c00a 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -75,7 +75,7 @@ class ExtStringListStrings(AstNode): applies_in_subclass=("Gtk", "StringList"), 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]") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 8617522..0cdea0b 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -80,7 +80,7 @@ class ExtStyles(AstNode): applies_in_subclass=("Gtk", "Widget"), 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"]') 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()