Skip to content

Add progress reporting #236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions pylsp/plugins/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@


@hookimpl
def pylsp_definitions(config, document, position):
settings = config.plugin_settings('jedi_definition')
code_position = _utils.position_to_jedi_linecolumn(document, position)
definitions = document.jedi_script(use_document_path=True).goto(
follow_imports=settings.get('follow_imports', True),
follow_builtin_imports=settings.get('follow_builtin_imports', True),
**code_position)
def pylsp_definitions(config, workspace, document, position):
with workspace.report_progress("definitions"):
settings = config.plugin_settings('jedi_definition')
code_position = _utils.position_to_jedi_linecolumn(document, position)
definitions = document.jedi_script(use_document_path=True).goto(
follow_imports=settings.get('follow_imports', True),
follow_builtin_imports=settings.get('follow_builtin_imports', True),
**code_position)

return [
{
'uri': uris.uri_with(document.uri, path=str(d.module_path)),
'range': {
'start': {'line': d.line - 1, 'character': d.column},
'end': {'line': d.line - 1, 'character': d.column + len(d.name)},
return [
{
'uri': uris.uri_with(document.uri, path=str(d.module_path)),
'range': {
'start': {'line': d.line - 1, 'character': d.column},
'end': {'line': d.line - 1, 'character': d.column + len(d.name)},
}
}
}
for d in definitions if d.is_definition() and _not_internal_definition(d)
]
for d in definitions if d.is_definition() and _not_internal_definition(d)
]


def _not_internal_definition(definition):
Expand Down
101 changes: 51 additions & 50 deletions pylsp/plugins/flake8_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,56 +23,57 @@ def pylsp_settings():

@hookimpl
def pylsp_lint(workspace, document):
config = workspace._config
settings = config.plugin_settings('flake8', document_path=document.path)
log.debug("Got flake8 settings: %s", settings)

ignores = settings.get("ignore", [])
per_file_ignores = settings.get("perFileIgnores")

if per_file_ignores:
prev_file_pat = None
for path in per_file_ignores:
try:
file_pat, errors = path.split(":")
prev_file_pat = file_pat
except ValueError:
# It's legal to just specify another error type for the same
# file pattern:
if prev_file_pat is None:
log.warning(
"skipping a Per-file-ignore with no file pattern")
continue
file_pat = prev_file_pat
errors = path
if PurePath(document.path).match(file_pat):
ignores.extend(errors.split(","))

opts = {
'config': settings.get('config'),
'exclude': settings.get('exclude'),
'filename': settings.get('filename'),
'hang-closing': settings.get('hangClosing'),
'ignore': ignores or None,
'max-line-length': settings.get('maxLineLength'),
'indent-size': settings.get('indentSize'),
'select': settings.get('select'),
}

# flake takes only absolute path to the config. So we should check and
# convert if necessary
if opts.get('config') and not os.path.isabs(opts.get('config')):
opts['config'] = os.path.abspath(os.path.expanduser(os.path.expandvars(
opts.get('config')
)))
log.debug("using flake8 with config: %s", opts['config'])

# Call the flake8 utility then parse diagnostics from stdout
flake8_executable = settings.get('executable', 'flake8')

args = build_args(opts)
output = run_flake8(flake8_executable, args, document)
return parse_stdout(document, output)
with workspace.report_progress("flake8"):
config = workspace._config
settings = config.plugin_settings('flake8', document_path=document.path)
log.debug("Got flake8 settings: %s", settings)

ignores = settings.get("ignore", [])
per_file_ignores = settings.get("perFileIgnores")

if per_file_ignores:
prev_file_pat = None
for path in per_file_ignores:
try:
file_pat, errors = path.split(":")
prev_file_pat = file_pat
except ValueError:
# It's legal to just specify another error type for the same
# file pattern:
if prev_file_pat is None:
log.warning(
"skipping a Per-file-ignore with no file pattern")
continue
file_pat = prev_file_pat
errors = path
if PurePath(document.path).match(file_pat):
ignores.extend(errors.split(","))

opts = {
'config': settings.get('config'),
'exclude': settings.get('exclude'),
'filename': settings.get('filename'),
'hang-closing': settings.get('hangClosing'),
'ignore': ignores or None,
'max-line-length': settings.get('maxLineLength'),
'indent-size': settings.get('indentSize'),
'select': settings.get('select'),
}

# flake takes only absolute path to the config. So we should check and
# convert if necessary
if opts.get('config') and not os.path.isabs(opts.get('config')):
opts['config'] = os.path.abspath(os.path.expanduser(os.path.expandvars(
opts.get('config')
)))
log.debug("using flake8 with config: %s", opts['config'])

# Call the flake8 utility then parse diagnostics from stdout
flake8_executable = settings.get('executable', 'flake8')

args = build_args(opts)
output = run_flake8(flake8_executable, args, document)
return parse_stdout(document, output)


def run_flake8(flake8_executable, args, document):
Expand Down
67 changes: 36 additions & 31 deletions pylsp/plugins/jedi_rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,43 @@

@hookimpl
def pylsp_rename(config, workspace, document, position, new_name): # pylint: disable=unused-argument
log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name)
kwargs = _utils.position_to_jedi_linecolumn(document, position)
kwargs['new_name'] = new_name
try:
refactoring = document.jedi_script().rename(**kwargs)
except NotImplementedError as exc:
raise Exception('No support for renaming in Python 2/3.5 with Jedi. '
'Consider using the rope_rename plugin instead') from exc
log.debug('Finished rename: %s', refactoring.get_diff())
changes = []
for file_path, changed_file in refactoring.get_changed_files().items():
uri = uris.from_fs_path(str(file_path))
doc = workspace.get_maybe_document(uri)
changes.append({
'textDocument': {
'uri': uri,
'version': doc.version if doc else None
},
'edits': [
{
'range': {
'start': {'line': 0, 'character': 0},
'end': {
'line': _num_lines(changed_file.get_new_code()),
'character': 0,
with workspace.report_progress("rename", percentage=0) as report_progress:
log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name)
kwargs = _utils.position_to_jedi_linecolumn(document, position)
kwargs['new_name'] = new_name
report_progress("refactoring")
try:
refactoring = document.jedi_script().rename(**kwargs)
except NotImplementedError as exc:
raise Exception('No support for renaming in Python 2/3.5 with Jedi. '
'Consider using the rope_rename plugin instead') from exc
log.debug('Finished rename: %s', refactoring.get_diff())
changes = []

changed_files = refactoring.get_changed_files()
for n, (file_path, changed_file) in enumerate(changed_files.items()):
report_progress(changed_file, percentage=n/len(changed_files)*100)
uri = uris.from_fs_path(str(file_path))
doc = workspace.get_maybe_document(uri)
changes.append({
'textDocument': {
'uri': uri,
'version': doc.version if doc else None
},
'edits': [
{
'range': {
'start': {'line': 0, 'character': 0},
'end': {
'line': _num_lines(changed_file.get_new_code()),
'character': 0,
},
},
},
'newText': changed_file.get_new_code(),
}
],
})
return {'documentChanges': changes}
'newText': changed_file.get_new_code(),
}
],
})
return {'documentChanges': changes}


