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,407 @@
|
|
|
1
|
+
# Copyright 2022- Python Language Server Contributors.
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any, Dict, Generator, List, Optional, Set, Union
|
|
6
|
+
|
|
7
|
+
import parso
|
|
8
|
+
from jedi import Script
|
|
9
|
+
from parso.python import tree
|
|
10
|
+
from parso.tree import NodeOrLeaf
|
|
11
|
+
from rope.base.resources import Resource
|
|
12
|
+
from rope.contrib.autoimport.defs import SearchResult
|
|
13
|
+
from rope.contrib.autoimport.sqlite import AutoImport
|
|
14
|
+
|
|
15
|
+
from pylsp import hookimpl
|
|
16
|
+
from pylsp.config.config import Config
|
|
17
|
+
from pylsp.workspace import Document, Workspace
|
|
18
|
+
|
|
19
|
+
from ._rope_task_handle import PylspTaskHandle
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_score_pow = 5
|
|
24
|
+
_score_max = 10**_score_pow
|
|
25
|
+
MAX_RESULTS_COMPLETIONS = 1000
|
|
26
|
+
MAX_RESULTS_CODE_ACTIONS = 5
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AutoimportCache:
|
|
30
|
+
"""Handles the cache creation."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self.thread = None
|
|
34
|
+
|
|
35
|
+
def reload_cache(
|
|
36
|
+
self,
|
|
37
|
+
config: Config,
|
|
38
|
+
workspace: Workspace,
|
|
39
|
+
files: Optional[List[Document]] = None,
|
|
40
|
+
single_thread: Optional[bool] = True,
|
|
41
|
+
):
|
|
42
|
+
if self.is_blocked():
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
memory: bool = config.plugin_settings("rope_autoimport").get("memory", False)
|
|
46
|
+
rope_config = config.settings().get("rope", {})
|
|
47
|
+
autoimport = workspace._rope_autoimport(rope_config, memory)
|
|
48
|
+
resources: Optional[List[Resource]] = (
|
|
49
|
+
None
|
|
50
|
+
if files is None
|
|
51
|
+
else [document._rope_resource(rope_config) for document in files]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if single_thread:
|
|
55
|
+
self._reload_cache(workspace, autoimport, resources)
|
|
56
|
+
else:
|
|
57
|
+
# Creating the cache may take 10-20s for a environment with 5k python modules. That's
|
|
58
|
+
# why we decided to move cache creation into its own thread.
|
|
59
|
+
self.thread = threading.Thread(
|
|
60
|
+
target=self._reload_cache, args=(workspace, autoimport, resources)
|
|
61
|
+
)
|
|
62
|
+
self.thread.start()
|
|
63
|
+
|
|
64
|
+
def _reload_cache(
|
|
65
|
+
self,
|
|
66
|
+
workspace: Workspace,
|
|
67
|
+
autoimport: AutoImport,
|
|
68
|
+
resources: Optional[List[Resource]] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
task_handle = PylspTaskHandle(workspace)
|
|
71
|
+
autoimport.generate_cache(task_handle=task_handle, resources=resources)
|
|
72
|
+
autoimport.generate_modules_cache(task_handle=task_handle)
|
|
73
|
+
|
|
74
|
+
def is_blocked(self):
|
|
75
|
+
return self.thread and self.thread.is_alive()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@hookimpl
|
|
79
|
+
def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]:
|
|
80
|
+
# Default rope_completion to disabled
|
|
81
|
+
return {
|
|
82
|
+
"plugins": {
|
|
83
|
+
"rope_autoimport": {
|
|
84
|
+
"enabled": False,
|
|
85
|
+
"memory": False,
|
|
86
|
+
"completions": {
|
|
87
|
+
"enabled": True,
|
|
88
|
+
},
|
|
89
|
+
"code_actions": {
|
|
90
|
+
"enabled": True,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if we should insert the word_node on the given expr.
|
|
100
|
+
|
|
101
|
+
Works for both correct and incorrect code. This is because the
|
|
102
|
+
user is often working on the code as they write it.
|
|
103
|
+
"""
|
|
104
|
+
if not word_node:
|
|
105
|
+
return False
|
|
106
|
+
if len(expr.children) == 0:
|
|
107
|
+
return True
|
|
108
|
+
first_child = expr.children[0]
|
|
109
|
+
if isinstance(first_child, tree.EndMarker):
|
|
110
|
+
if "#" in first_child.prefix:
|
|
111
|
+
return False # Check for single line comment
|
|
112
|
+
if first_child == word_node:
|
|
113
|
+
return True # If the word is the first word then its fine
|
|
114
|
+
if len(expr.children) > 1:
|
|
115
|
+
if any(
|
|
116
|
+
node.type == "operator" and "." in node.value or node.type == "trailer"
|
|
117
|
+
for node in expr.children
|
|
118
|
+
):
|
|
119
|
+
return False # Check if we're on a method of a function
|
|
120
|
+
if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)):
|
|
121
|
+
# The tree will often include error nodes like this to indicate errors
|
|
122
|
+
# we want to ignore errors since the code is being written
|
|
123
|
+
return _should_insert(first_child, word_node)
|
|
124
|
+
return _handle_first_child(first_child, expr, word_node)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _handle_first_child(
|
|
128
|
+
first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Check if we suggest imports given the following first child."""
|
|
131
|
+
if isinstance(first_child, tree.Import):
|
|
132
|
+
return False
|
|
133
|
+
if isinstance(first_child, (tree.PythonLeaf, tree.PythonErrorLeaf)):
|
|
134
|
+
# Check if the first item is a from or import statement even when incomplete
|
|
135
|
+
if first_child.value in ("import", "from"):
|
|
136
|
+
return False
|
|
137
|
+
if isinstance(first_child, tree.Keyword):
|
|
138
|
+
if first_child.value == "def":
|
|
139
|
+
return _should_import_function(word_node, expr)
|
|
140
|
+
if first_child.value == "class":
|
|
141
|
+
return _should_import_class(word_node, expr)
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _should_import_class(word_node: tree.Leaf, expr: tree.BaseNode) -> bool:
|
|
146
|
+
prev_node = None
|
|
147
|
+
for node in expr.children:
|
|
148
|
+
if isinstance(node, tree.Name):
|
|
149
|
+
if isinstance(prev_node, tree.Operator):
|
|
150
|
+
if node == word_node and prev_node.value == "(":
|
|
151
|
+
return True
|
|
152
|
+
prev_node = node
|
|
153
|
+
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _should_import_function(word_node: tree.Leaf, expr: tree.BaseNode) -> bool:
|
|
158
|
+
prev_node = None
|
|
159
|
+
for node in expr.children:
|
|
160
|
+
if _handle_argument(node, word_node):
|
|
161
|
+
return True
|
|
162
|
+
if isinstance(prev_node, tree.Operator):
|
|
163
|
+
if prev_node.value == "->":
|
|
164
|
+
if node == word_node:
|
|
165
|
+
return True
|
|
166
|
+
prev_node = node
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf):
|
|
171
|
+
if isinstance(node, tree.PythonNode):
|
|
172
|
+
if node.type == "tfpdef":
|
|
173
|
+
if node.children[2] == word_node:
|
|
174
|
+
return True
|
|
175
|
+
if node.type == "parameters":
|
|
176
|
+
for parameter in node.children:
|
|
177
|
+
if _handle_argument(parameter, word_node):
|
|
178
|
+
return True
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _process_statements(
|
|
183
|
+
suggestions: List[SearchResult],
|
|
184
|
+
doc_uri: str,
|
|
185
|
+
word: str,
|
|
186
|
+
autoimport: AutoImport,
|
|
187
|
+
document: Document,
|
|
188
|
+
feature: str = "completions",
|
|
189
|
+
) -> Generator[Dict[str, Any], None, None]:
|
|
190
|
+
for suggestion in suggestions:
|
|
191
|
+
insert_line = autoimport.find_insertion_line(document.source) - 1
|
|
192
|
+
start = {"line": insert_line, "character": 0}
|
|
193
|
+
edit_range = {"start": start, "end": start}
|
|
194
|
+
edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"}
|
|
195
|
+
score = _get_score(
|
|
196
|
+
suggestion.source, suggestion.import_statement, suggestion.name, word
|
|
197
|
+
)
|
|
198
|
+
if score > _score_max:
|
|
199
|
+
continue
|
|
200
|
+
# TODO make this markdown
|
|
201
|
+
if feature == "completions":
|
|
202
|
+
yield {
|
|
203
|
+
"label": suggestion.name,
|
|
204
|
+
"kind": suggestion.itemkind,
|
|
205
|
+
"sortText": _sort_import(score),
|
|
206
|
+
"data": {"doc_uri": doc_uri},
|
|
207
|
+
"detail": _document(suggestion.import_statement),
|
|
208
|
+
"additionalTextEdits": [edit],
|
|
209
|
+
}
|
|
210
|
+
elif feature == "code_actions":
|
|
211
|
+
yield {
|
|
212
|
+
"title": suggestion.import_statement,
|
|
213
|
+
"kind": "quickfix",
|
|
214
|
+
"edit": {"changes": {doc_uri: [edit]}},
|
|
215
|
+
# data is a supported field for codeAction responses
|
|
216
|
+
# See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction
|
|
217
|
+
"data": {"sortText": _sort_import(score)},
|
|
218
|
+
}
|
|
219
|
+
else:
|
|
220
|
+
raise ValueError(f"Unknown feature: {feature}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_names(script: Script) -> Set[str]:
|
|
224
|
+
"""Get all names to ignore from the current file."""
|
|
225
|
+
raw_names = script.get_names(definitions=True)
|
|
226
|
+
log.debug(raw_names)
|
|
227
|
+
return {name.name for name in raw_names}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@hookimpl
|
|
231
|
+
def pylsp_completions(
|
|
232
|
+
config: Config,
|
|
233
|
+
workspace: Workspace,
|
|
234
|
+
document: Document,
|
|
235
|
+
position,
|
|
236
|
+
ignored_names: Union[Set[str], None],
|
|
237
|
+
):
|
|
238
|
+
"""Get autoimport suggestions."""
|
|
239
|
+
if (
|
|
240
|
+
not config.plugin_settings("rope_autoimport")
|
|
241
|
+
.get("completions", {})
|
|
242
|
+
.get("enabled", True)
|
|
243
|
+
) or cache.is_blocked():
|
|
244
|
+
return []
|
|
245
|
+
|
|
246
|
+
line = document.lines[position["line"]]
|
|
247
|
+
expr = parso.parse(line)
|
|
248
|
+
word_node = expr.get_leaf_for_position((1, position["character"]))
|
|
249
|
+
if not _should_insert(expr, word_node):
|
|
250
|
+
return []
|
|
251
|
+
word = word_node.value
|
|
252
|
+
log.debug(f"autoimport: searching for word: {word}")
|
|
253
|
+
rope_config = config.settings(document_path=document.path).get("rope", {})
|
|
254
|
+
ignored_names: Set[str] = ignored_names or get_names(
|
|
255
|
+
document.jedi_script(use_document_path=True)
|
|
256
|
+
)
|
|
257
|
+
autoimport = workspace._rope_autoimport(rope_config)
|
|
258
|
+
suggestions = list(autoimport.search_full(word, ignored_names=ignored_names))
|
|
259
|
+
results = sorted(
|
|
260
|
+
_process_statements(
|
|
261
|
+
suggestions, document.uri, word, autoimport, document, "completions"
|
|
262
|
+
),
|
|
263
|
+
key=lambda statement: statement["sortText"],
|
|
264
|
+
)
|
|
265
|
+
if len(results) > MAX_RESULTS_COMPLETIONS:
|
|
266
|
+
results = results[:MAX_RESULTS_COMPLETIONS]
|
|
267
|
+
return results
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _document(import_statement: str) -> str:
|
|
271
|
+
return """# Auto-Import\n""" + import_statement
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _get_score(
|
|
275
|
+
source: int, full_statement: str, suggested_name: str, desired_name
|
|
276
|
+
) -> int:
|
|
277
|
+
import_length = len("import")
|
|
278
|
+
full_statement_score = len(full_statement) - import_length
|
|
279
|
+
suggested_name_score = (len(suggested_name) - len(desired_name)) ** 2
|
|
280
|
+
source_score = 20 * source
|
|
281
|
+
return suggested_name_score + full_statement_score + source_score
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _sort_import(score: int) -> str:
|
|
285
|
+
score = max(min(score, (_score_max) - 1), 0)
|
|
286
|
+
# Since we are using ints, we need to pad them.
|
|
287
|
+
# We also want to prioritize autoimport behind everything since its the last priority.
|
|
288
|
+
# The minimum is to prevent score from overflowing the pad
|
|
289
|
+
return "[z" + str(score).rjust(_score_pow, "0")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_name_or_module(document, diagnostic) -> str:
|
|
293
|
+
start = diagnostic["range"]["start"]
|
|
294
|
+
return (
|
|
295
|
+
parso.parse(document.lines[start["line"]])
|
|
296
|
+
.get_leaf_for_position((1, start["character"] + 1))
|
|
297
|
+
.value
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@hookimpl
|
|
302
|
+
def pylsp_code_actions(
|
|
303
|
+
config: Config,
|
|
304
|
+
workspace: Workspace,
|
|
305
|
+
document: Document,
|
|
306
|
+
range: Dict,
|
|
307
|
+
context: Dict,
|
|
308
|
+
) -> List[Dict]:
|
|
309
|
+
"""
|
|
310
|
+
Provide code actions through rope.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
config : pylsp.config.config.Config
|
|
315
|
+
Current config.
|
|
316
|
+
workspace : pylsp.workspace.Workspace
|
|
317
|
+
Current workspace.
|
|
318
|
+
document : pylsp.workspace.Document
|
|
319
|
+
Document to apply code actions on.
|
|
320
|
+
range : Dict
|
|
321
|
+
Range argument given by pylsp. Not used here.
|
|
322
|
+
context : Dict
|
|
323
|
+
CodeActionContext given as dict.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
List of dicts containing the code actions.
|
|
328
|
+
"""
|
|
329
|
+
if (
|
|
330
|
+
not config.plugin_settings("rope_autoimport")
|
|
331
|
+
.get("code_actions", {})
|
|
332
|
+
.get("enabled", True)
|
|
333
|
+
) or cache.is_blocked():
|
|
334
|
+
return []
|
|
335
|
+
|
|
336
|
+
log.debug(f"textDocument/codeAction: {document} {range} {context}")
|
|
337
|
+
code_actions = []
|
|
338
|
+
for diagnostic in context.get("diagnostics", []):
|
|
339
|
+
if "undefined name" not in diagnostic.get("message", "").lower():
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
word = get_name_or_module(document, diagnostic)
|
|
343
|
+
log.debug(f"autoimport: searching for word: {word}")
|
|
344
|
+
rope_config = config.settings(document_path=document.path).get("rope", {})
|
|
345
|
+
autoimport = workspace._rope_autoimport(rope_config)
|
|
346
|
+
suggestions = list(autoimport.search_full(word))
|
|
347
|
+
log.debug("autoimport: suggestions: %s", suggestions)
|
|
348
|
+
results = sorted(
|
|
349
|
+
_process_statements(
|
|
350
|
+
suggestions,
|
|
351
|
+
document.uri,
|
|
352
|
+
word,
|
|
353
|
+
autoimport,
|
|
354
|
+
document,
|
|
355
|
+
"code_actions",
|
|
356
|
+
),
|
|
357
|
+
key=lambda statement: statement["data"]["sortText"],
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if len(results) > MAX_RESULTS_CODE_ACTIONS:
|
|
361
|
+
results = results[:MAX_RESULTS_CODE_ACTIONS]
|
|
362
|
+
code_actions.extend(results)
|
|
363
|
+
|
|
364
|
+
return code_actions
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@hookimpl
|
|
368
|
+
def pylsp_initialize(config: Config, workspace: Workspace) -> None:
|
|
369
|
+
"""Initialize AutoImport.
|
|
370
|
+
|
|
371
|
+
Generates the cache for local and global items.
|
|
372
|
+
"""
|
|
373
|
+
cache.reload_cache(config, workspace)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@hookimpl
|
|
377
|
+
def pylsp_document_did_open(config: Config, workspace: Workspace) -> None:
|
|
378
|
+
"""Initialize AutoImport.
|
|
379
|
+
|
|
380
|
+
Generates the cache for local and global items.
|
|
381
|
+
"""
|
|
382
|
+
cache.reload_cache(config, workspace)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@hookimpl
|
|
386
|
+
def pylsp_document_did_save(
|
|
387
|
+
config: Config, workspace: Workspace, document: Document
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Update the names associated with this document."""
|
|
390
|
+
cache.reload_cache(config, workspace, [document])
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@hookimpl
|
|
394
|
+
def pylsp_workspace_configuration_changed(config: Config, workspace: Workspace) -> None:
|
|
395
|
+
"""
|
|
396
|
+
Initialize autoimport if it has been enabled through a
|
|
397
|
+
workspace/didChangeConfiguration message from the frontend.
|
|
398
|
+
|
|
399
|
+
Generates the cache for local and global items.
|
|
400
|
+
"""
|
|
401
|
+
if config.plugin_settings("rope_autoimport").get("enabled", False):
|
|
402
|
+
cache.reload_cache(config, workspace)
|
|
403
|
+
else:
|
|
404
|
+
log.debug("autoimport: Skipping cache reload.")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
cache: AutoimportCache = AutoimportCache()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from rope.contrib.codeassist import code_assist, sorted_proposals
|
|
7
|
+
|
|
8
|
+
from pylsp import _utils, hookimpl, lsp
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@hookimpl
|
|
14
|
+
def pylsp_settings():
|
|
15
|
+
# Default rope_completion to disabled
|
|
16
|
+
return {"plugins": {"rope_completion": {"enabled": False, "eager": False}}}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_completion(completion, data, markup_kind):
|
|
20
|
+
try:
|
|
21
|
+
doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
log.debug("Failed to resolve Rope completion: %s", e)
|
|
24
|
+
doc = ""
|
|
25
|
+
completion["detail"] = "{0} {1}".format(data.scope or "", data.name)
|
|
26
|
+
completion["documentation"] = doc
|
|
27
|
+
return completion
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@hookimpl
|
|
31
|
+
def pylsp_completions(config, workspace, document, position):
|
|
32
|
+
settings = config.plugin_settings("rope_completion", document_path=document.path)
|
|
33
|
+
resolve_eagerly = settings.get("eager", False)
|
|
34
|
+
|
|
35
|
+
# Rope is a bit rubbish at completing module imports, so we'll return None
|
|
36
|
+
word = document.word_at_position(
|
|
37
|
+
{
|
|
38
|
+
# The -1 should really be trying to look at the previous word, but that might be quite expensive
|
|
39
|
+
# So we only skip import completions when the cursor is one space after `import`
|
|
40
|
+
"line": position["line"],
|
|
41
|
+
"character": max(position["character"] - 1, 0),
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
if word == "import":
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
offset = document.offset_at_position(position)
|
|
48
|
+
rope_config = config.settings(document_path=document.path).get("rope", {})
|
|
49
|
+
rope_project = workspace._rope_project_builder(rope_config)
|
|
50
|
+
document_rope = document._rope_resource(rope_config)
|
|
51
|
+
|
|
52
|
+
completion_capabilities = config.capabilities.get("textDocument", {}).get(
|
|
53
|
+
"completion", {}
|
|
54
|
+
)
|
|
55
|
+
item_capabilities = completion_capabilities.get("completionItem", {})
|
|
56
|
+
supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
|
|
57
|
+
preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
definitions = code_assist(
|
|
61
|
+
rope_project, document.source, offset, document_rope, maxfixes=3
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
log.debug("Failed to run Rope code assist: %s", e)
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
definitions = sorted_proposals(definitions)
|
|
68
|
+
new_definitions = []
|
|
69
|
+
for d in definitions:
|
|
70
|
+
item = {
|
|
71
|
+
"label": d.name,
|
|
72
|
+
"kind": _kind(d),
|
|
73
|
+
"sortText": _sort_text(d),
|
|
74
|
+
"data": {"doc_uri": document.uri},
|
|
75
|
+
}
|
|
76
|
+
if resolve_eagerly:
|
|
77
|
+
item = _resolve_completion(item, d, preferred_markup_kind)
|
|
78
|
+
new_definitions.append(item)
|
|
79
|
+
|
|
80
|
+
# most recently retrieved completion items, used for resolution
|
|
81
|
+
document.shared_data["LAST_ROPE_COMPLETIONS"] = {
|
|
82
|
+
# label is the only required property; here it is assumed to be unique
|
|
83
|
+
completion["label"]: (completion, data)
|
|
84
|
+
for completion, data in zip(new_definitions, definitions)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
definitions = new_definitions
|
|
88
|
+
|
|
89
|
+
return definitions or None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@hookimpl
|
|
93
|
+
def pylsp_completion_item_resolve(config, completion_item, document):
|
|
94
|
+
"""Resolve formatted completion for given non-resolved completion"""
|
|
95
|
+
shared_data = document.shared_data["LAST_ROPE_COMPLETIONS"].get(
|
|
96
|
+
completion_item["label"]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
completion_capabilities = config.capabilities.get("textDocument", {}).get(
|
|
100
|
+
"completion", {}
|
|
101
|
+
)
|
|
102
|
+
item_capabilities = completion_capabilities.get("completionItem", {})
|
|
103
|
+
supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
|
|
104
|
+
preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
|
|
105
|
+
|
|
106
|
+
if shared_data:
|
|
107
|
+
completion, data = shared_data
|
|
108
|
+
return _resolve_completion(completion, data, preferred_markup_kind)
|
|
109
|
+
return completion_item
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _sort_text(definition):
|
|
113
|
+
"""Ensure builtins appear at the bottom.
|
|
114
|
+
Description is of format <type>: <module>.<item>
|
|
115
|
+
"""
|
|
116
|
+
if definition.name.startswith("_"):
|
|
117
|
+
# It's a 'hidden' func, put it next last
|
|
118
|
+
return "z" + definition.name
|
|
119
|
+
if definition.scope == "builtin":
|
|
120
|
+
return "y" + definition.name
|
|
121
|
+
|
|
122
|
+
# Else put it at the front
|
|
123
|
+
return "a" + definition.name
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _kind(d):
|
|
127
|
+
"""Return the LSP type"""
|
|
128
|
+
MAP = {
|
|
129
|
+
"none": lsp.CompletionItemKind.Value,
|
|
130
|
+
"type": lsp.CompletionItemKind.Class,
|
|
131
|
+
"tuple": lsp.CompletionItemKind.Class,
|
|
132
|
+
"dict": lsp.CompletionItemKind.Class,
|
|
133
|
+
"dictionary": lsp.CompletionItemKind.Class,
|
|
134
|
+
"function": lsp.CompletionItemKind.Function,
|
|
135
|
+
"lambda": lsp.CompletionItemKind.Function,
|
|
136
|
+
"generator": lsp.CompletionItemKind.Function,
|
|
137
|
+
"class": lsp.CompletionItemKind.Class,
|
|
138
|
+
"instance": lsp.CompletionItemKind.Reference,
|
|
139
|
+
"method": lsp.CompletionItemKind.Method,
|
|
140
|
+
"builtin": lsp.CompletionItemKind.Class,
|
|
141
|
+
"builtinfunction": lsp.CompletionItemKind.Function,
|
|
142
|
+
"module": lsp.CompletionItemKind.Module,
|
|
143
|
+
"file": lsp.CompletionItemKind.File,
|
|
144
|
+
"xrange": lsp.CompletionItemKind.Class,
|
|
145
|
+
"slice": lsp.CompletionItemKind.Class,
|
|
146
|
+
"traceback": lsp.CompletionItemKind.Class,
|
|
147
|
+
"frame": lsp.CompletionItemKind.Class,
|
|
148
|
+
"buffer": lsp.CompletionItemKind.Class,
|
|
149
|
+
"dictproxy": lsp.CompletionItemKind.Class,
|
|
150
|
+
"funcdef": lsp.CompletionItemKind.Function,
|
|
151
|
+
"property": lsp.CompletionItemKind.Property,
|
|
152
|
+
"import": lsp.CompletionItemKind.Module,
|
|
153
|
+
"keyword": lsp.CompletionItemKind.Keyword,
|
|
154
|
+
"constant": lsp.CompletionItemKind.Variable,
|
|
155
|
+
"variable": lsp.CompletionItemKind.Variable,
|
|
156
|
+
"value": lsp.CompletionItemKind.Value,
|
|
157
|
+
"param": lsp.CompletionItemKind.Variable,
|
|
158
|
+
"statement": lsp.CompletionItemKind.Keyword,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return MAP.get(d.type)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Copyright 2017-2020 Palantir Technologies, Inc.
|
|
2
|
+
# Copyright 2021- Python Language Server Contributors.
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pylsp import _utils, hookimpl
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
SPHINX = re.compile(r"\s*:param\s+(?P<param>\w+):\s*(?P<doc>[^\n]+)")
|
|
12
|
+
EPYDOC = re.compile(r"\s*@param\s+(?P<param>\w+):\s*(?P<doc>[^\n]+)")
|
|
13
|
+
GOOGLE = re.compile(r"\s*(?P<param>\w+).*:\s*(?P<doc>[^\n]+)")
|
|
14
|
+
|
|
15
|
+
DOC_REGEX = [SPHINX, EPYDOC, GOOGLE]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@hookimpl
|
|
19
|
+
def pylsp_signature_help(config, document, position):
|
|
20
|
+
code_position = _utils.position_to_jedi_linecolumn(document, position)
|
|
21
|
+
signatures = document.jedi_script().get_signatures(**code_position)
|
|
22
|
+
|
|
23
|
+
if not signatures:
|
|
24
|
+
return {"signatures": []}
|
|
25
|
+
|
|
26
|
+
signature_capabilities = config.capabilities.get("textDocument", {}).get(
|
|
27
|
+
"signatureHelp", {}
|
|
28
|
+
)
|
|
29
|
+
signature_information_support = signature_capabilities.get(
|
|
30
|
+
"signatureInformation", {}
|
|
31
|
+
)
|
|
32
|
+
supported_markup_kinds = signature_information_support.get(
|
|
33
|
+
"documentationFormat", ["markdown"]
|
|
34
|
+
)
|
|
35
|
+
preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
|
|
36
|
+
|
|
37
|
+
s = signatures[0]
|
|
38
|
+
|
|
39
|
+
docstring = s.docstring()
|
|
40
|
+
|
|
41
|
+
# Docstring contains one or more lines of signature, followed by empty line, followed by docstring
|
|
42
|
+
function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines()
|
|
43
|
+
function_sig = " ".join([line.strip() for line in function_sig_lines])
|
|
44
|
+
sig = {
|
|
45
|
+
"label": function_sig,
|
|
46
|
+
"documentation": _utils.format_docstring(
|
|
47
|
+
s.docstring(raw=True), markup_kind=preferred_markup_kind
|
|
48
|
+
)["value"],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# If there are params, add those
|
|
52
|
+
if s.params:
|
|
53
|
+
sig["parameters"] = [
|
|
54
|
+
{
|
|
55
|
+
"label": p.name,
|
|
56
|
+
"documentation": _utils.format_docstring(
|
|
57
|
+
_param_docs(docstring, p.name), markup_kind=preferred_markup_kind
|
|
58
|
+
)["value"],
|
|
59
|
+
}
|
|
60
|
+
for p in s.params
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# We only return a single signature because Python doesn't allow overloading
|
|
64
|
+
sig_info = {"signatures": [sig], "activeSignature": 0}
|
|
65
|
+
|
|
66
|
+
if s.index is not None and s.params:
|
|
67
|
+
# Then we know which parameter we're looking at
|
|
68
|
+
sig_info["activeParameter"] = s.index
|
|
69
|
+
|
|
70
|
+
return sig_info
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _param_docs(docstring, param_name):
|
|
74
|
+
for line in docstring.splitlines():
|
|
75
|
+
for regex in DOC_REGEX:
|
|
76
|
+
m = regex.match(line)
|
|
77
|
+
if not m:
|
|
78
|
+
continue
|
|
79
|
+
if m.group("param") != param_name:
|
|
80
|
+
continue
|
|
81
|
+
return m.group("doc") or ""
|