Skip to content

Commit 2261042

Browse files
committed
Merge branch 'importmagic' of github.com:youben11/python-language-server into importmagic
2 parents b0f04a3 + ecabe85 commit 2261042

12 files changed

+454
-46
lines changed

pyls/_utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2017 Palantir Technologies, Inc.
2+
from distutils.version import LooseVersion
23
import functools
34
import inspect
45
import logging
@@ -7,7 +8,10 @@
78
import threading
89
import re
910

11+
import jedi
12+
1013
PY2 = sys.version_info.major == 2
14+
JEDI_VERSION = jedi.__version__
1115

1216
if PY2:
1317
import pathlib2 as pathlib
@@ -137,6 +141,8 @@ def format_docstring(contents):
137141
"""
138142
contents = contents.replace('\t', u'\u00A0' * 4)
139143
contents = contents.replace(' ', u'\u00A0' * 2)
144+
if LooseVersion(JEDI_VERSION) < LooseVersion('0.15.0'):
145+
contents = contents.replace('*', '\\*')
140146
return contents
141147

142148

pyls/hookspecs.py

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ def pyls_experimental_capabilities(config, workspace):
6767
pass
6868

6969

70+
@hookspec(firstresult=True)
71+
def pyls_folding_range(config, workspace, document):
72+
pass
73+
74+
7075
@hookspec(firstresult=True)
7176
def pyls_format_document(config, workspace, document):
7277
pass

pyls/plugins/definition.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ def pyls_definitions(config, document, position):
2020
'end': {'line': d.line - 1, 'character': d.column + len(d.name)},
2121
}
2222
}
23-
for d in definitions
24-
if d.is_definition() and d.line is not None and d.column is not None and d.module_path is not None
23+
for d in definitions if d.is_definition() and _not_internal_definition(d)
2524
]
25+
26+
27+
def _not_internal_definition(definition):
28+
return (
29+
definition.line is not None and
30+
definition.column is not None and
31+
definition.module_path is not None and
32+
not definition.in_builtin_module()
33+
)

pyls/plugins/folding.py

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# pylint: disable=len-as-condition
2+
# Copyright 2019 Palantir Technologies, Inc.
3+
4+
import re
5+
6+
import parso
7+
import parso.python.tree as tree_nodes
8+
9+
from pyls 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 pyls_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+
'startLine': start_line,
30+
'endLine': end_line,
31+
})
32+
return results
33+
34+
35+
def __merge_folding_ranges(left, right):
36+
for start in list(left.keys()):
37+
right_start = right.pop(start, None)
38+
if right_start is not None:
39+
left[start] = max(right_start, start)
40+
left.update(right)
41+
return left
42+
43+
44+
def __empty_identation_stack(identation_stack, level_limits,
45+
current_line, folding_ranges):
46+
while identation_stack != []:
47+
upper_level = identation_stack.pop(0)
48+
level_start = level_limits.pop(upper_level)
49+
folding_ranges.append((level_start, current_line))
50+
return folding_ranges
51+
52+
53+
def __match_identation_stack(identation_stack, level, level_limits,
54+
folding_ranges, current_line):
55+
upper_level = identation_stack.pop(0)
56+
while upper_level >= level:
57+
level_start = level_limits.pop(upper_level)
58+
folding_ranges.append((level_start, current_line))
59+
upper_level = identation_stack.pop(0)
60+
identation_stack.insert(0, upper_level)
61+
return identation_stack, folding_ranges
62+
63+
64+
def __compute_folding_ranges_identation(text):
65+
lines = text.splitlines()
66+
folding_ranges = []
67+
identation_stack = []
68+
level_limits = {}
69+
current_level = 0
70+
current_line = 0
71+
while lines[current_line] == '':
72+
current_line += 1
73+
for i, line in enumerate(lines):
74+
if i < current_line:
75+
continue
76+
i += 1
77+
identation_match = IDENTATION_REGEX.match(line)
78+
if identation_match is not None:
79+
whitespace = identation_match.group(1)
80+
level = len(whitespace)
81+
if level > current_level:
82+
level_limits[current_level] = current_line
83+
identation_stack.insert(0, current_level)
84+
current_level = level
85+
elif level < current_level:
86+
identation_stack, folding_ranges = __match_identation_stack(
87+
identation_stack, level, level_limits, folding_ranges,
88+
current_line)
89+
current_level = level
90+
else:
91+
folding_ranges = __empty_identation_stack(
92+
identation_stack, level_limits, current_line, folding_ranges)
93+
current_level = 0
94+
if line.strip() != '':
95+
current_line = i
96+
folding_ranges = __empty_identation_stack(
97+
identation_stack, level_limits, current_line, folding_ranges)
98+
return dict(folding_ranges)
99+
100+
101+
def __check_if_node_is_valid(node):
102+
valid = True
103+
if isinstance(node, tree_nodes.PythonNode):
104+
kind = node.type
105+
valid = kind not in {'decorated', 'parameters'}
106+
if kind == 'suite':
107+
if isinstance(node.parent, tree_nodes.Function):
108+
valid = False
109+
return valid
110+
111+
112+
def __compute_start_end_lines(node, stack):
113+
start_line, _ = node.start_pos
114+
end_line, _ = node.end_pos
115+
116+
last_leaf = node.get_last_leaf()
117+
last_newline = isinstance(last_leaf, tree_nodes.Newline)
118+
last_operator = isinstance(last_leaf, tree_nodes.Operator)
119+
node_is_operator = isinstance(node, tree_nodes.Operator)
120+
last_operator = last_operator or not node_is_operator
121+
122+
end_line -= 1
123+
124+
modified = False
125+
if isinstance(node.parent, tree_nodes.PythonNode):
126+
kind = node.type
127+
if kind in {'suite', 'atom', 'atom_expr', 'arglist'}:
128+
if len(stack) > 0:
129+
next_node = stack[0]
130+
next_line, _ = next_node.start_pos
131+
if next_line > end_line:
132+
end_line += 1
133+
modified = True
134+
if not last_newline and not modified and not last_operator:
135+
end_line += 1
136+
return start_line, end_line
137+
138+
139+
def __compute_folding_ranges(tree, lines):
140+
folding_ranges = {}
141+
stack = [tree]
142+
143+
while len(stack) > 0:
144+
node = stack.pop(0)
145+
if isinstance(node, tree_nodes.Newline):
146+
# Skip newline nodes
147+
continue
148+
elif isinstance(node, tree_nodes.PythonErrorNode):
149+
# Fallback to identation-based (best-effort) folding
150+
start_line, _ = node.start_pos
151+
start_line -= 1
152+
padding = [''] * start_line
153+
text = '\n'.join(padding + lines[start_line:]) + '\n'
154+
identation_ranges = __compute_folding_ranges_identation(text)
155+
folding_ranges = __merge_folding_ranges(
156+
folding_ranges, identation_ranges)
157+
break
158+
elif not isinstance(node, SKIP_NODES):
159+
valid = __check_if_node_is_valid(node)
160+
if valid:
161+
start_line, end_line = __compute_start_end_lines(node, stack)
162+
if end_line > start_line:
163+
current_end = folding_ranges.get(start_line, -1)
164+
folding_ranges[start_line] = max(current_end, end_line)
165+
if hasattr(node, 'children'):
166+
stack = node.children + stack
167+
168+
folding_ranges = sorted(folding_ranges.items())
169+
return folding_ranges

pyls/plugins/hover.py

+31-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Copyright 2017 Palantir Technologies, Inc.
2+
from distutils.version import LooseVersion
23
import logging
4+
35
from pyls import hookimpl, _utils
46

57
log = logging.getLogger(__name__)
@@ -10,26 +12,34 @@ def pyls_hover(document, position):
1012
definitions = document.jedi_script(position).goto_definitions()
1113
word = document.word_at_position(position)
1214

13-
# Find first exact matching definition
14-
definition = next((x for x in definitions if x.name == word), None)
15-
16-
if not definition:
17-
return {'contents': ''}
15+
if LooseVersion(_utils.JEDI_VERSION) >= LooseVersion('0.15.0'):
16+
# Find first exact matching definition
17+
definition = next((x for x in definitions if x.name == word), None)
18+
19+
if not definition:
20+
return {'contents': ''}
21+
22+
# raw docstring returns only doc, without signature
23+
doc = _utils.format_docstring(definition.docstring(raw=True))
24+
25+
# Find first exact matching signature
26+
signature = next((x.to_string() for x in definition.get_signatures() if x.name == word), '')
27+
28+
contents = []
29+
if signature:
30+
contents.append({
31+
'language': 'python',
32+
'value': signature,
33+
})
34+
if doc:
35+
contents.append(doc)
36+
if not contents:
37+
return {'contents': ''}
38+
return {'contents': contents}
39+
else:
40+
# Find an exact match for a completion
41+
for d in definitions:
42+
if d.name == word:
43+
return {'contents': _utils.format_docstring(d.docstring()) or ''}
1844

19-
# raw docstring returns only doc, without signature
20-
doc = _utils.format_docstring(definition.docstring(raw=True))
21-
22-
# Find first exact matching signature
23-
signature = next((x.to_string() for x in definition.get_signatures() if x.name == word), '')
24-
25-
contents = []
26-
if signature:
27-
contents.append({
28-
'language': 'python',
29-
'value': signature,
30-
})
31-
if doc:
32-
contents.append(doc)
33-
if not contents:
3445
return {'contents': ''}
35-
return {'contents': contents}

pyls/python_ls.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Copyright 2017 Palantir Technologies, Inc.
2+
from functools import partial
23
import logging
4+
import os
35
import socketserver
46
import threading
5-
from functools import partial
67

78
from pyls_jsonrpc.dispatchers import MethodDispatcher
89
from pyls_jsonrpc.endpoint import Endpoint
@@ -33,7 +34,16 @@ def setup(self):
3334
self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile)
3435

3536
def handle(self):
36-
self.delegate.start()
37+
try:
38+
self.delegate.start()
39+
except OSError as e:
40+
if os.name == 'nt':
41+
# Catch and pass on ConnectionResetError when parent process
42+
# dies
43+
# pylint: disable=no-member, undefined-variable
44+
if isinstance(e, WindowsError) and e.winerror == 10054:
45+
pass
46+
3747
# pylint: disable=no-member
3848
self.SHUTDOWN_CALL()
3949

@@ -163,6 +173,7 @@ def capabilities(self):
163173
'hoverProvider': True,
164174
'referencesProvider': True,
165175
'renameProvider': True,
176+
'foldingRangeProvider': True,
166177
'signatureHelpProvider': {
167178
'triggerCharacters': ['(', ',', '=']
168179
},
@@ -202,7 +213,7 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati
202213
def watch_parent_process(pid):
203214
# exit when the given pid is not alive
204215
if not _utils.is_process_alive(pid):
205-
log.info("parent process %s is not alive", pid)
216+
log.info("parent process %s is not alive, exiting!", pid)
206217
self.m_exit()
207218
else:
208219
threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start()
@@ -272,6 +283,9 @@ def rename(self, doc_uri, position, new_name):
272283
def signature_help(self, doc_uri, position):
273284
return self._hook('pyls_signature_help', doc_uri, position=position)
274285

286+
def folding(self, doc_uri):
287+
return self._hook('pyls_folding_range', doc_uri)
288+
275289
def m_text_document__did_close(self, textDocument=None, **_kwargs):
276290
workspace = self._match_uri_to_workspace(textDocument['uri'])
277291
workspace.rm_document(textDocument['uri'])
@@ -323,6 +337,9 @@ def m_text_document__formatting(self, textDocument=None, _options=None, **_kwarg
323337
def m_text_document__rename(self, textDocument=None, position=None, newName=None, **_kwargs):
324338
return self.rename(textDocument['uri'], position, newName)
325339

340+
def m_text_document__folding_range(self, textDocument=None, **_kwargs):
341+
return self.folding(textDocument['uri'])
342+
326343
def m_text_document__range_formatting(self, textDocument=None, range=None, _options=None, **_kwargs):
327344
# Again, we'll ignore formatting options for now.
328345
return self.format_range(textDocument['uri'], range)

setup.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
'configparser; python_version<"3.0"',
3636
'future>=0.14.0; python_version<"3"',
3737
'backports.functools_lru_cache; python_version<"3.2"',
38-
'jedi>=0.15.0,<0.16',
38+
'jedi>=0.14.1,<0.16',
3939
'python-jsonrpc-server>=0.1.0',
4040
'pluggy'
4141
],
@@ -67,7 +67,8 @@
6767
'pylint': ['pylint'],
6868
'rope': ['rope>0.10.5'],
6969
'yapf': ['yapf'],
70-
'test': ['versioneer', 'pylint', 'pytest', 'mock', 'pytest-cov', 'coverage'],
70+
'test': ['versioneer', 'pylint', 'pytest', 'mock', 'pytest-cov',
71+
'coverage', 'numpy', 'pandas', 'matplotlib'],
7172
},
7273

7374
# To provide executable scripts, use entry points in preference to the
@@ -79,6 +80,7 @@
7980
],
8081
'pyls': [
8182
'autopep8 = pyls.plugins.autopep8_format',
83+
'folding = pyls.plugins.folding',
8284
'flake8 = pyls.plugins.flake8_lint',
8385
'importmagic = pyls.plugins.importmagic_lint',
8486
'jedi_completion = pyls.plugins.jedi_completion',
@@ -96,7 +98,7 @@
9698
'pylint = pyls.plugins.pylint_lint',
9799
'rope_completion = pyls.plugins.rope_completion',
98100
'rope_rename = pyls.plugins.rope_rename',
99-
'yapf = pyls.plugins.yapf_format',
101+
'yapf = pyls.plugins.yapf_format'
100102
]
101103
},
102104
)

0 commit comments

Comments
 (0)