def _num_lines(file_contents):
Expand Down
29 changes: 15 additions & 14 deletions pylsp/plugins/references.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@


@hookimpl
def pylsp_references(document, position, exclude_declaration=False):
code_position = _utils.position_to_jedi_linecolumn(document, position)
usages = document.jedi_script().get_references(**code_position)
def pylsp_references(document, workspace, position, exclude_declaration=False):
with workspace.report_progress("references"):
code_position = _utils.position_to_jedi_linecolumn(document, position)
usages = document.jedi_script().get_references(**code_position)

if exclude_declaration:
# Filter out if the usage is the actual declaration of the thing
usages = [d for d in usages if not d.is_definition()]
if exclude_declaration:
# Filter out if the usage is the actual declaration of the thing
usages = [d for d in usages if not d.is_definition()]

# Filter out builtin modules
return [{
'uri': uris.uri_with(document.uri, path=str(d.module_path)) if d.module_path else document.uri,
'range': {
'start': {'line': d.line - 1, 'character': d.column},
'end': {'line': d.line - 1, 'character': d.column + len(d.name)}
}
} for d in usages if not d.in_builtin_module()]
# Filter out builtin modules
return [{
'uri': uris.uri_with(document.uri, path=str(d.module_path)) if d.module_path else document.uri,
'range': {
'start': {'line': d.line - 1, 'character': d.column},
'end': {'line': d.line - 1, 'character': d.column + len(d.name)}
}
} for d in usages if not d.in_builtin_module()]
1 change: 1 addition & 0 deletions pylsp/python_lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ def _match_uri_to_workspace(self, uri):
def _hook(self, hook_name, doc_uri=None, **kwargs):
"""Calls hook_name and returns a list of results from all registered handlers"""
workspace = self._match_uri_to_workspace(doc_uri)

