diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 386f2d7..0a43388 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -31,13 +31,13 @@ Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] def _complete( - 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 ) -> 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(child, tokens, idx, token_idx) + yield from _complete(lsp, child, tokens, idx, token_idx) return prev_tokens: T.List[Token] = [] @@ -50,11 +50,11 @@ def _complete( token_idx -= 1 for completer in ast_node.completers: - yield from completer(prev_tokens, ast_node) + yield from completer(prev_tokens, ast_node, lsp) def complete( - ast_node: AstNode, tokens: T.List[Token], idx: int + lsp, ast_node: AstNode, tokens: T.List[Token], idx: int ) -> T.Iterator[Completion]: token_idx = 0 # find the current token @@ -67,11 +67,11 @@ def complete( idx = tokens[token_idx].start token_idx -= 1 - yield from _complete(ast_node, tokens, idx, token_idx) + yield from _complete(lsp, ast_node, tokens, idx, token_idx) @completer([language.GtkDirective]) -def using_gtk(ast_node, match_variables): +def using_gtk(lsp, ast_node, match_variables): yield Completion("using Gtk 4.0;", CompletionItemKind.Keyword) @@ -79,7 +79,7 @@ def using_gtk(ast_node, match_variables): applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) -def namespace(ast_node, match_variables): +def namespace(lsp, ast_node, match_variables): yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") for ns in ast_node.root.children[language.Import]: if ns.gir_namespace is not None: @@ -97,7 +97,7 @@ def namespace(ast_node, match_variables): [(TokenType.IDENT, None), (TokenType.OP, ".")], ], ) -def object_completer(ast_node, match_variables): +def object_completer(lsp, ast_node, match_variables): ns = ast_node.root.gir.namespaces.get(match_variables[0]) if ns is not None: for c in ns.classes.values(): @@ -108,7 +108,7 @@ def object_completer(ast_node, match_variables): applies_in=[language.UI, language.ObjectContent, language.Template], matches=new_statement_patterns, ) -def gtk_object_completer(ast_node, match_variables): +def gtk_object_completer(lsp, ast_node, match_variables): ns = ast_node.root.gir.namespaces.get("Gtk") if ns is not None: for c in ns.classes.values(): @@ -119,17 +119,52 @@ def gtk_object_completer(ast_node, match_variables): applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def property_completer(ast_node, match_variables): +def property_completer(lsp, ast_node, match_variables): if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): - for prop in ast_node.gir_class.properties: - yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") + 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|}};", + ) + elif isinstance(prop.type, gir.StringType): + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f'{prop_name}: "$0";', + ) + 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}|}};", + ) + else: + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: $0;", + ) @completer( applies_in=[language.Property, language.BaseAttribute], matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], ) -def prop_value_completer(ast_node, match_variables): +def prop_value_completer(lsp, ast_node, match_variables): if (vt := ast_node.value_type) is not None: if isinstance(vt.value_type, gir.Enumeration): for name, member in vt.value_type.members.items(): @@ -144,7 +179,7 @@ def prop_value_completer(ast_node, match_variables): applies_in=[language.ObjectContent], matches=new_statement_patterns, ) -def signal_completer(ast_node, match_variables): +def signal_completer(lsp, ast_node, match_variables): if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): for signal in ast_node.gir_class.signals: if not isinstance(ast_node.parent, language.Object): @@ -158,13 +193,14 @@ def signal_completer(ast_node, match_variables): ) yield Completion( signal, - CompletionItemKind.Property, + CompletionItemKind.Event, + sort_text=f"1 {signal}", snippet=f"{signal} => \$${{1:{name}_{signal.replace('-', '_')}}}()$0;", ) @completer(applies_in=[language.UI], matches=new_statement_patterns) -def template_completer(ast_node, match_variables): +def template_completer(lsp, ast_node, match_variables): yield Completion( "template", CompletionItemKind.Snippet, diff --git a/blueprintcompiler/completions_utils.py b/blueprintcompiler/completions_utils.py index 2aae874..52e325d 100644 --- a/blueprintcompiler/completions_utils.py +++ b/blueprintcompiler/completions_utils.py @@ -44,7 +44,7 @@ def applies_to(*ast_types): 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): + def inner(prev_tokens: T.List[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: @@ -77,7 +77,7 @@ def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None if not any_match: return - yield from func(ast_node, match_variables) + yield from func(lsp, ast_node, match_variables) for c in applies_in: c.completers.append(inner) diff --git a/blueprintcompiler/language/adw_message_dialog.py b/blueprintcompiler/language/adw_message_dialog.py index fb2be66..9b3d41c 100644 --- a/blueprintcompiler/language/adw_message_dialog.py +++ b/blueprintcompiler/language/adw_message_dialog.py @@ -141,7 +141,7 @@ class ExtAdwMessageDialog(AstNode): applies_in_subclass=("Adw", "MessageDialog"), matches=new_statement_patterns, ) -def style_completer(ast_node, match_variables): +def style_completer(lsp, ast_node, match_variables): 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 e9850f5..2ad8097 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -204,7 +204,7 @@ class ExtAccessibility(AstNode): applies_in=[ObjectContent], matches=new_statement_patterns, ) -def a11y_completer(ast_node, match_variables): +def a11y_completer(lsp, ast_node, match_variables): yield Completion( "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" ) @@ -214,7 +214,7 @@ def a11y_completer(ast_node, match_variables): applies_in=[ExtAccessibility], matches=new_statement_patterns, ) -def a11y_name_completer(ast_node, match_variables): +def a11y_name_completer(lsp, ast_node, match_variables): for name, type in get_types(ast_node.root.gir).items(): yield Completion( name, diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 4c6fda9..e514e4a 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -87,5 +87,5 @@ class ExtComboBoxItems(AstNode): applies_in_subclass=("Gtk", "ComboBoxText"), matches=new_statement_patterns, ) -def items_completer(ast_node, match_variables): +def items_completer(lsp, ast_node, match_variables): 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 1be83e8..8dcbcb8 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -104,7 +104,7 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix") applies_in_subclass=("Gtk", "FileFilter"), matches=new_statement_patterns, ) -def file_filter_completer(ast_node, match_variables): +def file_filter_completer(lsp, ast_node, match_variables): 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 892e0c6..508609d 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -89,7 +89,7 @@ class ExtLayout(AstNode): applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) -def layout_completer(ast_node, match_variables): +def layout_completer(lsp, ast_node, match_variables): 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 e1bbd57..844c309 100644 --- a/blueprintcompiler/language/gtk_menu.py +++ b/blueprintcompiler/language/gtk_menu.py @@ -221,7 +221,7 @@ from .ui import UI applies_in=[UI], matches=new_statement_patterns, ) -def menu_completer(ast_node, match_variables): +def menu_completer(lsp, ast_node, match_variables): yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") @@ -229,7 +229,7 @@ def menu_completer(ast_node, match_variables): applies_in=[Menu], matches=new_statement_patterns, ) -def menu_content_completer(ast_node, match_variables): +def menu_content_completer(lsp, ast_node, match_variables): 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 a2c9168..ac4b77c 100644 --- a/blueprintcompiler/language/gtk_scale.py +++ b/blueprintcompiler/language/gtk_scale.py @@ -129,14 +129,14 @@ class ExtScaleMarks(AstNode): applies_in_subclass=("Gtk", "Scale"), matches=new_statement_patterns, ) -def complete_marks(ast_node, match_variables): +def complete_marks(lsp, ast_node, match_variables): yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") @completer( applies_in=[ExtScaleMarks], ) -def complete_mark(ast_node, match_variables): +def complete_mark(lsp, ast_node, match_variables): 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 60af861..baa4c30 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -94,5 +94,5 @@ class ExtSizeGroupWidgets(AstNode): applies_in_subclass=("Gtk", "SizeGroup"), matches=new_statement_patterns, ) -def size_group_completer(ast_node, match_variables): +def size_group_completer(lsp, ast_node, match_variables): 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 37a46ec..be01262 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -71,5 +71,5 @@ class ExtStringListStrings(AstNode): applies_in_subclass=("Gtk", "StringList"), matches=new_statement_patterns, ) -def strings_completer(ast_node, match_variables): +def strings_completer(lsp, ast_node, match_variables): yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 26c0e74..236dde0 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -76,7 +76,7 @@ class ExtStyles(AstNode): applies_in_subclass=("Gtk", "Widget"), matches=new_statement_patterns, ) -def style_completer(ast_node, match_variables): +def style_completer(lsp, ast_node, match_variables): yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index 19c02e5..2281059 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -115,6 +115,7 @@ class LanguageServer: def __init__(self): self.client_capabilities = {} + self.client_supports_completion_choice = False self._open_files: T.Dict[str, OpenFile] = {} def run(self): @@ -187,6 +188,9 @@ class LanguageServer: from . import main self.client_capabilities = params.get("capabilities", {}) + self.client_supports_completion_choice = params.get("clientInfo", {}).get( + "name" + ) in ["Visual Studio Code", "VSCodium"] self._send_response( id, { @@ -271,7 +275,7 @@ class LanguageServer: idx = utils.pos_to_idx( params["position"]["line"], params["position"]["character"], open_file.text ) - completions = complete(open_file.ast, open_file.tokens, idx) + completions = complete(self, open_file.ast, open_file.tokens, idx) self._send_response( id, [completion.to_json(True) for completion in completions] ) diff --git a/blueprintcompiler/lsp_utils.py b/blueprintcompiler/lsp_utils.py index 9da8461..33ae923 100644 --- a/blueprintcompiler/lsp_utils.py +++ b/blueprintcompiler/lsp_utils.py @@ -80,6 +80,7 @@ class Completion: kind: CompletionItemKind signature: T.Optional[str] = None deprecated: bool = False + sort_text: T.Optional[str] = None docs: T.Optional[str] = None text: T.Optional[str] = None snippet: T.Optional[str] = None @@ -103,6 +104,7 @@ class Completion: if self.docs else None, "deprecated": self.deprecated, + "sortText": self.sort_text, "insertText": insert_text, "insertTextFormat": insert_text_format, } diff --git a/tests/test_samples.py b/tests/test_samples.py index 9d891f6..226e762 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -35,6 +35,7 @@ from blueprintcompiler.errors import ( MultipleErrors, PrintableError, ) +from blueprintcompiler.lsp import LanguageServer from blueprintcompiler.outputs.xml import XmlOutput from blueprintcompiler.tokenizer import Token, TokenType, tokenize @@ -44,7 +45,7 @@ class TestSamples(unittest.TestCase): for i in range(len(text)): ast.get_docs(i) for i in range(len(text)): - list(complete(ast, tokens, i)) + list(complete(LanguageServer(), ast, tokens, i)) ast.get_document_symbols() def assert_sample(self, name, skip_run=False):