From 83594b6a445a86940e6a4bdce4e23f26905bd432 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 1 Jan 2023 18:19:58 +0100 Subject: [PATCH 1/3] Allow missing window/workDoneProgress/create request from the server This commit adds a workaround for servers that report server-initiated progress but don't request window/workDoneProgress/create before reporting progress. --- plugin/active_request.py | 72 ++++++++++++++++++++++++++++++++++++++++ plugin/core/sessions.py | 37 ++++++++++++++++----- plugin/session_view.py | 49 ++++++--------------------- 3 files changed, 111 insertions(+), 47 deletions(-) create mode 100644 plugin/active_request.py diff --git a/plugin/active_request.py b/plugin/active_request.py new file mode 100644 index 000000000..bc18ebd29 --- /dev/null +++ b/plugin/active_request.py @@ -0,0 +1,72 @@ +from .core.sessions import SessionViewProtocol +from .core.progress import ProgressReporter +from .core.progress import ViewProgressReporter +from .core.progress import WindowProgressReporter +from .core.protocol import Request +from .core.typing import Any, Optional, Dict +from weakref import ref +import sublime + +class ActiveRequest: + """ + Holds state per request. + """ + + def __init__(self, sv: SessionViewProtocol, request_id: int, request: Request) -> None: + # sv is the parent object; there is no need to keep it alive explicitly. + self.weaksv = ref(sv) + self.request_id = request_id + self.request = request + self.progress = None # type: Optional[ProgressReporter] + # `request.progress` is either a boolean or a string. If it's a boolean, then that signals that the server does + # not support client-initiated progress. However, for some requests we still want to notify some kind of + # progress to the end-user. This is communicated by the boolean value being "True". + # If `request.progress` is a string, then this string is equal to the workDoneProgress token. In that case, the + # server should start reporting progress for this request. However, even if the server supports workDoneProgress + # then we still don't know for sure whether it will actually start reporting progress. So we still want to + # put a line in the status bar if the request takes a while even if the server promises to report progress. + if request.progress: + # Keep a weak reference because we don't want this delayed function to keep this object alive. + weakself = ref(self) + + def show() -> None: + this = weakself() + # If the server supports client-initiated progress, then it should have sent a "progress begin" + # notification. In that case, `this.progress` should not be None. So if `this.progress` is None + # then the server didn't notify in a timely manner and we will start putting a line in the status bar + # about this request taking a long time (>200ms). + if this is not None and this.progress is None: + # If this object is still alive then that means the request hasn't finished yet after 200ms, + # so put a message in the status bar to notify that this request is still in progress. + this.progress = this._start_progress_reporter_async(this.request.method) + + sublime.set_timeout_async(show, 200) + + def _start_progress_reporter_async( + self, + title: str, + message: Optional[str] = None, + percentage: Optional[float] = None + ) -> Optional[ProgressReporter]: + sv = self.weaksv() + if not sv: + return None + if self.request.view is not None: + key = "lspprogressview-{}-{}-{}".format(sv.session.config.name, self.request.view.id(), self.request_id) + return ViewProgressReporter(self.request.view, key, title, message, percentage) + else: + key = "lspprogresswindow-{}-{}-{}".format(sv.session.config.name, sv.session.window.id(), self.request_id) + return WindowProgressReporter(sv.session.window, key, title, message, percentage) + + def update_progress_async(self, params: Dict[str, Any]) -> None: + value = params['value'] + kind = value['kind'] + message = value.get("message") + percentage = value.get("percentage") + if kind == 'begin': + title = value["title"] + # This would potentially overwrite the "manual" progress that activates after 200ms, which is OK. + self.progress = self._start_progress_reporter_async(title, message, percentage) + elif kind == 'report': + if self.progress: + self.progress(message, percentage) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 947862d43..53592fcd7 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1123,7 +1123,8 @@ def check_applicable(self, sb: SessionBufferProtocol) -> None: return -_WORK_DONE_PROGRESS_PREFIX = "wd" +# This prefix should disambiguate common string generation techniques like UUID4. +_WORK_DONE_PROGRESS_PREFIX = "$ublime-" class Session(TransportCallbacks): @@ -1810,28 +1811,46 @@ def _invoke_views(self, request: Request, method: str, *args: Any) -> None: for sv in self.session_views_async(): getattr(sv, method)(*args) + def _create_window_progress_reporter(self, token: str, value: Dict[str, Any]) -> None: + self._progress[token] = WindowProgressReporter( + window=self.window, + key="lspprogress{}{}".format(self.config.name, token), + title=value["title"], + message=value.get("message") + ) + def m___progress(self, params: Any) -> None: """handles the $/progress notification""" token = params['token'] + value = params['value'] + kind = value['kind'] if token not in self._progress: + # If the token is not in the _progress map then that could mean two things: + # + # 1) The server is reporting on our client-initiated request progress. In that case, the progress token + # should be of the form $_WORK_DONE_PROGRESS_PREFIX$RequestId. We try to parse it, and if it succeeds, + # we can delegate to the appropriate session view instances. + # + # 2) The server is not spec-compliant and reports progress using server-initiated progress but didn't + # call window/workDoneProgress/create before hand. In that case, we check the 'kind' field of the + # progress data. If the 'kind' field is 'begin', we set up a progress reporter anyway. try: request_id = int(token[len(_WORK_DONE_PROGRESS_PREFIX):]) request = self._response_handlers[request_id][0] self._invoke_views(request, "on_request_progress", request_id, params) return except (IndexError, ValueError, KeyError): + # The parse failed so possibility (1) is apparently not applicable. At this point we may still be + # dealing with possibility (2). + if kind == 'begin': + # We are dealing with possibility (2), so create the progress reporter now. + self._create_window_progress_reporter(token, value) + return pass debug('unknown $/progress token: {}'.format(token)) return - value = params['value'] - kind = value['kind'] if kind == 'begin': - self._progress[token] = WindowProgressReporter( - window=self.window, - key="lspprogress{}{}".format(self.config.name, token), - title=value["title"], - message=value.get("message") - ) + self._create_window_progress_reporter(token, value) elif kind == 'report': progress = self._progress[token] assert isinstance(progress, WindowProgressReporter) diff --git a/plugin/session_view.py b/plugin/session_view.py index fbc4e0717..9e50b49d5 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -1,5 +1,5 @@ +from .active_request import ActiveRequest from .code_lens import CodeLensView -from .core.progress import ViewProgressReporter from .core.promise import Promise from .core.protocol import CodeLens from .core.protocol import CodeLensExtended @@ -10,7 +10,6 @@ from .core.sessions import AbstractViewListener from .core.sessions import Session from .core.settings import userprefs -from .core.types import debounced from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY from .core.views import text_document_identifier @@ -43,9 +42,8 @@ def __init__(self, listener: AbstractViewListener, session: Session, uri: Docume self._view = listener.view self._session = session self._initialize_region_keys() - self.active_requests = {} # type: Dict[int, Request] + self._active_requests = {} # type: Dict[int, ActiveRequest] self._listener = ref(listener) - self.progress = {} # type: Dict[int, ViewProgressReporter] self._code_lenses = CodeLensView(self._view) self.code_lenses_needs_refresh = False settings = self._view.settings() @@ -74,8 +72,8 @@ def on_before_remove(self) -> None: # If the session is exiting then there's no point in sending textDocument/didClose and there's also no point # in unregistering ourselves from the session. if not self.session.exiting: - for request_id, request in self.active_requests.items(): - if request.view and request.view.id() == self.view.id(): + for request_id, data in self._active_requests.items(): + if data.request.view and data.request.view.id() == self.view.id(): self.session.send_notification(Notification("$/cancelRequest", {"id": request_id})) self.session.unregister_session_view_async(self) self.session.config.erase_view_status(self.view) @@ -304,31 +302,15 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, self.view.erase_regions("{}_underline".format(key)) def on_request_started_async(self, request_id: int, request: Request) -> None: - self.active_requests[request_id] = request - if request.progress: - debounced( - functools.partial(self._start_progress_reporter_async, request_id, request.method), - timeout_ms=200, - condition=lambda: request_id in self.active_requests and request_id not in self.progress, - async_thread=True - ) + self._active_requests[request_id] = ActiveRequest(self, request_id, request) def on_request_finished_async(self, request_id: int) -> None: - self.active_requests.pop(request_id, None) - self.progress.pop(request_id, None) + self._active_requests.pop(request_id, None) def on_request_progress(self, request_id: int, params: Dict[str, Any]) -> None: - value = params['value'] - kind = value['kind'] - if kind == 'begin': - title = value["title"] - progress = self.progress.get(request_id) - if not progress: - progress = self._start_progress_reporter_async(request_id, title) - progress.title = title - progress(value.get("message"), value.get("percentage")) - elif kind == 'report': - self.progress[request_id](value.get("message"), value.get("percentage")) + request = self._active_requests.get(request_id, None) + if request: + request.update_progress_async(params) def on_text_changed_async(self, change_count: int, changes: Iterable[sublime.TextChange]) -> None: self.session_buffer.on_text_changed_async(self.view, change_count, changes) @@ -348,21 +330,12 @@ def on_pre_save_async(self) -> None: def on_post_save_async(self, new_uri: DocumentUri) -> None: self.session_buffer.on_post_save_async(self.view, new_uri) - def _start_progress_reporter_async(self, request_id: int, title: str) -> ViewProgressReporter: - progress = ViewProgressReporter( - view=self.view, - key="lspprogressview{}{}".format(self.session.config.name, request_id), - title=title - ) - self.progress[request_id] = progress - return progress - # --- textDocument/codeLens ---------------------------------------------------------------------------------------- def start_code_lenses_async(self) -> None: params = {'textDocument': text_document_identifier(self.view)} - for request_id, request in self.active_requests.items(): - if request.method == "codeAction/resolve": + for request_id, data in self._active_requests.items(): + if data.request.method == "codeAction/resolve": self.session.send_notification(Notification("$/cancelRequest", {"id": request_id})) self.session.send_request_async(Request("textDocument/codeLens", params, self.view), self._on_code_lenses_async) From 2525d25613184d671ceb04ed0e22c85f426dd7a5 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 1 Jan 2023 20:51:47 +0100 Subject: [PATCH 2/3] missing blank line --- plugin/active_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/active_request.py b/plugin/active_request.py index bc18ebd29..697f17c4a 100644 --- a/plugin/active_request.py +++ b/plugin/active_request.py @@ -7,6 +7,7 @@ from weakref import ref import sublime + class ActiveRequest: """ Holds state per request. From 703b00d1e0de59fcde18f2e630bfb17cc3da0c92 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Thu, 5 Jan 2023 22:40:39 +0100 Subject: [PATCH 3/3] Move active_request.py to plugin/core --- plugin/{ => core}/active_request.py | 12 ++++++------ plugin/session_view.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) rename plugin/{ => core}/active_request.py (93%) diff --git a/plugin/active_request.py b/plugin/core/active_request.py similarity index 93% rename from plugin/active_request.py rename to plugin/core/active_request.py index 697f17c4a..bdd39c9e7 100644 --- a/plugin/active_request.py +++ b/plugin/core/active_request.py @@ -1,9 +1,9 @@ -from .core.sessions import SessionViewProtocol -from .core.progress import ProgressReporter -from .core.progress import ViewProgressReporter -from .core.progress import WindowProgressReporter -from .core.protocol import Request -from .core.typing import Any, Optional, Dict +from .sessions import SessionViewProtocol +from .progress import ProgressReporter +from .progress import ViewProgressReporter +from .progress import WindowProgressReporter +from .protocol import Request +from .typing import Any, Optional, Dict from weakref import ref import sublime diff --git a/plugin/session_view.py b/plugin/session_view.py index 9e50b49d5..6c6043c8e 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -1,5 +1,5 @@ -from .active_request import ActiveRequest from .code_lens import CodeLensView +from .core.active_request import ActiveRequest from .core.promise import Promise from .core.protocol import CodeLens from .core.protocol import CodeLensExtended