doc = workspace.get_document(doc_uri) if doc_uri else None
hook_handlers = self.config.plugin_manager.subset_hook_caller(hook_name, self.config.disabled_plugins)
return hook_handlers(config=self.config, workspace=workspace, document=doc, **kwargs)
Expand Down
68 changes: 68 additions & 0 deletions pylsp/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

import io
import logging
from contextlib import contextmanager
import os
import re
import uuid
import functools
from typing import Optional, Generator
from threading import RLock

import jedi
Expand All @@ -31,6 +34,7 @@ def wrapper(self, *args, **kwargs):
class Workspace:

M_PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics'
M_PROGRESS = '$/progress'
M_APPLY_EDIT = 'workspace/applyEdit'
M_SHOW_MESSAGE = 'window/showMessage'

Expand Down Expand Up @@ -109,6 +113,70 @@ def apply_edit(self, edit):
def publish_diagnostics(self, doc_uri, diagnostics):
self._endpoint.notify(self.M_PUBLISH_DIAGNOSTICS, params={'uri': doc_uri, 'diagnostics': diagnostics})

@contextmanager
def report_progress(self, title: str, message: Optional[str]=None, percentage: Optional[int] = None):
token = self._progress_begin(title, message, percentage)

def progress_message(message: str, percentage: Optional[int]=None):
self._progress_report(token, message, percentage)

try:
yield progress_message
finally:
self._progress_end(token)

def _progress_begin(self, title: str, message: Optional[str]=None, percentage: Optional[int]=None) -> str:
token = str(uuid.uuid4())
value = {
'kind': 'begin',
'title': title,
}
if message:
value['message'] = message
if percentage:
value['percentage'] = percentage

self._endpoint.notify(
self.M_PROGRESS,
params={
'token': token,
'value': value,
}
)
return token

def _progress_report(self, token: str, message: Optional[str]=None, percentage: Optional[int]=None) -> None:
value = {
'kind': 'report',
}
if message:
value['message'] = message
if percentage:
value['percentage'] = percentage

self._endpoint.notify(
self.M_PROGRESS,
params={
'token': token,
'value': value,
}
)

def _progress_end(self, token: str, message: Optional[str]=None) -> None:
value = {
'kind': 'end',
}
if message:
value['message'] = message

self._endpoint.notify(
self.M_PROGRESS,
params={
'token': token,
'value': value,
}
)

def show_message(self, message, msg_type=lsp.MessageType.Info):
self._endpoint.notify(self.M_SHOW_MESSAGE, params={'type': msg_type, 'message': message})

Expand Down