Skip to content

Commit

Permalink
Allow missing window/workDoneProgress/create request from the server (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rwols authored Jan 6, 2023
1 parent bdb0c59 commit f726c90
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 47 deletions.
73 changes: 73 additions & 0 deletions plugin/core/active_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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


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)
37 changes: 28 additions & 9 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,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):
Expand Down Expand Up @@ -1813,28 +1814,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)
Expand Down
49 changes: 11 additions & 38 deletions plugin/session_view.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .code_lens import CodeLensView
from .core.progress import ViewProgressReporter
from .core.active_request import ActiveRequest
from .core.promise import Promise
from .core.protocol import CodeLens
from .core.protocol import CodeLensExtended
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down

0 comments on commit f726c90

Please sign in to comment.