deepnote-python-lsp-server 0.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepnote_python_lsp_server-0.0.0.dist-info/METADATA +263 -0
- deepnote_python_lsp_server-0.0.0.dist-info/RECORD +43 -0
- deepnote_python_lsp_server-0.0.0.dist-info/WHEEL +6 -0
- deepnote_python_lsp_server-0.0.0.dist-info/entry_points.txt +24 -0
- deepnote_python_lsp_server-0.0.0.dist-info/licenses/LICENSE +22 -0
- deepnote_python_lsp_server-0.0.0.dist-info/top_level.txt +1 -0
- pylsp/__init__.py +33 -0
- pylsp/__main__.py +132 -0
- pylsp/_utils.py +325 -0
- pylsp/config/__init__.py +2 -0
- pylsp/config/config.py +202 -0
- pylsp/config/flake8_conf.py +64 -0
- pylsp/config/pycodestyle_conf.py +34 -0
- pylsp/config/source.py +88 -0
- pylsp/hookspecs.py +140 -0
- pylsp/lsp.py +115 -0
- pylsp/plugins/__init__.py +2 -0
- pylsp/plugins/_resolvers.py +132 -0
- pylsp/plugins/_rope_task_handle.py +106 -0
- pylsp/plugins/autopep8_format.py +95 -0
- pylsp/plugins/definition.py +84 -0
- pylsp/plugins/flake8_lint.py +243 -0
- pylsp/plugins/folding.py +210 -0
- pylsp/plugins/highlight.py +36 -0
- pylsp/plugins/hover.py +57 -0
- pylsp/plugins/jedi_completion.py +318 -0
- pylsp/plugins/jedi_rename.py +56 -0
- pylsp/plugins/mccabe_lint.py +56 -0
- pylsp/plugins/preload_imports.py +38 -0
- pylsp/plugins/pycodestyle_lint.py +113 -0
- pylsp/plugins/pydocstyle_lint.py +127 -0
- pylsp/plugins/pyflakes_lint.py +97 -0
- pylsp/plugins/pylint_lint.py +367 -0
- pylsp/plugins/references.py +33 -0
- pylsp/plugins/rope_autoimport.py +407 -0
- pylsp/plugins/rope_completion.py +161 -0
- pylsp/plugins/signature.py +81 -0
- pylsp/plugins/symbols.py +214 -0
- pylsp/plugins/yapf_format.py +199 -0
- pylsp/python_lsp.py +921 -0
- pylsp/text_edit.py +97 -0
- pylsp/uris.py +139 -0
- pylsp/workspace.py +730 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List
|
|
7
|
+
|
|
8
|
+
import jedi
|
|
9
|
+
|
|
10
|
+
from pylsp import _utils, hookimpl, uris
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from jedi.api import Script
|
|
14
|
+
from jedi.api.classes import Name
|
|
15
|
+
|
|
16
|
+
from pylsp.config.config import Config
|
|
17
|
+
from pylsp.workspace import Document
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MAX_JEDI_GOTO_HOPS = 100
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_definition(
|
|
26
|
+
maybe_defn: Name, script: Script, settings: Dict[str, Any]
|
|
27
|
+
) -> Name:
|
|
28
|
+
for _ in range(MAX_JEDI_GOTO_HOPS):
|
|
29
|
+
if maybe_defn.is_definition() or maybe_defn.module_path != script.path:
|
|
30
|
+
break
|
|
31
|
+
defns = script.goto(
|
|
32
|
+
follow_imports=settings.get("follow_imports", True),
|
|
33
|
+
follow_builtin_imports=settings.get("follow_builtin_imports", True),
|
|
34
|
+
line=maybe_defn.line,
|
|
35
|
+
column=maybe_defn.column,
|
|
36
|
+
)
|
|
37
|
+
if len(defns) == 1:
|
|
38
|
+
maybe_defn = defns[0]
|
|
39
|
+
else:
|
|
40
|
+
break
|
|
41
|
+
return maybe_defn
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@hookimpl
|
|
45
|
+
def pylsp_definitions(
|
|
46
|
+
config: Config, document: Document, position: Dict[str, int]
|
|
47
|
+
) -> List[Dict[str, Any]]:
|
|
48
|
+
settings = config.plugin_settings("jedi_definition")
|
|
49
|
+
code_position = _utils.position_to_jedi_linecolumn(document, position)
|
|
50
|
+
script = document.jedi_script(use_document_path=True)
|
|
51
|
+
auto_import_modules = jedi.settings.auto_import_modules
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
jedi.settings.auto_import_modules = []
|
|
55
|
+
definitions = script.goto(
|
|
56
|
+
follow_imports=settings.get("follow_imports", True),
|
|
57
|
+
follow_builtin_imports=settings.get("follow_builtin_imports", True),
|
|
58
|
+
**code_position,
|
|
59
|
+
)
|
|
60
|
+
definitions = [_resolve_definition(d, script, settings) for d in definitions]
|
|
61
|
+
finally:
|
|
62
|
+
jedi.settings.auto_import_modules = auto_import_modules
|
|
63
|
+
|
|
64
|
+
follow_builtin_defns = settings.get("follow_builtin_definitions", True)
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
"uri": uris.uri_with(document.uri, path=str(d.module_path)),
|
|
68
|
+
"range": {
|
|
69
|
+
"start": {"line": d.line - 1, "character": d.column},
|
|
70
|
+
"end": {"line": d.line - 1, "character": d.column + len(d.name)},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
for d in definitions
|
|
74
|
+
if d.is_definition() and (follow_builtin_defns or _not_internal_definition(d))
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _not_internal_definition(definition: Name) -> bool:
|
|
79
|
+
return (
|
|
80
|
+
definition.line is not None
|
|
81
|
+
and definition.column is not None
|
|
82
|
+
and definition.module_path is not None
|
|
83
|
+
and not definition.in_builtin_module()
|
|
84
|
+
)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
"""Linter pluging for flake8"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os.path
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import PurePath
|
|
11
|
+
from subprocess import PIPE, Popen
|
|
12
|
+
|
|
13
|
+
from flake8.plugins.pyflakes import FLAKE8_PYFLAKES_CODES
|
|
14
|
+
|
|
15
|
+
from pylsp import hookimpl, lsp
|
|
16
|
+
from pylsp.plugins.pyflakes_lint import PYFLAKES_ERROR_MESSAGES
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
FIX_IGNORES_RE = re.compile(r"([^a-zA-Z0-9_,]*;.*(\W+||$))")
|
|
21
|
+
UNNECESSITY_CODES = {
|
|
22
|
+
"F401", # `module` imported but unused
|
|
23
|
+
"F504", # % format unused named arguments
|
|
24
|
+
"F522", # .format(...) unused named arguments
|
|
25
|
+
"F523", # .format(...) unused positional arguments
|
|
26
|
+
"F841", # local variable `name` is assigned to but never used
|
|
27
|
+
}
|
|
28
|
+
# NOTE: If the user sets the flake8 executable with workspace configuration, the
|
|
29
|
+
# error codes in this set may be inaccurate.
|
|
30
|
+
ERROR_CODES = (
|
|
31
|
+
# Errors from the pyflakes plugin of flake8
|
|
32
|
+
{FLAKE8_PYFLAKES_CODES.get(m.__name__, "E999") for m in PYFLAKES_ERROR_MESSAGES}
|
|
33
|
+
# Syntax error from flake8 itself
|
|
34
|
+
| {"E999"}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@hookimpl
|
|
39
|
+
def pylsp_settings():
|
|
40
|
+
# Default flake8 to disabled
|
|
41
|
+
return {"plugins": {"flake8": {"enabled": False}}}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@hookimpl
|
|
45
|
+
def pylsp_lint(workspace, document):
|
|
46
|
+
with workspace.report_progress("lint: flake8"):
|
|
47
|
+
config = workspace._config
|
|
48
|
+
settings = config.plugin_settings("flake8", document_path=document.path)
|
|
49
|
+
log.debug("Got flake8 settings: %s", settings)
|
|
50
|
+
|
|
51
|
+
ignores = settings.get("ignore", [])
|
|
52
|
+
per_file_ignores = settings.get("perFileIgnores")
|
|
53
|
+
|
|
54
|
+
if per_file_ignores:
|
|
55
|
+
prev_file_pat = None
|
|
56
|
+
for path in per_file_ignores:
|
|
57
|
+
try:
|
|
58
|
+
file_pat, errors = path.split(":")
|
|
59
|
+
prev_file_pat = file_pat
|
|
60
|
+
except ValueError:
|
|
61
|
+
# It's legal to just specify another error type for the same
|
|
62
|
+
# file pattern:
|
|
63
|
+
if prev_file_pat is None:
|
|
64
|
+
log.warning("skipping a Per-file-ignore with no file pattern")
|
|
65
|
+
continue
|
|
66
|
+
file_pat = prev_file_pat
|
|
67
|
+
errors = path
|
|
68
|
+
if PurePath(document.path).match(file_pat):
|
|
69
|
+
ignores.extend(errors.split(","))
|
|
70
|
+
|
|
71
|
+
opts = {
|
|
72
|
+
"config": settings.get("config"),
|
|
73
|
+
"exclude": settings.get("exclude"),
|
|
74
|
+
"extend-ignore": settings.get("extendIgnore"),
|
|
75
|
+
"extend-select": settings.get("extendSelect"),
|
|
76
|
+
"filename": settings.get("filename"),
|
|
77
|
+
"hang-closing": settings.get("hangClosing"),
|
|
78
|
+
"ignore": ignores or None,
|
|
79
|
+
"max-complexity": settings.get("maxComplexity"),
|
|
80
|
+
"max-line-length": settings.get("maxLineLength"),
|
|
81
|
+
"indent-size": settings.get("indentSize"),
|
|
82
|
+
"select": settings.get("select"),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# flake takes only absolute path to the config. So we should check and
|
|
86
|
+
# convert if necessary
|
|
87
|
+
if opts.get("config") and not os.path.isabs(opts.get("config")):
|
|
88
|
+
opts["config"] = os.path.abspath(
|
|
89
|
+
os.path.expanduser(os.path.expandvars(opts.get("config")))
|
|
90
|
+
)
|
|
91
|
+
log.debug("using flake8 with config: %s", opts["config"])
|
|
92
|
+
|
|
93
|
+
# Call the flake8 utility then parse diagnostics from stdout
|
|
94
|
+
flake8_executable = settings.get("executable", "flake8")
|
|
95
|
+
|
|
96
|
+
args = build_args(opts)
|
|
97
|
+
|
|
98
|
+
# ensure the same source is used for flake8 execution and result parsing;
|
|
99
|
+
# single source access improves performance as it is only one disk access
|
|
100
|
+
source = document.source
|
|
101
|
+
output = run_flake8(flake8_executable, args, document, source)
|
|
102
|
+
return parse_stdout(source, output)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_flake8(flake8_executable, args, document, source):
|
|
106
|
+
"""Run flake8 with the provided arguments, logs errors
|
|
107
|
+
from stderr if any.
|
|
108
|
+
"""
|
|
109
|
+
# a quick temporary fix to deal with Atom
|
|
110
|
+
args = [
|
|
111
|
+
(i if not i.startswith("--ignore=") else FIX_IGNORES_RE.sub("", i))
|
|
112
|
+
for i in args
|
|
113
|
+
if i is not None
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
if document.path and document.path.startswith(document._workspace.root_path):
|
|
117
|
+
args.extend(
|
|
118
|
+
[
|
|
119
|
+
"--stdin-display-name",
|
|
120
|
+
os.path.relpath(document.path, document._workspace.root_path),
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# if executable looks like a path resolve it
|
|
125
|
+
if not os.path.isfile(flake8_executable) and os.sep in flake8_executable:
|
|
126
|
+
flake8_executable = os.path.abspath(
|
|
127
|
+
os.path.expanduser(os.path.expandvars(flake8_executable))
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
log.debug("Calling %s with args: '%s'", flake8_executable, args)
|
|
131
|
+
popen_kwargs = {}
|
|
132
|
+
if cwd := document._workspace.root_path:
|
|
133
|
+
popen_kwargs["cwd"] = cwd
|
|
134
|
+
try:
|
|
135
|
+
cmd = [flake8_executable]
|
|
136
|
+
cmd.extend(args)
|
|
137
|
+
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs)
|
|
138
|
+
except IOError:
|
|
139
|
+
log.debug(
|
|
140
|
+
"Can't execute %s. Trying with '%s -m flake8'",
|
|
141
|
+
flake8_executable,
|
|
142
|
+
sys.executable,
|
|
143
|
+
)
|
|
144
|
+
cmd = [sys.executable, "-m", "flake8"]
|
|
145
|
+
cmd.extend(args)
|
|
146
|
+
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs)
|
|
147
|
+
(stdout, stderr) = p.communicate(source.encode())
|
|
148
|
+
if stderr:
|
|
149
|
+
log.error("Error while running flake8 '%s'", stderr.decode())
|
|
150
|
+
return stdout.decode()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def build_args(options):
|
|
154
|
+
"""Build arguments for calling flake8.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
options: dictionary of argument names and their values.
|
|
158
|
+
"""
|
|
159
|
+
args = ["-"] # use stdin
|
|
160
|
+
for arg_name, arg_val in options.items():
|
|
161
|
+
if arg_val is None:
|
|
162
|
+
continue
|
|
163
|
+
arg = None
|
|
164
|
+
if isinstance(arg_val, list):
|
|
165
|
+
arg = "--{}={}".format(arg_name, ",".join(arg_val))
|
|
166
|
+
elif isinstance(arg_val, bool):
|
|
167
|
+
if arg_val:
|
|
168
|
+
arg = "--{}".format(arg_name)
|
|
169
|
+
else:
|
|
170
|
+
arg = "--{}={}".format(arg_name, arg_val)
|
|
171
|
+
args.append(arg)
|
|
172
|
+
return args
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def parse_stdout(source, stdout):
|
|
176
|
+
"""
|
|
177
|
+
Build a diagnostics from flake8's output, it should extract every result and format
|
|
178
|
+
it into a dict that looks like this:
|
|
179
|
+
{
|
|
180
|
+
'source': 'flake8',
|
|
181
|
+
'code': code, # 'E501'
|
|
182
|
+
'range': {
|
|
183
|
+
'start': {
|
|
184
|
+
'line': start_line,
|
|
185
|
+
'character': start_column,
|
|
186
|
+
},
|
|
187
|
+
'end': {
|
|
188
|
+
'line': end_line,
|
|
189
|
+
'character': end_column,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
'message': msg,
|
|
193
|
+
'severity': lsp.DiagnosticSeverity.*,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
document: The document to be linted.
|
|
198
|
+
stdout: output from flake8
|
|
199
|
+
Returns:
|
|
200
|
+
A list of dictionaries.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
document_lines = source.splitlines(True)
|
|
204
|
+
diagnostics = []
|
|
205
|
+
lines = stdout.splitlines()
|
|
206
|
+
for raw_line in lines:
|
|
207
|
+
parsed_line = re.match(r"(.*):(\d*):(\d*): (\w*) (.*)", raw_line)
|
|
208
|
+
if not parsed_line:
|
|
209
|
+
log.debug("Flake8 output parser can't parse line '%s'", raw_line)
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
parsed_line = parsed_line.groups()
|
|
213
|
+
if len(parsed_line) != 5:
|
|
214
|
+
log.debug("Flake8 output parser can't parse line '%s'", raw_line)
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
_, line, character, code, msg = parsed_line
|
|
218
|
+
line = int(line) - 1
|
|
219
|
+
character = int(character) - 1
|
|
220
|
+
# show also the code in message
|
|
221
|
+
msg = code + " " + msg
|
|
222
|
+
severity = lsp.DiagnosticSeverity.Warning
|
|
223
|
+
if code in ERROR_CODES:
|
|
224
|
+
severity = lsp.DiagnosticSeverity.Error
|
|
225
|
+
diagnostic = {
|
|
226
|
+
"source": "flake8",
|
|
227
|
+
"code": code,
|
|
228
|
+
"range": {
|
|
229
|
+
"start": {"line": line, "character": character},
|
|
230
|
+
"end": {
|
|
231
|
+
"line": line,
|
|
232
|
+
# no way to determine the column
|
|
233
|
+
"character": len(document_lines[line]),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
"message": msg,
|
|
237
|
+
"severity": severity,
|
|
238
|
+
}
|
|
239
|
+
if code in UNNECESSITY_CODES:
|
|
240
|
+
diagnostic["tags"] = [lsp.DiagnosticTag.Unnecessary]
|
|
241
|
+
diagnostics.append(diagnostic)
|
|
242
|
+
|
|
243
|
+
return diagnostics
|
pylsp/plugins/folding.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import parso
|
|
7
|
+
import parso.python.tree as tree_nodes
|
|
8
|
+
|
|
9
|
+
from pylsp import hookimpl
|
|
10
|
+
|
|
11
|
+
SKIP_NODES = (tree_nodes.Module, tree_nodes.IfStmt, tree_nodes.TryStmt)
|
|
12
|
+
IDENTATION_REGEX = re.compile(r"(\s+).+")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@hookimpl
|
|
16
|
+
def pylsp_folding_range(document):
|
|
17
|
+
program = document.source + "\n"
|
|
18
|
+
lines = program.splitlines()
|
|
19
|
+
tree = parso.parse(program)
|
|
20
|
+
ranges = __compute_folding_ranges(tree, lines)
|
|
21
|
+
|
|
22
|
+
results = []
|
|
23
|
+
for start_line, end_line in ranges:
|
|
24
|
+
start_line -= 1
|
|
25
|
+
end_line -= 1
|
|
26
|
+
# If start/end character is not defined, then it defaults to the
|
|
27
|
+
# corresponding line last character
|
|
28
|
+
results.append(
|
|
29
|
+
{
|
|
30
|
+
"startLine": start_line,
|
|
31
|
+
"endLine": end_line,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
return results
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __merge_folding_ranges(left, right):
|
|
38
|
+
for start in list(left.keys()):
|
|
39
|
+
right_start = right.pop(start, None)
|
|
40
|
+
if right_start is not None:
|
|
41
|
+
left[start] = max(right_start, start)
|
|
42
|
+
left.update(right)
|
|
43
|
+
return left
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __empty_identation_stack(
|
|
47
|
+
identation_stack, level_limits, current_line, folding_ranges
|
|
48
|
+
):
|
|
49
|
+
while identation_stack != []:
|
|
50
|
+
upper_level = identation_stack.pop(0)
|
|
51
|
+
level_start = level_limits.pop(upper_level)
|
|
52
|
+
folding_ranges.append((level_start, current_line))
|
|
53
|
+
return folding_ranges
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __match_identation_stack(
|
|
57
|
+
identation_stack, level, level_limits, folding_ranges, current_line
|
|
58
|
+
):
|
|
59
|
+
upper_level = identation_stack.pop(0)
|
|
60
|
+
while upper_level >= level:
|
|
61
|
+
level_start = level_limits.pop(upper_level)
|
|
62
|
+
folding_ranges.append((level_start, current_line))
|
|
63
|
+
upper_level = identation_stack.pop(0)
|
|
64
|
+
identation_stack.insert(0, upper_level)
|
|
65
|
+
return identation_stack, folding_ranges
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def __compute_folding_ranges_identation(text):
|
|
69
|
+
lines = text.splitlines()
|
|
70
|
+
folding_ranges = []
|
|
71
|
+
identation_stack = []
|
|
72
|
+
level_limits = {}
|
|
73
|
+
current_level = 0
|
|
74
|
+
current_line = 0
|
|
75
|
+
while lines[current_line] == "":
|
|
76
|
+
current_line += 1
|
|
77
|
+
for i, line in enumerate(lines):
|
|
78
|
+
if i < current_line:
|
|
79
|
+
continue
|
|
80
|
+
i += 1
|
|
81
|
+
identation_match = IDENTATION_REGEX.match(line)
|
|
82
|
+
if identation_match is not None:
|
|
83
|
+
whitespace = identation_match.group(1)
|
|
84
|
+
level = len(whitespace)
|
|
85
|
+
if level > current_level:
|
|
86
|
+
level_limits[current_level] = current_line
|
|
87
|
+
identation_stack.insert(0, current_level)
|
|
88
|
+
current_level = level
|
|
89
|
+
elif level < current_level:
|
|
90
|
+
identation_stack, folding_ranges = __match_identation_stack(
|
|
91
|
+
identation_stack, level, level_limits, folding_ranges, current_line
|
|
92
|
+
)
|
|
93
|
+
current_level = level
|
|
94
|
+
else:
|
|
95
|
+
folding_ranges = __empty_identation_stack(
|
|
96
|
+
identation_stack, level_limits, current_line, folding_ranges
|
|
97
|
+
)
|
|
98
|
+
current_level = 0
|
|
99
|
+
if line.strip() != "":
|
|
100
|
+
current_line = i
|
|
101
|
+
folding_ranges = __empty_identation_stack(
|
|
102
|
+
identation_stack, level_limits, current_line, folding_ranges
|
|
103
|
+
)
|
|
104
|
+
return dict(folding_ranges)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def __check_if_node_is_valid(node):
|
|
108
|
+
valid = True
|
|
109
|
+
if isinstance(node, tree_nodes.PythonNode):
|
|
110
|
+
kind = node.type
|
|
111
|
+
valid = kind not in {
|
|
112
|
+
"decorated",
|
|
113
|
+
"parameters",
|
|
114
|
+
"dictorsetmaker",
|
|
115
|
+
"testlist_comp",
|
|
116
|
+
}
|
|
117
|
+
if kind == "suite":
|
|
118
|
+
if isinstance(node.parent, tree_nodes.Function):
|
|
119
|
+
valid = False
|
|
120
|
+
return valid
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def __handle_skip(stack, skip):
|
|
124
|
+
body = stack[skip]
|
|
125
|
+
children = [body]
|
|
126
|
+
if hasattr(body, "children"):
|
|
127
|
+
children = body.children
|
|
128
|
+
stack = stack[:skip] + children + stack[skip + 1 :]
|
|
129
|
+
node = body
|
|
130
|
+
end_line, _ = body.end_pos
|
|
131
|
+
return node, end_line
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def __handle_flow_nodes(node, end_line, stack):
|
|
135
|
+
from_keyword = False
|
|
136
|
+
if isinstance(node, tree_nodes.Keyword):
|
|
137
|
+
from_keyword = True
|
|
138
|
+
if node.value in {"if", "elif", "with", "while"}:
|
|
139
|
+
node, end_line = __handle_skip(stack, 2)
|
|
140
|
+
elif node.value in {"except"}:
|
|
141
|
+
first_node = stack[0]
|
|
142
|
+
if isinstance(first_node, tree_nodes.Operator):
|
|
143
|
+
node, end_line = __handle_skip(stack, 1)
|
|
144
|
+
else:
|
|
145
|
+
node, end_line = __handle_skip(stack, 2)
|
|
146
|
+
elif node.value in {"for"}:
|
|
147
|
+
node, end_line = __handle_skip(stack, 4)
|
|
148
|
+
elif node.value in {"else"}:
|
|
149
|
+
node, end_line = __handle_skip(stack, 1)
|
|
150
|
+
return end_line, from_keyword, node, stack
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def __compute_start_end_lines(node, stack):
|
|
154
|
+
start_line, _ = node.start_pos
|
|
155
|
+
end_line, _ = node.end_pos
|
|
156
|
+
modified = False
|
|
157
|
+
end_line, from_keyword, node, stack = __handle_flow_nodes(node, end_line, stack)
|
|
158
|
+
|
|
159
|
+
last_leaf = node.get_last_leaf()
|
|
160
|
+
last_newline = isinstance(last_leaf, tree_nodes.Newline)
|
|
161
|
+
last_operator = isinstance(last_leaf, tree_nodes.Operator)
|
|
162
|
+
node_is_operator = isinstance(node, tree_nodes.Operator)
|
|
163
|
+
last_operator = last_operator or not node_is_operator
|
|
164
|
+
|
|
165
|
+
end_line -= 1
|
|
166
|
+
|
|
167
|
+
if isinstance(node.parent, tree_nodes.PythonNode) and not from_keyword:
|
|
168
|
+
kind = node.type
|
|
169
|
+
if kind in {"suite", "atom", "atom_expr", "arglist"}:
|
|
170
|
+
if len(stack) > 0:
|
|
171
|
+
next_node = stack[0]
|
|
172
|
+
next_line, _ = next_node.start_pos
|
|
173
|
+
if next_line > end_line:
|
|
174
|
+
end_line += 1
|
|
175
|
+
modified = True
|
|
176
|
+
if not last_newline and not modified and not last_operator:
|
|
177
|
+
end_line += 1
|
|
178
|
+
return start_line, end_line, stack
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def __compute_folding_ranges(tree, lines):
|
|
182
|
+
folding_ranges = {}
|
|
183
|
+
stack = [tree]
|
|
184
|
+
|
|
185
|
+
while len(stack) > 0:
|
|
186
|
+
node = stack.pop(0)
|
|
187
|
+
if isinstance(node, tree_nodes.Newline):
|
|
188
|
+
# Skip newline nodes
|
|
189
|
+
continue
|
|
190
|
+
if isinstance(node, tree_nodes.PythonErrorNode):
|
|
191
|
+
# Fallback to indentation-based (best-effort) folding
|
|
192
|
+
start_line, _ = node.start_pos
|
|
193
|
+
start_line -= 1
|
|
194
|
+
padding = [""] * start_line
|
|
195
|
+
text = "\n".join(padding + lines[start_line:]) + "\n"
|
|
196
|
+
identation_ranges = __compute_folding_ranges_identation(text)
|
|
197
|
+
folding_ranges = __merge_folding_ranges(folding_ranges, identation_ranges)
|
|
198
|
+
break
|
|
199
|
+
if not isinstance(node, SKIP_NODES):
|
|
200
|
+
valid = __check_if_node_is_valid(node)
|
|
201
|
+
if valid:
|
|
202
|
+
start_line, end_line, stack = __compute_start_end_lines(node, stack)
|
|
203
|
+
if end_line > start_line:
|
|
204
|
+
current_end = folding_ranges.get(start_line, -1)
|
|
205
|
+
folding_ranges[start_line] = max(current_end, end_line)
|
|
206
|
+
if hasattr(node, "children"):
|
|
207
|
+
stack = node.children + stack
|
|
208
|
+
|
|
209
|
+
folding_ranges = sorted(folding_ranges.items())
|
|
210
|
+
return folding_ranges
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from pylsp import _utils, hookimpl, lsp
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@hookimpl
|
|
12
|
+
def pylsp_document_highlight(document, position):
|
|
13
|
+
code_position = _utils.position_to_jedi_linecolumn(document, position)
|
|
14
|
+
usages = document.jedi_script().get_references(**code_position)
|
|
15
|
+
|
|
16
|
+
def is_valid(definition):
|
|
17
|
+
return definition.line is not None and definition.column is not None
|
|
18
|
+
|
|
19
|
+
def local_to_document(definition):
|
|
20
|
+
return (
|
|
21
|
+
not definition.module_path or str(definition.module_path) == document.path
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
"range": {
|
|
27
|
+
"start": {"line": d.line - 1, "character": d.column},
|
|
28
|
+
"end": {"line": d.line - 1, "character": d.column + len(d.name)},
|
|
29
|
+
},
|
|
30
|
+
"kind": lsp.DocumentHighlightKind.Write
|
|
31
|
+
if d.is_definition()
|
|
32
|
+
else lsp.DocumentHighlightKind.Read,
|
|
33
|
+
}
|
|
34
|
+
for d in usages
|
|
35
|
+
if is_valid(d) and local_to_document(d)
|
|
36
|
+
]
|
pylsp/plugins/hover.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from pylsp import _utils, hookimpl
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@hookimpl
|
|
12
|
+
def pylsp_hover(config, document, position):
|
|
13
|
+
code_position = _utils.position_to_jedi_linecolumn(document, position)
|
|
14
|
+
definitions = document.jedi_script(use_document_path=True).infer(**code_position)
|
|
15
|
+
word = document.word_at_position(position)
|
|
16
|
+
|
|
17
|
+
# Find first exact matching definition
|
|
18
|
+
definition = next((x for x in definitions if x.name == word), None)
|
|
19
|
+
|
|
20
|
+
# Ensure a definition is used if only one is available
|
|
21
|
+
# even if the word doesn't match. An example of this case is 'np'
|
|
22
|
+
# where 'numpy' doesn't match with 'np'. Same for NumPy ufuncs
|
|
23
|
+
if len(definitions) == 1:
|
|
24
|
+
definition = definitions[0]
|
|
25
|
+
|
|
26
|
+
if not definition:
|
|
27
|
+
return {"contents": ""}
|
|
28
|
+
|
|
29
|
+
hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {})
|
|
30
|
+
supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"])
|
|
31
|
+
preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
|
|
32
|
+
|
|
33
|
+
doc = _utils.format_docstring(definition.docstring(raw=True), preferred_markup_kind)["value"]
|
|
34
|
+
|
|
35
|
+
# Find first exact matching signature
|
|
36
|
+
signature = next(
|
|
37
|
+
(
|
|
38
|
+
x.to_string()
|
|
39
|
+
for x in definition.get_signatures()
|
|
40
|
+
if (x.name == word and x.type not in ["module"])
|
|
41
|
+
),
|
|
42
|
+
"",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
contents = []
|
|
46
|
+
if signature:
|
|
47
|
+
contents.append({
|
|
48
|
+
'language': 'python',
|
|
49
|
+
'value': signature,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if doc:
|
|
53
|
+
contents.append(doc)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"contents": contents or ''
|
|
57
|
+
}
|