Compare commits
No commits in common. "9c3f8d95f188b6ded415ba1a58c7912246492032" and "b22c9838b9232d8ecdf0efb685b746146e3ff382" have entirely different histories.
9c3f8d95f1
...
b22c9838b9
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.egg-info/
|
|
||||||
|
|
34
README.md
34
README.md
|
@ -6,18 +6,14 @@ Minimal Python language server, based on [Jedi][jedi] and [pygls][pygls].
|
||||||
[jedi]: https://jedi.readthedocs.io/en/latest/index.html
|
[jedi]: https://jedi.readthedocs.io/en/latest/index.html
|
||||||
[pygls]: https://pygls.readthedocs.io/en/latest/index.html
|
[pygls]: https://pygls.readthedocs.io/en/latest/index.html
|
||||||
|
|
||||||
Still in development but works on my machine. ✨
|
WIP! ⚠ 👷🚧
|
||||||
|
|
||||||
Supported features:
|
Supported features:
|
||||||
|
|
||||||
| LSP method | Description |
|
| LSP method | Description |
|
||||||
|-------------------------------|----------------------------|
|
|---------------------------|---------------|
|
||||||
| `textDocument/completion` | Complete |
|
| `textDocument/completion` | Completions |
|
||||||
| `textDocument/definition` | Go to definition |
|
| `textDocument/hover` | Documentation |
|
||||||
| `textDocument/typeDefinition` | Go to type definition |
|
|
||||||
| `textDocument/hover` | Show documentation |
|
|
||||||
| `textDocument/references` | Show references |
|
|
||||||
| `textDocument/rename` | Renaming symbols and files |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,21 +29,19 @@ About
|
||||||
|
|
||||||
### Why?
|
### Why?
|
||||||
|
|
||||||
General-purpose servers (e.g. pyls, py-lsp) try to do too much and break stuff
|
General-purpose servers (pyls, py-lsp) try to do too much and break stuff too
|
||||||
too often for me. Locking Neovim when I press tab, crashes of all kind,
|
often for me. Locking Neovim when I press tab, crashes of all kind, LspRestart
|
||||||
LspRestart failing. Also I like my linting and formatting done by dedicated
|
failing. Also I like my linting and formatting done by dedicated tools such as
|
||||||
tools such as [nvim-lint][nvim-lint] and [formatter][formatter].
|
[nvim-lint][nvim-lint] and [formatter][formatter].
|
||||||
|
|
||||||
[nvim-lint]: https://github.com/mfussenegger/nvim-lint
|
[nvim-lint]: https://github.com/mfussenegger/nvim-lint
|
||||||
[formatter]: https://github.com/mhartington/formatter.nvim
|
[formatter]: https://github.com/mhartington/formatter.nvim
|
||||||
|
|
||||||
Other Jedi-based servers (e.g. jedi-language-server) seem to focus on coc-nvim
|
Other Jedi-based servers seem to focus on coc-nvim and frequently fail on
|
||||||
and frequently fail on Neovim's native LSP client for me. I tried to fix
|
Neovim's native LSP client for me. I tried to fix jedi-language-server several
|
||||||
jedi-language-server several times when it failed me but I thought it could be
|
times when it failed me but thought it could be fun to try pygls to redo it as
|
||||||
fun to try pygls to redo it as small and simple as I can. And running a Node
|
small and simple as I can. And running a Node server to get Python completions?
|
||||||
server to get Python completions? No way. That said, jedi-language-server is a
|
HA!
|
||||||
good project and if you're fine with coc-nvim you should definitely check it
|
|
||||||
out. Lots of the code here is ~~stolen~~ inspired from this project.
|
|
||||||
|
|
||||||
### Why the name?
|
### Why the name?
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
from italianswirls.server import LS
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import logging
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.setLevel(logging.DEBUG)
|
|
||||||
root_logger.addHandler(logging.FileHandler("/tmp/a.log"))
|
|
||||||
LS.start_io()
|
|
|
@ -1,157 +0,0 @@
|
||||||
"""Glue between Jedi and LSP data structures."""
|
|
||||||
|
|
||||||
import bisect
|
|
||||||
import difflib
|
|
||||||
from typing import Generator, Optional, Union
|
|
||||||
|
|
||||||
from jedi.api.classes import Name
|
|
||||||
from jedi.api.refactoring import Refactoring
|
|
||||||
from pygls.lsp.types import (CompletionItemKind, Location, Position, Range,
|
|
||||||
RenameFile, RenameFileOptions,
|
|
||||||
ResourceOperationKind, TextDocumentEdit, TextEdit,
|
|
||||||
VersionedTextDocumentIdentifier)
|
|
||||||
from pygls.workspace import Workspace
|
|
||||||
|
|
||||||
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_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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_line_offsets(text: str) -> list[int]:
|
|
||||||
"""Return a list of indexes where each line of the text starts."""
|
|
||||||
line_offsets = []
|
|
||||||
offset = 0
|
|
||||||
for line in text.splitlines(keepends=True):
|
|
||||||
line_offsets.append(offset)
|
|
||||||
offset += len(line)
|
|
||||||
return line_offsets
|
|
||||||
|
|
||||||
|
|
||||||
def _get_lsp_position_from_offsets(
|
|
||||||
line_offsets: list[int],
|
|
||||||
offset: int,
|
|
||||||
) -> Position:
|
|
||||||
"""Return an LSP Position for this offset, using these line offsets."""
|
|
||||||
line_number = bisect.bisect_right(line_offsets, offset) - 1
|
|
||||||
character_number = offset - line_offsets[line_number]
|
|
||||||
return Position(line=line_number, character=character_number)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_document_edits(
|
|
||||||
refactoring: Refactoring,
|
|
||||||
workspace: Workspace,
|
|
||||||
) -> Generator[Union[TextDocumentEdit, RenameFile], None, None]:
|
|
||||||
"""Generate TextDocumentEdit and RenameFile objects from a refactoring."""
|
|
||||||
yield from _gen_document_text_edits(refactoring, workspace)
|
|
||||||
yield from _gen_document_renames(refactoring, workspace)
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_document_text_edits(
|
|
||||||
refactoring: Refactoring,
|
|
||||||
workspace: Workspace,
|
|
||||||
) -> Generator[TextDocumentEdit, None, None]:
|
|
||||||
"""Generate TextDocumentEdit objects for each text modification.
|
|
||||||
|
|
||||||
Compare previous code and refactored code using standard difflib. The main
|
|
||||||
complexity here is to translate difflib's opcode positions into LSP ranges.
|
|
||||||
This code is 99% taken from the neat jedi-language-server implementation.
|
|
||||||
"""
|
|
||||||
for path, changed_file in refactoring.get_changed_files().items():
|
|
||||||
document_uri = path.as_uri()
|
|
||||||
document = workspace.get_document(document_uri)
|
|
||||||
old_code = document.source
|
|
||||||
new_code = changed_file.get_new_code()
|
|
||||||
line_offsets = _build_line_offsets(old_code)
|
|
||||||
edit_operations = (
|
|
||||||
opcode for opcode in
|
|
||||||
difflib.SequenceMatcher(a=old_code, b=new_code).get_opcodes()
|
|
||||||
if opcode[0] != "equal"
|
|
||||||
)
|
|
||||||
text_edits: list[TextEdit] = []
|
|
||||||
for op, old_start, old_end, new_start, new_end in edit_operations:
|
|
||||||
new_text = new_code[new_start:new_end]
|
|
||||||
start_pos = _get_lsp_position_from_offsets(line_offsets, old_start)
|
|
||||||
end_pos = _get_lsp_position_from_offsets(line_offsets, old_end)
|
|
||||||
edit_range = Range(start=start_pos, end=end_pos)
|
|
||||||
text_edits.append(TextEdit(range=edit_range, new_text=new_text))
|
|
||||||
if not text_edits:
|
|
||||||
continue
|
|
||||||
|
|
||||||
document_id = VersionedTextDocumentIdentifier(
|
|
||||||
uri=document_uri,
|
|
||||||
version=document.version or 0,
|
|
||||||
)
|
|
||||||
yield TextDocumentEdit(
|
|
||||||
text_document=document_id,
|
|
||||||
edits=text_edits,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_document_renames(
|
|
||||||
refactoring: Refactoring,
|
|
||||||
workspace: Workspace,
|
|
||||||
) -> Generator[RenameFile, None, None]:
|
|
||||||
"""Generate RenameFile objects for each renamed file."""
|
|
||||||
for old_name, new_name in refactoring.get_renames():
|
|
||||||
yield RenameFile(
|
|
||||||
kind=ResourceOperationKind.Rename,
|
|
||||||
old_uri=old_name.as_uri(),
|
|
||||||
new_uri=new_name.as_uri(),
|
|
||||||
options=RenameFileOptions(ignore_if_exists=True, overwrite=True)
|
|
||||||
)
|
|
|
@ -2,23 +2,35 @@ import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from jedi import Script
|
from jedi import Script
|
||||||
from jedi.api.refactoring import RefactoringError
|
from jedi.api.classes import Name
|
||||||
from pygls.lsp.methods import (COMPLETION, DEFINITION, HOVER, REFERENCES,
|
from pygls.lsp.methods import COMPLETION, DEFINITION, HOVER, TYPE_DEFINITION
|
||||||
RENAME, TYPE_DEFINITION)
|
from pygls.lsp.types import (CompletionItem, CompletionItemKind,
|
||||||
from pygls.lsp.types import (CompletionItem, CompletionList, CompletionOptions,
|
CompletionList, CompletionOptions,
|
||||||
CompletionParams, DefinitionParams, Hover,
|
CompletionParams, DefinitionParams, Hover,
|
||||||
HoverParams, InsertTextFormat, Location,
|
HoverParams, InsertTextFormat,
|
||||||
ReferenceParams, RenameParams,
|
Location, Position, Range,
|
||||||
TextDocumentPositionParams, TypeDefinitionParams,
|
TextDocumentPositionParams, TypeDefinitionParams)
|
||||||
WorkspaceEdit)
|
|
||||||
from pygls.server import LanguageServer
|
from pygls.server import LanguageServer
|
||||||
from pygls.workspace import Document
|
from pygls.workspace import Document
|
||||||
|
|
||||||
from italianswirls.glue import (gen_document_edits, get_jedi_position,
|
LOG = logging.getLogger()
|
||||||
get_lsp_completion_kind, get_lsp_locations)
|
LOG.setLevel(logging.DEBUG)
|
||||||
|
LOG.addHandler(logging.FileHandler("/tmp/a.log"))
|
||||||
|
|
||||||
LS = LanguageServer('italianswirls', 'v0.0.1')
|
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:
|
def get_jedi_script(document: Document) -> Script:
|
||||||
"""Get Jedi Script object from this document."""
|
"""Get Jedi Script object from this document."""
|
||||||
|
@ -36,6 +48,54 @@ def get_jedi_script_from_params(
|
||||||
return script
|
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=["."]))
|
@LS.feature(COMPLETION, CompletionOptions(trigger_characters=["."]))
|
||||||
async def do_completion(
|
async def do_completion(
|
||||||
server: LanguageServer,
|
server: LanguageServer,
|
||||||
|
@ -93,18 +153,6 @@ async def do_type_definition(
|
||||||
return get_lsp_locations(jedi_names) or None
|
return get_lsp_locations(jedi_names) or None
|
||||||
|
|
||||||
|
|
||||||
@LS.feature(REFERENCES)
|
|
||||||
async def do_references(
|
|
||||||
server: LanguageServer,
|
|
||||||
params: ReferenceParams,
|
|
||||||
) -> 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.get_references(*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,
|
||||||
|
@ -142,20 +190,5 @@ async def do_hover(
|
||||||
return Hover(contents=hover_text)
|
return Hover(contents=hover_text)
|
||||||
|
|
||||||
|
|
||||||
@LS.feature(RENAME)
|
if __name__ == "__main__":
|
||||||
async def do_rename(
|
LS.start_io()
|
||||||
server: LanguageServer,
|
|
||||||
params: RenameParams,
|
|
||||||
) -> Optional[WorkspaceEdit]:
|
|
||||||
"""Ask Jedi to rename a symbol and return the resulting state."""
|
|
||||||
script = get_jedi_script_from_params(params, server)
|
|
||||||
jedi_position = get_jedi_position(params.position)
|
|
||||||
try:
|
|
||||||
refactoring = script.rename(*jedi_position, new_name=params.new_name)
|
|
||||||
except RefactoringError as exc:
|
|
||||||
logging.error(f"Refactoring failed: {exc}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
changes = list(gen_document_edits(refactoring, server.workspace))
|
|
||||||
logging.info(f"changes: {changes}")
|
|
||||||
return WorkspaceEdit(document_changes=changes) if changes else None
|
|
||||||
|
|
6
mypy.ini
6
mypy.ini
|
@ -1,6 +0,0 @@
|
||||||
[mypy]
|
|
||||||
warn_return_any = True
|
|
||||||
warn_unused_configs = True
|
|
||||||
|
|
||||||
zsh:1: command not found: a
|
|
||||||
ignore_missing_imports = True
|
|
|
@ -1,3 +0,0 @@
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
Loading…
Reference in a new issue