diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f788a..8d3335a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.5.0 + +### Added + +- Support for Jedi `0.17` + +### Changed + +- Major internal updates to helper functions. Jedi `0.17` has a different public API. + +### Removed + +- Remove support for Workspace symbols. I never used this feature and I figure we can do this better with Jedi's new project constructs. +- Remove support for any version of Jedi before `0.17`. If you must use an older Jedi, stick to `0.4.2`. + ## 0.4.2 ### Changed diff --git a/README.md b/README.md index 331e61e..00ca0af 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,12 @@ pip install -U jedi jedi-language-server jedi-language-server aims to support all of Jedi's capabilities and expose them through the Language Server Protocol. It currently supports the following Language Server requests: -* [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) -* [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol) -* [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover) -* [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references) -* [textDocument/rename](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename) -* [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol) +- [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) +- [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol) +- [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover) +- [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references) +- [textDocument/rename](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename) These language server requests are not currently configurable by the user, but we expect to relax this constraint in a future release. @@ -107,12 +106,12 @@ To build and run this project from source: Install the following tools manually: -* [Poetry](https://github.com/sdispater/poetry#installation) -* [GNU Make](https://www.gnu.org/software/make/) +- [Poetry](https://github.com/sdispater/poetry#installation) +- [GNU Make](https://www.gnu.org/software/make/) #### Recommended -* [asdf](https://github.com/asdf-vm/asdf) +- [asdf](https://github.com/asdf-vm/asdf) ### Get source code @@ -139,10 +138,10 @@ make test Palantir's [python-language-server](https://github.com/palantir/python-language-server) inspired this project. Unlike python-language-server, jedi-language-server: -* Uses `pygls` instead of creating its own low-level Language Server Protocol bindings -* Supports one powerful 3rd party library: Jedi. By only supporting Jedi, we can focus on supporting all Jedi features without exposing ourselves to too many broken 3rd party dependencies (I'm looking at you, [rope](https://github.com/python-rope/rope)). -* Is supremely simple because of its scope constraints. Leave complexity to the Jedi [master](https://github.com/davidhalter). If the force is strong with you, please submit a PR! +- Uses `pygls` instead of creating its own low-level Language Server Protocol bindings +- Supports one powerful 3rd party library: Jedi. By only supporting Jedi, we can focus on supporting all Jedi features without exposing ourselves to too many broken 3rd party dependencies (I'm looking at you, [rope](https://github.com/python-rope/rope)). +- Is supremely simple because of its scope constraints. Leave complexity to the Jedi [master](https://github.com/davidhalter). If the force is strong with you, please submit a PR! ## Written by -Samuel Roeca *samuel.roeca@gmail.com* +Samuel Roeca _samuel.roeca@gmail.com_ diff --git a/jedi_language_server/server.py b/jedi_language_server/server.py index ed8bbc2..81dea67 100644 --- a/jedi_language_server/server.py +++ b/jedi_language_server/server.py @@ -16,7 +16,6 @@ HOVER, REFERENCES, RENAME, - WORKSPACE_SYMBOL, ) from pygls.server import LanguageServer from pygls.types import ( @@ -31,15 +30,12 @@ TextDocumentPositionParams, TextEdit, WorkspaceEdit, - WorkspaceSymbolParams, ) from .server_utils import ( - get_jedi_document_names, get_jedi_script, - get_jedi_workspace_names, - get_location_from_definition, - get_symbol_information_from_definition, + get_location_from_name, + get_symbol_information_from_name, ) from .type_map import get_lsp_completion_type @@ -47,10 +43,12 @@ @SERVER.feature(COMPLETION, triggerCharacters=["."]) -def lsp_completion(server: LanguageServer, params: CompletionParams = None): +def lsp_completion(server: LanguageServer, params: CompletionParams): """Returns completion items.""" - script = get_jedi_script(server, params) - jedi_completions = script.completions() + script = get_jedi_script(server, params.textDocument.uri) + jedi_completions = script.complete( + line=params.position.line + 1, column=params.position.character, + ) return CompletionList( is_incomplete=False, items=[ @@ -71,13 +69,14 @@ def lsp_definition( server: LanguageServer, params: TextDocumentPositionParams ) -> List[Location]: """Support Goto Definition""" - script = get_jedi_script(server, params) - definitions = script.goto_assignments( - follow_imports=True, follow_builtin_imports=True + script = get_jedi_script(server, params.textDocument.uri) + names = script.goto( + line=params.position.line + 1, + column=params.position.character, + follow_imports=True, + follow_builtin_imports=True, ) - return [ - get_location_from_definition(definition) for definition in definitions - ] + return [get_location_from_name(name) for name in names] @SERVER.feature(HOVER) @@ -85,13 +84,13 @@ def lsp_hover( server: LanguageServer, params: TextDocumentPositionParams ) -> Hover: """Support the hover feature""" - script = get_jedi_script(server, params) - definitions = script.goto_definitions() + script = get_jedi_script(server, params.textDocument.uri) + names = script.infer( + line=params.position.line + 1, column=params.position.character, + ) return Hover( contents=( - definitions[0].docstring() - if definitions - else "No docstring definition found." + names[0].docstring() if names else "No docstring definition found." ) ) @@ -101,14 +100,14 @@ def lsp_references( server: LanguageServer, params: TextDocumentPositionParams ) -> List[Location]: """Obtain all references to document""" - script = get_jedi_script(server, params) + script = get_jedi_script(server, params.textDocument.uri) try: - definitions = script.usages() + names = script.get_references( + line=params.position.line + 1, column=params.position.character, + ) except Exception: # pylint: disable=broad-except return [] - return [ - get_location_from_definition(definition) for definition in definitions - ] + return [get_location_from_name(name) for name in names] @SERVER.feature(RENAME) @@ -116,14 +115,14 @@ def lsp_rename( server: LanguageServer, params: RenameParams ) -> Optional[WorkspaceEdit]: """Rename a symbol across a workspace""" - script = get_jedi_script(server, params) + script = get_jedi_script(server, params.textDocument.uri) try: - definitions = script.usages() + names = script.get_references( + line=params.position.line + 1, column=params.position.character, + ) except Exception: # pylint: disable=broad-except return None - locations = [ - get_location_from_definition(definition) for definition in definitions - ] + locations = [get_location_from_name(name) for name in names] if not locations: return None changes = {} # type: Dict[str, List[TextEdit]] @@ -141,21 +140,11 @@ def lsp_document_symbol( server: LanguageServer, params: DocumentSymbolParams ) -> List[SymbolInformation]: """Document Python document symbols""" - jedi_names = get_jedi_document_names(server, params) - return [ - get_symbol_information_from_definition(definition) - for definition in jedi_names - ] - - -@SERVER.feature(WORKSPACE_SYMBOL) -def lsp_workspace_symbol( - server: LanguageServer, - params: WorkspaceSymbolParams, # pylint: disable=unused-argument -) -> List[SymbolInformation]: - """Document Python workspace symbols""" - jedi_names = get_jedi_workspace_names(server) - return [ - get_symbol_information_from_definition(definition) - for definition in jedi_names - ] + script = get_jedi_script(server, params.textDocument.uri) + try: + names = script.get_names( + line=params.position.line + 1, column=params.position.character, + ) + except Exception: # pylint: disable=broad-except + return [] + return [get_symbol_information_from_name(name) for name in names] diff --git a/jedi_language_server/server_utils.py b/jedi_language_server/server_utils.py index 3019793..ef8f364 100644 --- a/jedi_language_server/server_utils.py +++ b/jedi_language_server/server_utils.py @@ -1,132 +1,44 @@ """Utility functions used by the language server""" -import itertools -import os -import subprocess -from typing import List, Optional, Union +from typing import Optional -import jedi.api -from jedi import Script -from jedi.api.classes import Definition +from jedi import Project, Script +from jedi.api.classes import Name from jedi.api.environment import get_cached_default_environment -from pygls.server import LanguageServer, Workspace -from pygls.types import ( - DocumentSymbolParams, - Location, - Position, - Range, - RenameParams, - SymbolInformation, - TextDocumentItem, - TextDocumentPositionParams, -) +from pygls.server import LanguageServer +from pygls.types import Location, Position, Range, SymbolInformation from pygls.uris import from_fs_path from .type_map import get_lsp_symbol_type -def get_jedi_script( - server: LanguageServer, - params: Union[TextDocumentPositionParams, RenameParams], -) -> Script: +def get_jedi_script(server: LanguageServer, doc_uri: str) -> Script: """Simplifies getting jedi Script + :param doc_uri: the uri for the LSP text document. Obtained through + params.textDocument.uri + NOTE: * jedi is 1-indexed for lines and 0-indexed for columns * LSP is 0-indexed for lines and 0-indexed for columns * Therefore, add 1 to LSP's request for the line """ workspace = server.workspace - text_doc = workspace.get_document(params.textDocument.uri) - return Script( - source=text_doc.source, - path=text_doc.path, - line=params.position.line + 1, - column=params.position.character, - environment=get_cached_default_environment(), + text_doc = workspace.get_document(doc_uri) + project = Project( + path=workspace.root_path, + smart_sys_path=True, + load_unsafe_extensions=False, ) - - -def get_jedi_document_names( - server: LanguageServer, params: DocumentSymbolParams, -) -> List[Definition]: - """Get a list of document names from Jedi""" - workspace = server.workspace - text_doc = workspace.get_document(params.textDocument.uri) - return jedi.api.names( - source=text_doc.source, + return Script( + code=text_doc.source, path=text_doc.path, - all_scopes=True, - definitions=True, - references=False, + project=project, environment=get_cached_default_environment(), ) -class WorkspaceDocuments: - """A class to manage one-time gathering of workspace documents""" - - # pylint: disable=too-few-public-methods - - def __init__(self) -> None: - self.gathered_names = False - - def gather_workspace_names(self, workspace: Workspace) -> None: - """Collect the workspace names""" - if not self.gathered_names: - git_path = os.path.join(workspace.root_path, ".git") - git_command = "git --git-dir=" + git_path + " ls-files --full-name" - try: - git_output = subprocess.check_output(git_command, shell=True) - except subprocess.CalledProcessError: - self.gathered_names = True - return - project_paths = git_output.decode("utf-8").splitlines() - for doc_path in project_paths: - if os.path.splitext(doc_path)[1] == ".py": - full_doc_path = os.path.join(workspace.root_path, doc_path) - doc_uri = from_fs_path(full_doc_path) - with open(full_doc_path) as infile: - text = infile.read() - workspace.put_document( - TextDocumentItem( - uri=doc_uri, - language_id="python", - version=0, - text=text, - ) - ) - self.gathered_names = True - - -_WORKSPACE_DOCUMENTS = WorkspaceDocuments() - - -def get_jedi_workspace_names(server: LanguageServer) -> List[Definition]: - """Get a list of workspace names from Jedi""" - workspace = server.workspace - _WORKSPACE_DOCUMENTS.gather_workspace_names(workspace) - definitions_by_document = ( - jedi.api.names( - source=text_doc.source, - path=text_doc.path, - all_scopes=True, - definitions=True, - references=False, - environment=get_cached_default_environment(), - ) - for text_doc in workspace.documents.values() - if text_doc and os.path.splitext(text_doc.path)[1] == ".py" - ) - return list( - itertools.chain.from_iterable( - definitions if definitions else [] - for definitions in definitions_by_document - ) - ) - - -def get_location_from_definition(definition: Definition) -> Location: +def get_location_from_name(name: Name) -> Location: """Get LSP location from Jedi definition NOTE: @@ -135,38 +47,33 @@ def get_location_from_definition(definition: Definition) -> Location: * Therefore, subtract 1 from Jedi's definition line """ return Location( - uri=from_fs_path(definition.module_path), + uri=from_fs_path(name.module_path), range=Range( - start=Position( - line=definition.line - 1, character=definition.column - ), + start=Position(line=name.line - 1, character=name.column), end=Position( - line=definition.line - 1, - character=definition.column + len(definition.name), + line=name.line - 1, character=name.column + len(name.name), ), ), ) -def get_symbol_information_from_definition( - definition: Definition, -) -> SymbolInformation: +def get_symbol_information_from_name(name: Name,) -> SymbolInformation: """Get LSP SymbolInformation from Jedi definition""" return SymbolInformation( - name=definition.name, - kind=get_lsp_symbol_type(definition.type), - location=get_location_from_definition(definition), - container_name=get_jedi_parent_name(definition), + name=name.name, + kind=get_lsp_symbol_type(name.type), + location=get_location_from_name(name), + container_name=get_jedi_parent_name(name), ) -def get_jedi_parent_name(definition: Definition) -> Optional[str]: +def get_jedi_parent_name(name: Name) -> Optional[str]: """Retrieve the parent name from Jedi Error prone Jedi calls are wrapped in try/except to avoid """ try: - parent = definition.parent() + parent = name.parent() except Exception: # pylint: disable=broad-except return None return parent.name if parent and parent.parent() else None diff --git a/poetry.lock b/poetry.lock index 5c4e444..36960b7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -185,15 +185,15 @@ category = "main" description = "An autocompletion tool for Python that can be used for text editors." name = "jedi" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.16.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.17.0" [package.dependencies] -parso = ">=0.5.2" +parso = ">=0.7.0" [package.extras] qa = ["flake8 (3.7.9)"] -testing = ["colorama (0.4.1)", "docopt", "pytest (>=3.9.0,<5.0.0)"] +testing = ["colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] category = "dev" @@ -557,7 +557,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "952fec8602b10a6b302725d593b343a5569bb56a8395760876146ec408275069" +content-hash = "a8a4cf0b950bdd3e93c8d57f82db4ada9b2e99b382a99887bf5738e6220b65d6" python-versions = "^3.6" [metadata.files] @@ -621,8 +621,8 @@ isort = [ {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, ] jedi = [ - {file = "jedi-0.16.0-py2.py3-none-any.whl", hash = "sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2"}, - {file = "jedi-0.16.0.tar.gz", hash = "sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5"}, + {file = "jedi-0.17.0-py2.py3-none-any.whl", hash = "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798"}, + {file = "jedi-0.17.0.tar.gz", hash = "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, diff --git a/pyproject.toml b/pyproject.toml index 91a91e7..4899076 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include_trailing_comma = true [tool.poetry] name = "jedi-language-server" -version = "0.4.2" +version = "0.5.0" description = "A language server for Jedi!" authors = ["Sam Roeca "] readme = "README.md" @@ -45,7 +45,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.6" click = ">=7.0" -jedi = ">=0.15.1,<0.17.0" +jedi = ">=0.17.0" pygls = ">=0.8.1" [tool.poetry.dev-dependencies]