ItalianSwirls/italianswirls/server.py

195 lines
6.5 KiB
Python

import logging
from typing import Optional
from jedi import Script
from jedi.api.classes import Name
from pygls.lsp.methods import COMPLETION, DEFINITION, HOVER, TYPE_DEFINITION
from pygls.lsp.types import (CompletionItem, CompletionItemKind,
CompletionList, CompletionOptions,
CompletionParams, DefinitionParams, Hover,
HoverParams, InsertTextFormat,
Location, Position, Range,
TextDocumentPositionParams, TypeDefinitionParams)
from pygls.server import LanguageServer
from pygls.workspace import Document
LOG = logging.getLogger()
LOG.setLevel(logging.DEBUG)
LOG.addHandler(logging.FileHandler("/tmp/a.log"))
LS = LanguageServer('italianswirls', 'v0.0.1')
JEDI_COMPLETION_TYPE_MAP = {
"module": CompletionItemKind.Module,
"class": CompletionItemKind.Class,
"instance": CompletionItemKind.Variable,
"function": CompletionItemKind.Function,
"param": CompletionItemKind.Variable,
"path": CompletionItemKind.File,
"keyword": CompletionItemKind.Keyword,
"property": CompletionItemKind.Property,
"statement": CompletionItemKind.Variable,
}
def get_jedi_script(document: Document) -> Script:
"""Get Jedi Script object from this document."""
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]:
"""Translate LSP Position to Jedi position (where line is 1-based)."""
return position.line + 1, position.character
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(
jedi_compl_type,
CompletionItemKind.Text
)
@LS.feature(COMPLETION, CompletionOptions(trigger_characters=["."]))
async def do_completion(
server: LanguageServer,
params: CompletionParams,
) -> CompletionList:
"""Return completion items."""
script = get_jedi_script_from_params(params, server)
jedi_position = get_jedi_position(params.position)
jedi_completions = script.complete(*jedi_position)
completion_items = []
for jedi_completion in jedi_completions:
name = jedi_completion.name
item = CompletionItem(
label=name,
filter_text=name,
kind=get_lsp_completion_kind(jedi_completion.type),
sort_text=name,
insert_text_name=name,
insert_text_format=InsertTextFormat.PlainText,
)
completion_items.append(item)
return CompletionList(
is_incomplete=False,
items=completion_items,
)
@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)
async def do_hover(
server: LanguageServer,
params: HoverParams,
) -> Optional[Hover]:
"""Provide "hover", which is the documentation of the target symbol.
Jedi provides a list of names with information, usually only one. We handle
them all and concatenate them, separated by a horizontal line. For
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.
"""
script = get_jedi_script_from_params(params, server)
jedi_position = get_jedi_position(params.position)
jedi_help_names = script.help(*jedi_position)
if not jedi_help_names:
return None
help_texts = []
for jedi_name in jedi_help_names:
text = ""
if full_name := jedi_name.full_name:
text += f"`{full_name}`\n"
if sigs := jedi_name.get_signatures():
text += "\n".join(f"`{sig.to_string()}`" for sig in sigs) + "\n"
if docstring := jedi_name.docstring(raw=True):
text += "\n" + docstring
if text:
help_texts.append(text)
if not help_texts:
return None
hover_text = "\n\n---\n\n".join(help_texts)
return Hover(contents=hover_text)
if __name__ == "__main__":
LS.start_io()