This commit is contained in:
gregorni 2023-08-29 22:55:14 +02:00
commit 4f5c1537dd
15 changed files with 76 additions and 33 deletions

View file

@ -31,13 +31,13 @@ Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]]
def _complete( 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]: ) -> T.Iterator[Completion]:
for child in ast_node.children: for child in ast_node.children:
if child.group.start <= idx and ( if child.group.start <= idx and (
idx < child.group.end or (idx == child.group.end and child.incomplete) 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 return
prev_tokens: T.List[Token] = [] prev_tokens: T.List[Token] = []
@ -50,11 +50,11 @@ def _complete(
token_idx -= 1 token_idx -= 1
for completer in ast_node.completers: for completer in ast_node.completers:
yield from completer(prev_tokens, ast_node) yield from completer(prev_tokens, ast_node, lsp)
def complete( def complete(
ast_node: AstNode, tokens: T.List[Token], idx: int lsp, ast_node: AstNode, tokens: T.List[Token], idx: int
) -> T.Iterator[Completion]: ) -> T.Iterator[Completion]:
token_idx = 0 token_idx = 0
# find the current token # find the current token
@ -67,11 +67,11 @@ def complete(
idx = tokens[token_idx].start idx = tokens[token_idx].start
token_idx -= 1 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]) @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) 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], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def namespace(ast_node, match_variables): def namespace(lsp, ast_node, match_variables):
yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.")
for ns in ast_node.root.children[language.Import]: for ns in ast_node.root.children[language.Import]:
if ns.gir_namespace is not None: if ns.gir_namespace is not None:
@ -97,7 +97,7 @@ def namespace(ast_node, match_variables):
[(TokenType.IDENT, None), (TokenType.OP, ".")], [(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]) ns = ast_node.root.gir.namespaces.get(match_variables[0])
if ns is not None: if ns is not None:
for c in ns.classes.values(): 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], applies_in=[language.UI, language.ObjectContent, language.Template],
matches=new_statement_patterns, 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") ns = ast_node.root.gir.namespaces.get("Gtk")
if ns is not None: if ns is not None:
for c in ns.classes.values(): for c in ns.classes.values():
@ -119,17 +119,52 @@ def gtk_object_completer(ast_node, match_variables):
applies_in=[language.ObjectContent], applies_in=[language.ObjectContent],
matches=new_statement_patterns, 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): if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType):
for prop in ast_node.gir_class.properties: for prop_name, prop in ast_node.gir_class.properties.items():
yield Completion(prop, CompletionItemKind.Property, snippet=f"{prop}: $0;") 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( @completer(
applies_in=[language.Property, language.BaseAttribute], applies_in=[language.Property, language.BaseAttribute],
matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], 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 (vt := ast_node.value_type) is not None:
if isinstance(vt.value_type, gir.Enumeration): if isinstance(vt.value_type, gir.Enumeration):
for name, member in vt.value_type.members.items(): 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], applies_in=[language.ObjectContent],
matches=new_statement_patterns, 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): if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType):
for signal in ast_node.gir_class.signals: for signal in ast_node.gir_class.signals:
if not isinstance(ast_node.parent, language.Object): if not isinstance(ast_node.parent, language.Object):
@ -158,13 +193,14 @@ def signal_completer(ast_node, match_variables):
) )
yield Completion( yield Completion(
signal, signal,
CompletionItemKind.Property, CompletionItemKind.Event,
sort_text=f"1 {signal}",
snippet=f"{signal} => \$${{1:{name}_{signal.replace('-', '_')}}}()$0;", snippet=f"{signal} => \$${{1:{name}_{signal.replace('-', '_')}}}()$0;",
) )
@completer(applies_in=[language.UI], matches=new_statement_patterns) @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( yield Completion(
"template", "template",
CompletionItemKind.Snippet, CompletionItemKind.Snippet,

View file

@ -44,7 +44,7 @@ def applies_to(*ast_types):
def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None):
def decorator(func): 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 # For completers that apply in ObjectContent nodes, we can further
# check that the object is the right class # check that the object is the right class
if applies_in_subclass is not None: 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: if not any_match:
return return
yield from func(ast_node, match_variables) yield from func(lsp, ast_node, match_variables)
for c in applies_in: for c in applies_in:
c.completers.append(inner) c.completers.append(inner)

View file

@ -141,7 +141,7 @@ class ExtAdwMessageDialog(AstNode):
applies_in_subclass=("Adw", "MessageDialog"), applies_in_subclass=("Adw", "MessageDialog"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def style_completer(ast_node, match_variables): def style_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]"
) )

View file

@ -204,7 +204,7 @@ class ExtAccessibility(AstNode):
applies_in=[ObjectContent], applies_in=[ObjectContent],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def a11y_completer(ast_node, match_variables): def a11y_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}"
) )
@ -214,7 +214,7 @@ def a11y_completer(ast_node, match_variables):
applies_in=[ExtAccessibility], applies_in=[ExtAccessibility],
matches=new_statement_patterns, 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(): for name, type in get_types(ast_node.root.gir).items():
yield Completion( yield Completion(
name, name,

View file

@ -87,5 +87,5 @@ class ExtComboBoxItems(AstNode):
applies_in_subclass=("Gtk", "ComboBoxText"), applies_in_subclass=("Gtk", "ComboBoxText"),
matches=new_statement_patterns, 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]") yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]")

