From 29e4a56bfc842a0b2c59d0acf9a8f78337b5deb9 Mon Sep 17 00:00:00 2001 From: Alexey Yerin Date: Sat, 4 Jan 2025 17:15:14 +0300 Subject: [PATCH 1/8] Formatter: Remove trailing whitespace from comments Fixes #153 --- blueprintcompiler/formatter.py | 3 +++ tests/formatting/comment_in.blp | 4 +++- tests/formatting/comment_out.blp | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) 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/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: +// From 461fe253169c50c45ab4f49b91b10c6b884bbc6c Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 13:45:47 -0600 Subject: [PATCH 2/8] 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() From 21f138fa83e35996804f6ac5cbbc9715aa354cae Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 16:05:27 -0600 Subject: [PATCH 3/8] completions: Complete available namespaces Add completions for namespaces in the typelib path that can be imported. Accepting the completion automatically adds an import statement. --- blueprintcompiler/completions.py | 20 +++++++++++++++++++- blueprintcompiler/language/ui.py | 18 ++++++++++++------ blueprintcompiler/lsp_utils.py | 6 ++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index f1c17b6..60cadad 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 from .parser import SKIP_TOKENS from .tokenizer import Token, TokenType @@ -96,14 +96,32 @@ def using_gtk(_ctx: CompletionContext): ) def namespace(ctx: CompletionContext): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") + + imported_namespaces = set(["Gtk"]) + for ns in ctx.ast_node.root.children[language.Import]: if ns.gir_namespace is not None: + imported_namespaces.add(ns.gir_namespace.name) yield Completion( ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".", ) + for ns, version in gir.get_available_namespaces(): + if ns not in imported_namespaces: + yield Completion( + ns, + CompletionItemKind.Module, + text=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], 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} From 533a49acd7f39568d8720d214f45241d4e2f1fcf Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 16:23:42 -0600 Subject: [PATCH 4/8] completions: Add translation-domain completer --- blueprintcompiler/completions.py | 26 ++++++++++++++++++++++++-- blueprintcompiler/completions_utils.py | 6 +++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 60cadad..cdd8d7d 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, TextEdit +from .lsp_utils import Completion, CompletionItemKind, TextEdit, get_docs_section from .parser import SKIP_TOKENS from .tokenizer import Token, TokenType @@ -55,7 +55,7 @@ def _complete( token_idx -= 1 for completer in ast_node.completers: - yield from completer(prev_tokens, next_token, ast_node, lsp) + yield from completer(prev_tokens, next_token, ast_node, lsp, idx) def complete( @@ -90,6 +90,28 @@ def using_gtk(_ctx: CompletionContext): ) +@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, + snippet='translation-domain "$0";', + docs=get_docs_section("Syntax TranslationDomain"), + ) + + @completer( applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index effb152..eb0bb79 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -32,6 +32,7 @@ class CompletionContext: ast_node: AstNode match_variables: T.List[str] next_token: Token + index: int new_statement_patterns = [ @@ -44,7 +45,9 @@ new_statement_patterns = [ 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): + 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: @@ -82,6 +85,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None ast_node=ast_node, match_variables=match_variables, next_token=next_token, + index=idx, ) yield from func(context) From 2204eaf92d951298dc4f626d3c3d83944b975b95 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 16:29:10 -0600 Subject: [PATCH 5/8] completions: Add completer for import statements --- blueprintcompiler/completions.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index cdd8d7d..c2186dd 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -90,6 +90,29 @@ def using_gtk(_ctx: CompletionContext): ) +@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};", + ) + + @completer([language.UI]) def translation_domain(ctx: CompletionContext): if ctx.ast_node.root.translation_domain is not None: From 3cdc3a159c5d69c2e5ea8c25c0750ac59f49a185 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 4 Jan 2025 20:22:02 -0600 Subject: [PATCH 6/8] completions: Add object value completions For object properties, add completions for named objects in the blueprint and for matching classes in imported namespaces. Also add null and bind. --- blueprintcompiler/completions.py | 84 +++++++++++++++---- .../language/gobject_property.py | 6 +- blueprintcompiler/language/gtk_a11y.py | 4 +- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index c2186dd..be881fa 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -135,26 +135,13 @@ def translation_domain(ctx: CompletionContext): ) -@completer( - applies_in=[language.UI, language.ObjectContent, language.Template], - matches=new_statement_patterns, -) -def namespace(ctx: CompletionContext): - yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") - - imported_namespaces = set(["Gtk"]) - - for ns in ctx.ast_node.root.children[language.Import]: - if ns.gir_namespace is not None: - imported_namespaces.add(ns.gir_namespace.name) - yield Completion( - ns.gir_namespace.name, - CompletionItemKind.Module, - text=ns.gir_namespace.name + ".", - ) +def _ns_prefix_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: + if ns not in imported_namespaces and ns != "Gtk": yield Completion( ns, CompletionItemKind.Module, @@ -168,6 +155,24 @@ def namespace(ctx: CompletionContext): ) +@completer( + applies_in=[language.UI, language.ObjectContent, language.Template], + matches=new_statement_patterns, +) +def namespace(ctx: CompletionContext): + yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") + + 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 + ".", + ) + + yield from _ns_prefix_completions(ctx) + + @completer( applies_in=[language.UI, language.ObjectContent, language.Template], matches=[ @@ -261,6 +266,14 @@ def property_completer(ctx: CompletionContext): matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) 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"), + ) + assert isinstance(ctx.ast_node, language.Property) or isinstance( ctx.ast_node, language.A11yProperty ) @@ -279,6 +292,41 @@ def prop_value_completer(ctx: CompletionContext): yield Completion("true", CompletionItemKind.Constant) yield Completion("false", CompletionItemKind.Constant) + elif isinstance(vt.value_type, gir.Class) or isinstance( + vt.value_type, gir.Interface + ): + yield Completion( + "null", + CompletionItemKind.Constant, + ) + + yield from _ns_prefix_completions(ctx) + + 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, + detail=obj.signature, + ) + + 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, + snippet=snippet, + detail=c.detail, + docs=c.doc, + ) + @completer( applies_in=[language.ObjectContent], 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 417eaec..a8bf300 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -121,7 +121,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 From bcb8e4ea1456b63c39b601e5f9e481a095cfb299 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 14:16:29 -0600 Subject: [PATCH 7/8] completions: Sort completion items --- blueprintcompiler/completions.py | 31 ++++++++++++--- blueprintcompiler/completions_utils.py | 16 ++++++++ .../language/adw_response_dialog.py | 5 ++- blueprintcompiler/language/gtk_a11y.py | 5 ++- .../language/gtk_combo_box_text.py | 7 +++- blueprintcompiler/language/gtk_file_filter.py | 19 +++++++-- blueprintcompiler/language/gtk_layout.py | 7 +++- blueprintcompiler/language/gtk_menu.py | 39 ++++++++++++++++--- blueprintcompiler/language/gtk_scale.py | 7 +++- blueprintcompiler/language/gtk_size_group.py | 7 +++- blueprintcompiler/language/gtk_string_list.py | 7 +++- blueprintcompiler/language/gtk_styles.py | 7 +++- 12 files changed, 135 insertions(+), 22 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index be881fa..b5a0721 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -110,6 +110,7 @@ def using(ctx: CompletionContext): f"using {ns} {version}", CompletionItemKind.Module, text=f"using {ns} {version};", + sort_text=get_sort_key(CompletionPriority.NAMESPACE, ns), ) @@ -130,6 +131,7 @@ def translation_domain(ctx: CompletionContext): 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"), ) @@ -146,6 +148,7 @@ def _ns_prefix_completions(ctx: CompletionContext): ns, CompletionItemKind.Module, text=ns + ".", + sort_text=get_sort_key(CompletionPriority.IMPORT_NAMESPACE, ns), signature=f" using {ns} {version}", additional_text_edits=[ TextEdit( @@ -168,6 +171,9 @@ def namespace(ctx: CompletionContext): ns.gir_namespace.name, CompletionItemKind.Module, text=ns.gir_namespace.name + ".", + sort_text=get_sort_key( + CompletionPriority.NAMESPACE, ns.gir_namespace.name + ), ) yield from _ns_prefix_completions(ctx) @@ -191,6 +197,7 @@ def object_completer(ctx: CompletionContext): yield Completion( c.name, CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, c.name), snippet=snippet, docs=c.doc, detail=c.detail, @@ -212,6 +219,7 @@ def gtk_object_completer(ctx: CompletionContext): yield Completion( c.name, CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, c.name), snippet=snippet, docs=c.doc, detail=c.detail, @@ -254,7 +262,7 @@ def property_completer(ctx: CompletionContext): yield Completion( prop_name, CompletionItemKind.Property, - sort_text=f"0 {prop_name}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, prop_name), snippet=snippet, docs=prop.doc, detail=prop.detail, @@ -272,6 +280,7 @@ def prop_value_completer(ctx: CompletionContext): 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( @@ -286,11 +295,20 @@ def prop_value_completer(ctx: CompletionContext): 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 @@ -298,6 +316,7 @@ def prop_value_completer(ctx: CompletionContext): yield Completion( "null", CompletionItemKind.Constant, + sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), ) yield from _ns_prefix_completions(ctx) @@ -309,7 +328,8 @@ def prop_value_completer(ctx: CompletionContext): yield Completion( id, CompletionItemKind.Variable, - detail=obj.signature, + signature=" " + obj.signature, + sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), ) for ns in ctx.ast_node.root.gir.namespaces.values(): @@ -322,6 +342,7 @@ def prop_value_completer(ctx: CompletionContext): yield Completion( name, CompletionItemKind.Class, + sort_text=get_sort_key(CompletionPriority.CLASS, name), snippet=snippet, detail=c.detail, docs=c.doc, @@ -355,7 +376,7 @@ def signal_completer(ctx: CompletionContext): yield Completion( signal_name, CompletionItemKind.Event, - sort_text=f"1 {signal_name}", + sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, signal_name), snippet=snippet, docs=signal.doc, detail=signal.detail, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index eb0bb79..4d9ceb2 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -20,12 +20,28 @@ import typing as T from dataclasses import dataclass +from enum import Enum from .ast_utils import AstNode from .lsp_utils import Completion 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 diff --git a/blueprintcompiler/language/adw_response_dialog.py b/blueprintcompiler/language/adw_response_dialog.py index 516c69a..c621df0 100644 --- a/blueprintcompiler/language/adw_response_dialog.py +++ b/blueprintcompiler/language/adw_response_dialog.py @@ -156,7 +156,10 @@ def complete_adw_message_dialog(_ctx: CompletionContext): ) 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/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index a8bf300..cd52746 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -236,7 +236,10 @@ class ExtAccessibility(AstNode): ) 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"), ) diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index e162cca..5a7a892 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -95,7 +95,12 @@ class ExtComboBoxItems(AstNode): matches=new_statement_patterns, ) 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") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 754130a..d0e53d2 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -103,10 +103,23 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") ) 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 901c40d..63bc0f6 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -94,7 +94,12 @@ class ExtLayout(AstNode): matches=new_statement_patterns, ) 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") diff --git a/blueprintcompiler/language/gtk_menu.py b/blueprintcompiler/language/gtk_menu.py index d5bf4fb..ed7ede8 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -253,21 +253,48 @@ def menu_completer(_ctx: CompletionContext): ) 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 2615a67..21089a4 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -138,7 +138,12 @@ class ExtScaleMarks(AstNode): matches=new_statement_patterns, ) 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( diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 3505a06..d30eef9 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -105,7 +105,12 @@ class ExtSizeGroupWidgets(AstNode): matches=new_statement_patterns, ) 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") diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index 0e6c00a..a4fa3b5 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -76,7 +76,12 @@ class ExtStringListStrings(AstNode): matches=new_statement_patterns, ) 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") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 0cdea0b..7c9252c 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -81,7 +81,12 @@ class ExtStyles(AstNode): matches=new_statement_patterns, ) 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") From 72583be26789a5b4bd93bf2c813301ea6411daf1 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sun, 5 Jan 2025 15:21:26 -0600 Subject: [PATCH 8/8] completions: Improve accessibility properties --- blueprintcompiler/completions.py | 77 +++++++++----------------- blueprintcompiler/completions_utils.py | 37 ++++++++++++- blueprintcompiler/language/gtk_a11y.py | 30 +++++++--- 3 files changed, 86 insertions(+), 58 deletions(-) diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index b5a0721..a4e86b9 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -137,7 +137,7 @@ def translation_domain(ctx: CompletionContext): ) -def _ns_prefix_completions(ctx: CompletionContext): +def _available_namespace_completions(ctx: CompletionContext): imported_namespaces = set( [import_.namespace for import_ in ctx.ast_node.root.using] ) @@ -176,7 +176,7 @@ def namespace(ctx: CompletionContext): ), ) - yield from _ns_prefix_completions(ctx) + yield from _available_namespace_completions(ctx) @completer( @@ -234,38 +234,12 @@ 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 ctx.client_supports_completion_choice - ): - 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";' - ) - elif ( - isinstance(prop.type, gir.Enumeration) - and len(prop.type.members) <= 10 - and ctx.client_supports_completion_choice - ): - choices = ",".join(prop.type.members.keys()) - snippet = f"{prop_name}: ${{1|{choices}|}};" - elif prop.type.full_name == "Gtk.Expression": - snippet = f"{prop_name}: expr $0;" - else: - snippet = f"{prop_name}: $0;" - - yield Completion( + yield get_property_completion( prop_name, - CompletionItemKind.Property, - sort_text=get_sort_key(CompletionPriority.OBJECT_MEMBER, prop_name), - snippet=snippet, - docs=prop.doc, - detail=prop.detail, + prop, + ctx, + annotations.is_property_translated(prop), + prop.doc, ) @@ -319,8 +293,6 @@ def prop_value_completer(ctx: CompletionContext): sort_text=get_sort_key(CompletionPriority.KEYWORD, "null"), ) - yield from _ns_prefix_completions(ctx) - 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 @@ -332,21 +304,26 @@ def prop_value_completer(ctx: CompletionContext): sort_text=get_sort_key(CompletionPriority.NAMED_OBJECT, id), ) - 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, - ) + 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( diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 4d9ceb2..bfca55a 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -22,8 +22,9 @@ import typing as T from dataclasses import dataclass from enum import Enum +from . import gir from .ast_utils import AstNode -from .lsp_utils import Completion +from .lsp_utils import Completion, CompletionItemKind from .tokenizer import Token, TokenType @@ -110,3 +111,37 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None 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/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index cd52746..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), @@ -247,12 +261,14 @@ def a11y_completer(_ctx: CompletionContext): applies_in=[ExtAccessibility], matches=new_statement_patterns, ) -def a11y_name_completer(ctx: CompletionContext): +def a11y_property_completer(ctx: CompletionContext): for name, type in get_types(ctx.ast_node.root.gir).items(): - yield Completion( + yield get_property_completion( name, - CompletionItemKind.Property, - docs=_get_docs(ctx.ast_node.root.gir, type.name), + type, + ctx, + name in TRANSLATED, + _get_docs(ctx.ast_node.root.gir, name), )