import logging from typing import Optional from jedi import Script from jedi.api.classes import Name from pygls.lsp.methods import COMPLETION, DEFINITION, HOVER from pygls.lsp.types import (CompletionItem, CompletionItemKind, CompletionList, CompletionOptions, CompletionParams, DefinitionParams, Hover, HoverParams, InsertTextFormat, Location, Position, Range, TextDocumentPositionParams) 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(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()