Skip to content

Commit

Permalink
Merge branch 'develop' into importmagic
Browse files Browse the repository at this point in the history
  • Loading branch information
gatesn authored Nov 4, 2019
2 parents 2668de3 + ac79cef commit ecabe85
Show file tree
Hide file tree
Showing 12 changed files with 454 additions and 46 deletions.
6 changes: 6 additions & 0 deletions pyls/_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2017 Palantir Technologies, Inc.
from distutils.version import LooseVersion
import functools
import inspect
import logging
Expand All @@ -7,7 +8,10 @@
import threading
import re

import jedi

PY2 = sys.version_info.major == 2
JEDI_VERSION = jedi.__version__

if PY2:
import pathlib2 as pathlib
Expand Down Expand Up @@ -137,6 +141,8 @@ def format_docstring(contents):
"""
contents = contents.replace('\t', u'\u00A0' * 4)
contents = contents.replace(' ', u'\u00A0' * 2)
if LooseVersion(JEDI_VERSION) < LooseVersion('0.15.0'):
contents = contents.replace('*', '\\*')
return contents


Expand Down
5 changes: 5 additions & 0 deletions pyls/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def pyls_experimental_capabilities(config, workspace):
pass


@hookspec(firstresult=True)
def pyls_folding_range(config, workspace, document):
pass


@hookspec(firstresult=True)
def pyls_format_document(config, workspace, document):
pass
Expand Down
12 changes: 10 additions & 2 deletions pyls/plugins/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ def pyls_definitions(config, document, position):
'end': {'line': d.line - 1, 'character': d.column + len(d.name)},
}
}
for d in definitions
if d.is_definition() and d.line is not None and d.column is not None and d.module_path is not None
for d in definitions if d.is_definition() and _not_internal_definition(d)
]


def _not_internal_definition(definition):
return (
definition.line is not None and
definition.column is not None and
definition.module_path is not None and
not definition.in_builtin_module()
)
169 changes: 169 additions & 0 deletions pyls/plugins/folding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# pylint: disable=len-as-condition
# Copyright 2019 Palantir Technologies, Inc.

import re

import parso
import parso.python.tree as tree_nodes

from pyls import hookimpl

SKIP_NODES = (tree_nodes.Module, tree_nodes.IfStmt, tree_nodes.TryStmt)
IDENTATION_REGEX = re.compile(r'(\s+).+')


@hookimpl
def pyls_folding_range(document):
program = document.source + '\n'
lines = program.splitlines()
tree = parso.parse(program)
ranges = __compute_folding_ranges(tree, lines)

results = []
for (start_line, end_line) in ranges:
start_line -= 1
end_line -= 1
# If start/end character is not defined, then it defaults to the
# corresponding line last character
results.append({
'startLine': start_line,
'endLine': end_line,
})
return results


def __merge_folding_ranges(left, right):
for start in list(left.keys()):
right_start = right.pop(start, None)
if right_start is not None:
left[start] = max(right_start, start)
left.update(right)
return left


def __empty_identation_stack(identation_stack, level_limits,
current_line, folding_ranges):
while identation_stack != []:
upper_level = identation_stack.pop(0)
level_start = level_limits.pop(upper_level)
folding_ranges.append((level_start, current_line))
return folding_ranges


def __match_identation_stack(identation_stack, level, level_limits,
folding_ranges, current_line):
upper_level = identation_stack.pop(0)
while upper_level >= level:
level_start = level_limits.pop(upper_level)
folding_ranges.append((level_start, current_line))
upper_level = identation_stack.pop(0)
identation_stack.insert(0, upper_level)
return identation_stack, folding_ranges


def __compute_folding_ranges_identation(text):
lines = text.splitlines()
folding_ranges = []
identation_stack = []
level_limits = {}
current_level = 0
current_line = 0
while lines[current_line] == '':
current_line += 1
for i, line in enumerate(lines):
if i < current_line:
continue
i += 1
identation_match = IDENTATION_REGEX.match(line)
if identation_match is not None:
whitespace = identation_match.group(1)
level = len(whitespace)
if level > current_level:
level_limits[current_level] = current_line
identation_stack.insert(0, current_level)
current_level = level
elif level < current_level:
identation_stack, folding_ranges = __match_identation_stack(
identation_stack, level, level_limits, folding_ranges,
current_line)
current_level = level
else:
folding_ranges = __empty_identation_stack(
identation_stack, level_limits, current_line, folding_ranges)
current_level = 0
if line.strip() != '':
current_line = i
folding_ranges = __empty_identation_stack(
identation_stack, level_limits, current_line, folding_ranges)
return dict(folding_ranges)


def __check_if_node_is_valid(node):
valid = True
if isinstance(node, tree_nodes.PythonNode):
kind = node.type
valid = kind not in {'decorated', 'parameters'}
if kind == 'suite':
if isinstance(node.parent, tree_nodes.Function):
valid = False
return valid


def __compute_start_end_lines(node, stack):
start_line, _ = node.start_pos
end_line, _ = node.end_pos

last_leaf = node.get_last_leaf()
last_newline = isinstance(last_leaf, tree_nodes.Newline)
last_operator = isinstance(last_leaf, tree_nodes.Operator)
node_is_operator = isinstance(node, tree_nodes.Operator)
last_operator = last_operator or not node_is_operator

end_line -= 1

modified = False
if isinstance(node.parent, tree_nodes.PythonNode):
kind = node.type
if kind in {'suite', 'atom', 'atom_expr', 'arglist'}:
if len(stack) > 0:
next_node = stack[0]
next_line, _ = next_node.start_pos
if next_line > end_line:
end_line += 1
modified = True
if not last_newline and not modified and not last_operator:
end_line += 1
return start_line, end_line


def __compute_folding_ranges(tree, lines):
folding_ranges = {}
stack = [tree]

while len(stack) > 0:
node = stack.pop(0)
if isinstance(node, tree_nodes.Newline):
# Skip newline nodes
continue
elif isinstance(node, tree_nodes.PythonErrorNode):
# Fallback to identation-based (best-effort) folding
start_line, _ = node.start_pos
start_line -= 1
padding = [''] * start_line
text = '\n'.join(padding + lines[start_line:]) + '\n'
identation_ranges = __compute_folding_ranges_identation(text)
folding_ranges = __merge_folding_ranges(
folding_ranges, identation_ranges)
break
elif not isinstance(node, SKIP_NODES):
valid = __check_if_node_is_valid(node)
if valid:
start_line, end_line = __compute_start_end_lines(node, stack)
if end_line > start_line:
current_end = folding_ranges.get(start_line, -1)
folding_ranges[start_line] = max(current_end, end_line)
if hasattr(node, 'children'):
stack = node.children + stack

folding_ranges = sorted(folding_ranges.items())
return folding_ranges
52 changes: 31 additions & 21 deletions pyls/plugins/hover.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright 2017 Palantir Technologies, Inc.
from distutils.version import LooseVersion
import logging

from pyls import hookimpl, _utils

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

# Find first exact matching definition
definition = next((x for x in definitions if x.name == word), None)

if not definition:
return {'contents': ''}
if LooseVersion(_utils.JEDI_VERSION) >= LooseVersion('0.15.0'):
# Find first exact matching definition
definition = next((x for x in definitions if x.name == word), None)

if not definition:
return {'contents': ''}

# raw docstring returns only doc, without signature
doc = _utils.format_docstring(definition.docstring(raw=True))

# Find first exact matching signature
signature = next((x.to_string() for x in definition.get_signatures() if x.name == word), '')

contents = []
if signature:
contents.append({
'language': 'python',
'value': signature,
})
if doc:
contents.append(doc)
if not contents:
return {'contents': ''}
return {'contents': contents}
else:
# Find an exact match for a completion
for d in definitions:
if d.name == word:
return {'contents': _utils.format_docstring(d.docstring()) or ''}

# raw docstring returns only doc, without signature
doc = _utils.format_docstring(definition.docstring(raw=True))

# Find first exact matching signature
signature = next((x.to_string() for x in definition.get_signatures() if x.name == word), '')

contents = []
if signature:
contents.append({
'language': 'python',
'value': signature,
})
if doc:
contents.append(doc)
if not contents:
return {'contents': ''}
return {'contents': contents}
23 changes: 20 additions & 3 deletions pyls/python_ls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Copyright 2017 Palantir Technologies, Inc.
from functools import partial
import logging
import os
import socketserver
import threading
from functools import partial

from pyls_jsonrpc.dispatchers import MethodDispatcher
from pyls_jsonrpc.endpoint import Endpoint
Expand Down Expand Up @@ -33,7 +34,16 @@ def setup(self):
self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile)

def handle(self):
self.delegate.start()
try:
self.delegate.start()
except OSError as e:
if os.name == 'nt':
# Catch and pass on ConnectionResetError when parent process
# dies
# pylint: disable=no-member, undefined-variable
if isinstance(e, WindowsError) and e.winerror == 10054:
pass

# pylint: disable=no-member
self.SHUTDOWN_CALL()

Expand Down Expand Up @@ -163,6 +173,7 @@ def capabilities(self):
'hoverProvider': True,
'referencesProvider': True,
'renameProvider': True,
'foldingRangeProvider': True,
'signatureHelpProvider': {
'triggerCharacters': ['(', ',', '=']
},
Expand Down Expand Up @@ -202,7 +213,7 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati
def watch_parent_process(pid):
# exit when the given pid is not alive
if not _utils.is_process_alive(pid):
log.info("parent process %s is not alive", pid)
log.info("parent process %s is not alive, exiting!", pid)
self.m_exit()
else:
threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start()
Expand Down Expand Up @@ -272,6 +283,9 @@ def rename(self, doc_uri, position, new_name):
def signature_help(self, doc_uri, position):
return self._hook('pyls_signature_help', doc_uri, position=position)

def folding(self, doc_uri):
return self._hook('pyls_folding_range', doc_uri)

def m_text_document__did_close(self, textDocument=None, **_kwargs):
workspace = self._match_uri_to_workspace(textDocument['uri'])
workspace.rm_document(textDocument['uri'])
Expand Down Expand Up @@ -323,6 +337,9 @@ def m_text_document__formatting(self, textDocument=None, _options=None, **_kwarg
def m_text_document__rename(self, textDocument=None, position=None, newName=None, **_kwargs):
return self.rename(textDocument['uri'], position, newName)

def m_text_document__folding_range(self, textDocument=None, **_kwargs):
return self.folding(textDocument['uri'])

def m_text_document__range_formatting(self, textDocument=None, range=None, _options=None, **_kwargs):
# Again, we'll ignore formatting options for now.
return self.format_range(textDocument['uri'], range)
Expand Down
8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
'configparser; python_version<"3.0"',
'future>=0.14.0; python_version<"3"',
'backports.functools_lru_cache; python_version<"3.2"',
'jedi>=0.15.0,<0.16',
'jedi>=0.14.1,<0.16',
'python-jsonrpc-server>=0.1.0',
'pluggy'
],
Expand Down Expand Up @@ -67,7 +67,8 @@
'pylint': ['pylint'],
'rope': ['rope>0.10.5'],
'yapf': ['yapf'],
'test': ['versioneer', 'pylint', 'pytest', 'mock', 'pytest-cov', 'coverage'],
'test': ['versioneer', 'pylint', 'pytest', 'mock', 'pytest-cov',
'coverage', 'numpy', 'pandas', 'matplotlib'],
},

# To provide executable scripts, use entry points in preference to the
Expand All @@ -79,6 +80,7 @@
],
'pyls': [
'autopep8 = pyls.plugins.autopep8_format',
'folding = pyls.plugins.folding',
'flake8 = pyls.plugins.flake8_lint',
'importmagic = pyls.plugins.importmagic_lint',
'jedi_completion = pyls.plugins.jedi_completion',
Expand All @@ -96,7 +98,7 @@
'pylint = pyls.plugins.pylint_lint',
'rope_completion = pyls.plugins.rope_completion',
'rope_rename = pyls.plugins.rope_rename',
'yapf = pyls.plugins.yapf_format',
'yapf = pyls.plugins.yapf_format'
]
},
)
Loading

0 comments on commit ecabe85

Please sign in to comment.