View file

@ -104,7 +104,7 @@ ext_file_filter_suffixes = create_node("suffixes", "suffix")
applies_in_subclass=("Gtk", "FileFilter"), applies_in_subclass=("Gtk", "FileFilter"),
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def file_filter_completer(ast_node, match_variables): def file_filter_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]'
) )

View file

@ -89,7 +89,7 @@ class ExtLayout(AstNode):
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns, 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}") yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}")

View file

@ -221,7 +221,7 @@ from .ui import UI
applies_in=[UI], applies_in=[UI],
matches=new_statement_patterns, 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}") yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}")
@ -229,7 +229,7 @@ def menu_completer(ast_node, match_variables):
applies_in=[Menu], applies_in=[Menu],
matches=new_statement_patterns, matches=new_statement_patterns,
) )
def menu_content_completer(ast_node, match_variables): def menu_content_completer(lsp, ast_node, match_variables):
yield Completion( yield Completion(
"submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}"
) )

View file

@ -129,14 +129,14 @@ class ExtScaleMarks(AstNode):
applies_in_subclass=("Gtk", "Scale"), applies_in_subclass=("Gtk", "Scale"),
matches=new_statement_patterns, 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]") yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]")
@completer( @completer(
applies_in=[ExtScaleMarks], 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),") yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),")

View file

@ -94,5 +94,5 @@ class ExtSizeGroupWidgets(AstNode):
applies_in_subclass=("Gtk", "SizeGroup"), applies_in_subclass=("Gtk", "SizeGroup"),
matches=new_statement_patterns, 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]") yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]")

View file

@ -71,5 +71,5 @@ class ExtStringListStrings(AstNode):
applies_in_subclass=("Gtk", "StringList"), applies_in_subclass=("Gtk", "StringList"),
matches=new_statement_patterns, 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]") yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]")

View file

@ -76,7 +76,7 @@ class ExtStyles(AstNode):
applies_in_subclass=("Gtk", "Widget"), applies_in_subclass=("Gtk", "Widget"),
matches=new_statement_patterns, 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"]') yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]')

View file

@ -117,6 +117,7 @@ class LanguageServer:
def __init__(self): def __init__(self):
self.client_capabilities = {} self.client_capabilities = {}
self.client_supports_completion_choice = False
self._open_files: T.Dict[str, OpenFile] = {} self._open_files: T.Dict[str, OpenFile] = {}
def run(self): def run(self):
@ -189,6 +190,9 @@ class LanguageServer:
from . import main from . import main
self.client_capabilities = params.get("capabilities", {}) self.client_capabilities = params.get("capabilities", {})
self.client_supports_completion_choice = params.get("clientInfo", {}).get(
"name"
) in ["Visual Studio Code", "VSCodium"]
self._send_response( self._send_response(
id, id,
{ {
@ -273,7 +277,7 @@ class LanguageServer:
idx = utils.pos_to_idx( idx = utils.pos_to_idx(
params["position"]["line"], params["position"]["character"], open_file.text 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( self._send_response(
id, [completion.to_json(True) for completion in completions] id, [completion.to_json(True) for completion in completions]
) )

View file

@ -80,6 +80,7 @@ class Completion:
kind: CompletionItemKind kind: CompletionItemKind
signature: T.Optional[str] = None signature: T.Optional[str] = None
deprecated: bool = False deprecated: bool = False
sort_text: T.Optional[str] = None
docs: T.Optional[str] = None docs: T.Optional[str] = None
text: T.Optional[str] = None text: T.Optional[str] = None
snippet: T.Optional[str] = None snippet: T.Optional[str] = None
@ -103,6 +104,7 @@ class Completion:
if self.docs if self.docs
else None, else None,
"deprecated": self.deprecated, "deprecated": self.deprecated,
"sortText": self.sort_text,
"insertText": insert_text, "insertText": insert_text,
"insertTextFormat": insert_text_format, "insertTextFormat": insert_text_format,
} }

View file

@ -35,6 +35,7 @@ from blueprintcompiler.errors import (
MultipleErrors, MultipleErrors,
PrintableError, PrintableError,
) )
from blueprintcompiler.lsp import LanguageServer
from blueprintcompiler.outputs.xml import XmlOutput from blueprintcompiler.outputs.xml import XmlOutput
from blueprintcompiler.tokenizer import Token, TokenType, tokenize from blueprintcompiler.tokenizer import Token, TokenType, tokenize
@ -44,7 +45,7 @@ class TestSamples(unittest.TestCase):
for i in range(len(text)): for i in range(len(text)):
ast.get_docs(i) ast.get_docs(i)
for i in range(len(text)): for i in range(len(text)):
list(complete(ast, tokens, i)) list(complete(LanguageServer(), ast, tokens, i))
ast.get_document_symbols() ast.get_document_symbols()
def assert_sample(self, name, skip_run=False): def assert_sample(self, name, skip_run=False):