Compare commits

...

4 commits

Author SHA1 Message Date
dece b22c9838b9 server: implement type definition 2022-12-03 18:06:05 +01:00
dece e20406d00b requirements: pin pygls to latest 2022-12-03 18:05:50 +01:00
dece 6cf1b28345 server: implement goto definition 2022-12-03 18:05:36 +01:00
dece 2e842adc88 server: enhance hover 2022-12-03 16:48:13 +01:00
2 changed files with 107 additions and 28 deletions

View file

@ -1,11 +1,15 @@
import logging import logging
from typing import Optional
from jedi import Script from jedi import Script
from pygls.lsp.methods import COMPLETION, HOVER, INITIALIZE from jedi.api.classes import Name
from pygls.lsp.methods import COMPLETION, DEFINITION, HOVER, TYPE_DEFINITION
from pygls.lsp.types import (CompletionItem, CompletionItemKind, from pygls.lsp.types import (CompletionItem, CompletionItemKind,
CompletionList, CompletionOptions, CompletionList, CompletionOptions,
CompletionParams, Hover, InsertTextFormat, CompletionParams, DefinitionParams, Hover,
Position, TextDocumentPositionParams) HoverParams, InsertTextFormat,
Location, Position, Range,
TextDocumentPositionParams, TypeDefinitionParams)
from pygls.server import LanguageServer from pygls.server import LanguageServer
from pygls.workspace import Document from pygls.workspace import Document
@ -29,36 +33,76 @@ JEDI_COMPLETION_TYPE_MAP = {
def get_jedi_script(document: Document) -> Script: def get_jedi_script(document: Document) -> Script:
"""Get Jedi Script object from this document and project.""" """Get Jedi Script object from this document."""
return Script(code=document.source, path=document.path) return Script(code=document.source, path=document.path)
def get_jedi_script_from_params(
params: TextDocumentPositionParams,
server: LanguageServer
) -> Script:
"""Get Jedi Script using text document params provided by the client."""
document_uri = params.text_document.uri
document = server.workspace.get_document(document_uri)
script = get_jedi_script(document)
return script
def get_jedi_position(position: Position) -> tuple[int, int]: def get_jedi_position(position: Position) -> tuple[int, int]:
"""Translate pygls's Position to Jedi position (line is 1-based).""" """Translate LSP Position to Jedi position (where line is 1-based)."""
return position.line + 1, position.character return position.line + 1, position.character
def get_pygls_compl_kind(jedi_compl_type: str) -> CompletionItemKind: def get_lsp_position(line: int, column: int) -> Position:
"""Translate Jedi position to LSP Position (where line is 0-based)."""
return Position(line=line - 1, character=column)
def get_lsp_range(name: Name) -> Optional[Range]:
"""Get an LSP range for this name, if it has a location."""
if name.line is None or name.column is None:
return None
start_position = get_lsp_position(name.line, name.column)
end_position = get_lsp_position(name.line, name.column + len(name.name))
return Range(start=start_position, end=end_position)
def get_lsp_location(name: Name) -> Optional[Location]:
"""Return an LSP location from this Jedi Name."""
if name.module_path is None:
return None
if (lsp_range := get_lsp_range(name)) is None:
return None
return Location(uri=name.module_path.as_uri(), range=lsp_range)
def get_lsp_locations(names: list[Name]) -> list[Location]:
"""Return a list of LSP locations from this list of Jedi Names.
Names that cannot be converted to a LSP location are discarded.
"""
lsp_locations = []
for name in names:
if lsp_location := get_lsp_location(name):
lsp_locations.append(lsp_location)
return lsp_locations
def get_lsp_completion_kind(jedi_compl_type: str) -> CompletionItemKind:
"""Return an LSP completion item kind from this Jedi completion type."""
return JEDI_COMPLETION_TYPE_MAP.get( return JEDI_COMPLETION_TYPE_MAP.get(
jedi_compl_type, jedi_compl_type,
CompletionItemKind.Text CompletionItemKind.Text
) )
@LS.feature(INITIALIZE)
async def do_initialize(*args):
LOG.debug("do_initialize 👋")
@LS.feature(COMPLETION, CompletionOptions(trigger_characters=["."])) @LS.feature(COMPLETION, CompletionOptions(trigger_characters=["."]))
async def do_completion( async def do_completion(
server: LanguageServer, server: LanguageServer,
params: CompletionParams, params: CompletionParams,
) -> CompletionList: ) -> CompletionList:
"""Return completion items.""" """Return completion items."""
document_uri = params.text_document.uri script = get_jedi_script_from_params(params, server)
document = server.workspace.get_document(document_uri)
script = get_jedi_script(document)
jedi_position = get_jedi_position(params.position) jedi_position = get_jedi_position(params.position)
jedi_completions = script.complete(*jedi_position) jedi_completions = script.complete(*jedi_position)
@ -68,7 +112,7 @@ async def do_completion(
item = CompletionItem( item = CompletionItem(
label=name, label=name,
filter_text=name, filter_text=name,
kind=get_pygls_compl_kind(jedi_completion.type), kind=get_lsp_completion_kind(jedi_completion.type),
sort_text=name, sort_text=name,
insert_text_name=name, insert_text_name=name,
insert_text_format=InsertTextFormat.PlainText, insert_text_format=InsertTextFormat.PlainText,
@ -81,34 +125,69 @@ async def do_completion(
) )
@LS.feature(DEFINITION)
async def do_definition(
server: LanguageServer,
params: DefinitionParams,
) -> Optional[list[Location]]:
"""Return the definition location(s) of the target symbol."""
script = get_jedi_script_from_params(params, server)
jedi_position = get_jedi_position(params.position)
jedi_names = script.goto(
*jedi_position,
follow_imports=True,
follow_builtin_imports=True,
)
return get_lsp_locations(jedi_names) or None
@LS.feature(TYPE_DEFINITION)
async def do_type_definition(
server: LanguageServer,
params: TypeDefinitionParams,
) -> Optional[list[Location]]:
"""Return the type definition location(s) of the target symbol."""
script = get_jedi_script_from_params(params, server)
jedi_position = get_jedi_position(params.position)
jedi_names = script.infer(*jedi_position)
return get_lsp_locations(jedi_names) or None
@LS.feature(HOVER) @LS.feature(HOVER)
async def do_hover( async def do_hover(
server: LanguageServer, server: LanguageServer,
params: TextDocumentPositionParams, params: HoverParams,
) -> Hover: ) -> Optional[Hover]:
"""Provide "hover", which is documentation of a symbol. """Provide "hover", which is the documentation of the target symbol.
Jedi provides a list of names with information, usually only one. We handle Jedi provides a list of names with information, usually only one. We handle
them all and concatenate them, separated by a horizontal line. For them all and concatenate them, separated by a horizontal line. For
simplicity, the text is mostly provided untouched. simplicity, the text is mostly provided untouched, including docstrings, so
if your client tries to interpret it as Markdown even though there are
rogue `**kwargs` hanging around you might have a few display issues.
""" """
document_uri = params.text_document.uri script = get_jedi_script_from_params(params, server)
document = server.workspace.get_document(document_uri)
script = get_jedi_script(document)
jedi_position = get_jedi_position(params.position) jedi_position = get_jedi_position(params.position)
jedi_help_names = script.help(*jedi_position) jedi_help_names = script.help(*jedi_position)
if not jedi_help_names:
return None
help_texts = [] help_texts = []
for jedi_name in jedi_help_names: for jedi_name in jedi_help_names:
text = f"`{jedi_name.full_name}`" text = ""
if full_name := jedi_name.full_name:
text += f"`{full_name}`\n"
if sigs := jedi_name.get_signatures(): if sigs := jedi_name.get_signatures():
text += "\n" + "\n".join(f"`{sig.to_string()}`" for sig in sigs) text += "\n".join(f"`{sig.to_string()}`" for sig in sigs) + "\n"
if docstring := jedi_name.docstring(raw=True): if docstring := jedi_name.docstring(raw=True):
text += "\n\n" + docstring text += "\n" + docstring
if text:
help_texts.append(text) help_texts.append(text)
if not help_texts:
return None
hover_text = "\n\n---\n\n".join(help_texts) hover_text = "\n\n---\n\n".join(help_texts)
return Hover(contents=hover_text) # TODO range return Hover(contents=hover_text)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,2 +1,2 @@
pygls pygls ~= 0.13.1
jedi ~= 0.18.2 jedi ~= 0.18.2