Skip to content

Initialize LSP progress token before using it and remove progress for sync plugins #328

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 15 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions pylsp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jedi

JEDI_VERSION = jedi.__version__
CALL_TIMEOUT = 10

# Eol chars accepted by the LSP protocol
# the ordering affects performance
Expand Down
8 changes: 5 additions & 3 deletions pylsp/python_lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class PythonLSPServer(MethodDispatcher):

# pylint: disable=too-many-public-methods,redefined-builtin

def __init__(self, rx, tx, check_parent_process=False, consumer=None):
def __init__(self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None):
self.workspace = None
self.config = None
self.root_uri = None
Expand All @@ -172,11 +172,13 @@ def __init__(self, rx, tx, check_parent_process=False, consumer=None):
else:
self._jsonrpc_stream_writer = None

endpoint_cls = endpoint_cls or Endpoint

# if consumer is None, it is assumed that the default streams-based approach is being used
if consumer is None:
self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS)
self._endpoint = endpoint_cls(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS)
else:
self._endpoint = Endpoint(self, consumer, max_workers=MAX_WORKERS)
self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS)

self._dispatchers = []
self._shutdown = False
Expand Down
4 changes: 4 additions & 0 deletions pylsp/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Workspace:

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

Expand Down Expand Up @@ -152,6 +153,9 @@ def _progress_begin(
percentage: Optional[int] = None,
) -> str:
token = str(uuid.uuid4())

self._endpoint.request(self.M_INITIALIZE_PROGRESS, {'token': token}).result(_utils.CALL_TIMEOUT)

value = {
"kind": "begin",
"title": title,
Expand Down
49 changes: 46 additions & 3 deletions test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from io import StringIO
from unittest.mock import MagicMock
import pytest
from pylsp_jsonrpc.dispatchers import MethodDispatcher
from pylsp_jsonrpc.endpoint import Endpoint
from pylsp_jsonrpc.exceptions import JsonRpcException

from pylsp import uris
from pylsp.config.config import Config
Expand All @@ -24,7 +26,7 @@ def main():
@pytest.fixture
def pylsp(tmpdir):
""" Return an initialized python LS """
ls = PythonLSPServer(StringIO, StringIO)
ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint)

ls.m_initialize(
processId=1,
Expand All @@ -38,7 +40,7 @@ def pylsp(tmpdir):
@pytest.fixture
def pylsp_w_workspace_folders(tmpdir):
""" Return an initialized python LS """
ls = PythonLSPServer(StringIO, StringIO)
ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint)

folder1 = tmpdir.mkdir('folder1')
folder2 = tmpdir.mkdir('folder2')
Expand All @@ -63,14 +65,55 @@ def pylsp_w_workspace_folders(tmpdir):
return (ls, workspace_folders)


class FakeEditorMethodsMixin:
"""
Represents the methods to be added to a dispatcher class when faking an editor.
"""
def m_window__work_done_progress__create(self, *_args, **_kwargs):
"""
Fake editor method `window/workDoneProgress/create`.

related spec:
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_workDoneProgress_create
"""
return None


class FakePythonLSPServer(FakeEditorMethodsMixin, PythonLSPServer):
pass


class FakeEndpoint(Endpoint):
"""
Fake Endpoint representing the editor / LSP client.

The `dispatcher` dict will be used to synchronously calculate the responses
for calls to `.request` and resolve the futures with the value or errors.

Fake methods in the `dispatcher` should raise `JsonRpcException` for any
error.
"""
def request(self, method, params=None):
request_future = super().request(method, params)
try:
request_future.set_result(self._dispatcher[method](params))
except JsonRpcException as e:
request_future.set_exception(e)

return request_future


@pytest.fixture()
def consumer():
return MagicMock()


@pytest.fixture()
def endpoint(consumer): # pylint: disable=redefined-outer-name
return Endpoint({}, consumer, id_generator=lambda: "id")
class Dispatcher(FakeEditorMethodsMixin, MethodDispatcher):
pass

return FakeEndpoint(Dispatcher(), consumer, id_generator=lambda: "id")


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion test/test_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import pytest

from pylsp.python_lsp import start_io_lang_server, PythonLSPServer
from pylsp._utils import CALL_TIMEOUT

CALL_TIMEOUT = 10
RUNNING_IN_CI = bool(os.environ.get('CI'))


Expand Down
31 changes: 21 additions & 10 deletions test/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,17 @@ def test_progress_simple(workspace, consumer):
with workspace.report_progress("some_title"):
pass

init_call, *progress_calls = consumer.call_args_list

assert init_call[0][0]['method'] == 'window/workDoneProgress/create'

# same method for all calls
assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
assert all(call[0][0]["method"] == "$/progress" for call in progress_calls), consumer.call_args_list

# same token used in all calls
assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1
assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1

assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [
assert [call[0][0]["params"]["value"] for call in progress_calls] == [
{"kind": "begin", "title": "some_title"},
{"kind": "end"},
]
Expand All @@ -319,13 +323,17 @@ def test_progress_with_percent(workspace, consumer):
progress_message("fifty", 50)
progress_message("ninety", 90)

# same method for all calls
assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
init_call, *progress_calls = consumer.call_args_list

assert init_call[0][0]['method'] == 'window/workDoneProgress/create'

# same method for all progress calls
assert all(call[0][0]["method"] == "$/progress" for call in progress_calls)

# same token used in all calls
assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1
assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1

assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [
assert [call[0][0]["params"]["value"] for call in progress_calls] == [
{
"kind": "begin",
"message": "initial message",
Expand Down Expand Up @@ -353,13 +361,16 @@ class DummyError(Exception):
# test.
pass

init_call, *progress_calls = consumer.call_args_list
assert init_call[0][0]['method'] == 'window/workDoneProgress/create'

# same method for all calls
assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
assert all(call[0][0]["method"] == "$/progress" for call in progress_calls)

# same token used in all calls
assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1
assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1

assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [
assert [call[0][0]["params"]["value"] for call in progress_calls] == [
{"kind": "begin", "title": "some_title"},
{"kind": "end"},
]