diff --git a/README.md b/README.md index 6ed4c7d..c5d5f1f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ jedi-language-server aims to support all of Jedi's capabilities and expose them ### Language Features +- [completionItem/resolve](https://microsoft.github.io/language-server-protocol/specification#completionItem_resolve) - [textDocument/codeAction](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction) (refactor.inline, refactor.extract) - [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion) - [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition) @@ -91,7 +92,8 @@ If you are configuring manually, jedi-language-server supports the following [in "autoImportModules": [] }, "completion": { - "disableSnippets": false + "disableSnippets": false, + "resolveEagerly": false }, "diagnostics": { "enable": true, diff --git a/jedi_language_server/initialize_params_parser.py b/jedi_language_server/initialize_params_parser.py index 2ca18d4..1db5a33 100644 --- a/jedi_language_server/initialize_params_parser.py +++ b/jedi_language_server/initialize_params_parser.py @@ -136,6 +136,17 @@ def initializationOptions_completion_disableSnippets(self) -> bool: default = False return bool(rgetattr(self._initialize_params, path, default)) + @cached_property + def initializationOptions_completion_resolveEagerly(self): + _path = ( + "initializationOptions", + "completion", + "resolveEagerly", + ) + path = ".".join(_path) + default = False + return bool(rgetattr(self._initialize_params, path, default)) + @cached_property def initializationOptions_diagnostics_enable(self) -> bool: _path = ( diff --git a/jedi_language_server/jedi_utils.py b/jedi_language_server/jedi_utils.py index 2a827cc..13dbea5 100644 --- a/jedi_language_server/jedi_utils.py +++ b/jedi_language_server/jedi_utils.py @@ -324,33 +324,46 @@ def is_import(script_: Script, line: int, column: int) -> bool: CompletionItemKind.Function, } +_MOST_RECENT_COMPLETIONS: Dict[str, Completion] = {} + + +def clear_completions_cache(): + """Clears the cache of completions used for completionItem/resolve.""" + _MOST_RECENT_COMPLETIONS.clear() + def lsp_completion_item( - name: Completion, + completion: Completion, char_before_cursor: str, enable_snippets: bool, + resolve_eagerly: bool, markup_kind: MarkupKind, ) -> CompletionItem: """Using a Jedi completion, obtain a jedi completion item.""" - name_name = name.name - name_clean = clean_completion_name(name_name, char_before_cursor) - lsp_type = get_lsp_completion_type(name.type) + completion_name = completion.name + name_clean = clean_completion_name(completion_name, char_before_cursor) + lsp_type = get_lsp_completion_type(completion.type) completion_item = CompletionItem( - label=name_name, - filter_text=name_name, + label=completion_name, + filter_text=completion_name, kind=lsp_type, - detail=name.description, - documentation=MarkupContent(kind=markup_kind, value=name.docstring()), - sort_text=complete_sort_name(name), + sort_text=complete_sort_name(completion), insert_text=name_clean, insert_text_format=InsertTextFormat.PlainText, ) + if resolve_eagerly: + completion_item = lsp_completion_item_resolve( + completion_item, markup_kind=markup_kind + ) + else: + _MOST_RECENT_COMPLETIONS[completion_name] = completion + if not enable_snippets: return completion_item if lsp_type not in _LSP_TYPE_FOR_SNIPPET: return completion_item - signatures = name.get_signatures() + signatures = completion.get_signatures() if not signatures: return completion_item @@ -358,12 +371,25 @@ def lsp_completion_item( snippet_signature = get_snippet_signature(signatures[0]) except Exception: # pylint: disable=broad-except return completion_item - new_text = name_name + snippet_signature + new_text = completion_name + snippet_signature completion_item.insertText = new_text completion_item.insertTextFormat = InsertTextFormat.Snippet return completion_item +def lsp_completion_item_resolve( + item: CompletionItem, + markup_kind: MarkupKind, +) -> CompletionItem: + """Resolve completion item using cached jedi completion data.""" + completion = _MOST_RECENT_COMPLETIONS[item.label] + item.detail = completion.description + item.documentation = MarkupContent( + kind=markup_kind, value=completion.docstring() + ) + return item + + def random_var( beginning: str, random_length: int = 8, diff --git a/jedi_language_server/server.py b/jedi_language_server/server.py index fa7685f..616f9c7 100644 --- a/jedi_language_server/server.py +++ b/jedi_language_server/server.py @@ -14,6 +14,7 @@ from pygls.features import ( CODE_ACTION, COMPLETION, + COMPLETION_ITEM_RESOLVE, DEFINITION, DOCUMENT_HIGHLIGHT, DOCUMENT_SYMBOL, @@ -32,6 +33,7 @@ CodeAction, CodeActionKind, CodeActionParams, + CompletionItem, CompletionList, CompletionParams, DidChangeTextDocumentParams, @@ -45,6 +47,7 @@ InitializeResult, Location, MarkupContent, + MarkupKind, RenameParams, SymbolInformation, TextDocumentPositionParams, @@ -115,6 +118,36 @@ def __init__(self, *args, **kwargs): # Server capabilities +@SERVER.feature(COMPLETION_ITEM_RESOLVE) +def completion_item_resolve( + server: JediLanguageServer, params: CompletionItem +) -> CompletionItem: + """Resolves documentation and detail of given completion item.""" + markup_kind = choose_markup(server) + # note: params is not a CompletionItem + # but a namedtuple complying with CompletionItem protocol + item = CompletionItem( + label=params.label, + kind=params.kind, + detail=params.detail, + documentation=params.documentation, + deprecated=params.deprecated, + preselect=params.preselect, + sort_text=params.sortText, + filter_text=params.filterText, + insert_text=params.insertText, + insert_text_format=params.insertTextFormat, + text_edit=params.textEdit, + additional_text_edits=params.additionalTextEdits, + commit_characters=params.commitCharacters, + command=params.command, + data=params.data, + ) + return jedi_utils.lsp_completion_item_resolve( + item, markup_kind=markup_kind + ) + + @SERVER.feature(COMPLETION, trigger_characters=[".", "'", '"']) def completion( server: JediLanguageServer, params: CompletionParams @@ -124,23 +157,16 @@ def completion( jedi_script = jedi_utils.script(server.project, document) jedi_lines = jedi_utils.line_column(jedi_script, params.position) completions_jedi = jedi_script.complete(**jedi_lines) - markup_preferred = ( - server.initialize_params.initializationOptions_markupKindPreferred - ) - markup_supported = ( - server.initialize_params.capabilities_textDocument_completion_completionItem_documentationFormat - ) - markup_kind = ( - markup_preferred - if markup_preferred in markup_supported - else markup_supported[0] - ) snippet_support = ( server.initialize_params.capabilities_textDocument_completion_completionItem_snippetSupport ) snippet_disable = ( server.initialize_params.initializationOptions_completion_disableSnippets ) + resolve_eagerly = ( + server.initialize_params.initializationOptions_completion_resolveEagerly + ) + markup_kind = choose_markup(server) is_import_context = jedi_utils.is_import( script_=jedi_script, line=jedi_lines["line"], @@ -153,11 +179,13 @@ def completion( document=server.workspace.get_document(params.textDocument.uri), position=params.position, ) + jedi_utils.clear_completions_cache() completion_items = [ jedi_utils.lsp_completion_item( - name=completion, + completion=completion, char_before_cursor=char_before_cursor, enable_snippets=enable_snippets, + resolve_eagerly=resolve_eagerly, markup_kind=markup_kind, ) for completion in completions_jedi @@ -255,17 +283,7 @@ def hover( docstring = name.docstring() if not docstring: continue - markup_preferred = ( - server.initialize_params.initializationOptions_markupKindPreferred - ) - markup_supported = ( - server.initialize_params.capabilities_textDocument_hover_contentFormat - ) - markup_kind = ( - markup_preferred - if markup_preferred in markup_supported - else markup_supported[0] - ) + markup_kind = choose_markup(server) contents = MarkupContent(kind=markup_kind, value=docstring) document = server.workspace.get_document(params.textDocument.uri) _range = pygls_utils.current_word_range(document, params.position) @@ -498,3 +516,18 @@ def did_change( def did_open(server: JediLanguageServer, params: DidOpenTextDocumentParams): """Actions run on textDocument/didOpen.""" _publish_diagnostics(server, params.textDocument.uri) + + +def choose_markup(server: JediLanguageServer) -> MarkupKind: + """Returns the preferred or first of supported markup kinds.""" + markup_preferred = ( + server.initialize_params.initializationOptions_markupKindPreferred + ) + markup_supported = ( + server.initialize_params.capabilities_textDocument_completion_completionItem_documentationFormat + ) + return ( + markup_preferred + if markup_preferred in markup_supported + else markup_supported[0] + )