Use asyncio (2) - #3021
Conversation
Also write the rest of the requests in terms of Session.request
- Add sublime.set_timeout executor wrapper - Make all request handlers `async` - Define a CancellableInflightStreamingRequest class that enables `async for` syntax - Start inheriting DocumentSyncListener from sublime_aio.ViewEventListener (This one doesn't work yet) The state is fairly broken at this point.
- Typo fixes: various typos fixed - Session starting logic: only partially. Sessions attach to listeners now, but, only one session starts while multiple should start. I think the solution is now to simply start all the `WindowManager.start` coroutines at the same time. They'll wait on each other via the `WindowManager._start_lock`. - Fix folding ranges using `send_request_async` while it should be using `send_request` (because it calls that from the main thread). - Requests seem to be generally working *provided pull diagnostics are not used*. Testing with clangd works, testing with pyright shows requests not working and something fundamental being stuck somewhere. Broken: - didOpen/didClose is sent two times - Workspace/pull diagnostics are broken - There's various `sublime.set_timeout_async` calls throughout the codebase, but we're now at the point where that's "wrong". I hope to find-and-replace these invocations with `sublime_aio.call_soon_threadsafe`.
✅ Deploy Preview for sublime-lsp ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| if task := self.create_task_threadsafe( | ||
| self.run_command(command, progress=progress, view=view, is_refactoring=is_refactoring) | ||
| ): | ||
| return Promise.wrap_task(task) | ||
| raise RuntimeError("unable to schedule task") |
There was a problem hiding this comment.
self.create_task_threadsafe doesn't return anything so this will always throw.
| self._plugin = self._plugin_class(weakref.ref(self)) | ||
| self.transport = transport | ||
| self.working_directory = working_directory | ||
| self._variables = variables |
There was a problem hiding this comment.
This line was dropped but there is still code that references self._variables which now will always be empty.
| return | ||
| self.workspace_diagnostics_pending_responses[identifier] = None | ||
| return | ||
| self._on_workspace_diagnostics_async(identifier, response, reset_pending_response=False) |
There was a problem hiding this comment.
All callers now call _on_workspace_diagnostics_async with reset_pending_response=False. That doesn't seem right.
| 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) | ||
| lambda: self._invoke_views(request, "on_request_progress", request_id, params) |
There was a problem hiding this comment.
This syntax doesn't quite make sense - lambda created and discarded immediately?
| if os.path.isfile(new_path): # noqa: ASYNC240 | ||
| if options.get('overwrite') and os.path.isfile(old_path): # noqa: ASYNC240 | ||
| await delete_file(new_path) | ||
| await _continue(rename_file(old_path, new_path)) |
| while os.path.exists(path) and attempts < self._FILE_DELETED_MAX_CHECK_ATTEMPTS: # noqa: ASYNC240 | ||
| await asyncio.sleep(0.1) | ||
| attempts += 1 | ||
| if attempts >= self._FILE_DELETED_MAX_CHECK_ATTEMPTS: | ||
| raise asyncio.TimeoutError(f"Timeout waiting for deletion of {path}") |
There was a problem hiding this comment.
There should be a final check after the last sleep. Otherwise that last sleep is pointless.
| with self._threading_condition: | ||
| run_on_asyncio_thread(set_request_id) | ||
| self._threading_condition.wait_for(lambda: request_id is not None) |
There was a problem hiding this comment.
This will deadlock when request errors?
| return cast('ApplyWorkspaceEditResult', {}), cast('WorkspaceEditSummary', {}) | ||
| return x | ||
|
|
||
| if task := self.create_task(self.apply_workspace_edit(edit, label=label, is_refactoring=is_refactoring)): |
There was a problem hiding this comment.
Should this use a threadsafe variant to create task?
| def purge_changes_async(self) -> None: | ||
| def purge_changes(self) -> asyncio.Future[list[BaseException | None]]: | ||
| raise NotImplementedError | ||
|
|
||
| @abstractmethod | ||
| def trigger_on_pre_save_async(self) -> None: | ||
| def trigger_on_pre_save(self) -> asyncio.Future[list[BaseException | None]]: |
There was a problem hiding this comment.
Is there specific reason for annotating the return value with asyncio.Future vs. just using async keyword on the method?
Some methods here do it one way and some do another.
There was a problem hiding this comment.
The reason for returning asyncio.Future is that this function will unconditionally do that.
In a coroutine, say
async def trigger_on_pre_save(self) -> list[BaseException | None]:
...you can conditionally return early, or conditionally await something and suspend. You need the function to be a coroutine function in that case.
But unconditionally, always, returning something awaitable, such a function does not have to be a coroutine function but can be a "simpler" regular function that returns the Future.
There was a problem hiding this comment.
And why do that? What is the benefit of trigger_on_pre_save implementation returning a Future rather than awaiting it itself?
| _id: int | None | ||
| _weaksession: weakref.ref[Session] |
There was a problem hiding this comment.
Why declare private properties this way? I would understand public, to act as a documentation, but for private this seems unnecessary. It would be better to let the type be inferred from the assignment in __init__ or assign explicit type there (can matter if class is overridden).
| ) -> None: | ||
| transport: TransportWrapper | ||
| ) -> InitializeResult | Error: | ||
| loop = asyncio.get_running_loop() |
There was a problem hiding this comment.
Is there a particular reason why loop variable is assigned on the first line if it's only needed in the last line?
| task: PackagedTask[R | Error | None] = Promise.packaged_task() | ||
| task: PackagedTask[LSPAny | Error | None] = Promise.packaged_task() | ||
| promise, resolve = task | ||
| if self._plugin.on_pre_server_command(command, lambda: resolve(None)): | ||
| return promise | ||
| resolve(None) | ||
| return cast("LSPAny", await promise) |
There was a problem hiding this comment.
There was a resolve(None) here before. I think it should still be here. Not sure if not having it would prevent GC but even if not, better to clean after ourselves.
| code_action_or_error = await self._maybe_resolve_code_action(code_action, view) | ||
| if isinstance(code_action_or_error, Error): | ||
| return code_action_or_error | ||
| return await self._apply_code_action(code_action_or_error, view) |
There was a problem hiding this comment.
_apply_code_action does handle Error too. If you've made decision here to handle error at call-site then we can now remove Error handling from _apply_code_action.
| return await self._open_file_uri(uri, r, flags, group) | ||
|
|
||
| # Try to find a pre-existing session-buffer | ||
| if sb := self.get_session_buffer_for_uri_async(uri): | ||
| view = sb.get_view_in_group(group) | ||
| self.window.focus_view(view) | ||
| if r: | ||
| center_selection(view, r) | ||
| return Promise.resolve(view) | ||
| return view | ||
| if scheme == 'res': | ||
| return self._open_res_uri_async(uri, r, group) | ||
| return await self._open_res_uri(uri, r, group) | ||
|
|
||
| if scheme == 'untitled': # VSCode specific URI scheme for unsaved buffers |
| return | ||
| if isinstance(code_action, Error): | ||
| # TODO: our promise must be able to handle exceptions (or, wait until we can use coroutines) | ||
| # TODO: do something with the error? |
There was a problem hiding this comment.
don't see what else than printing an error we can do
| x: tuple[ApplyWorkspaceEditResult, WorkspaceEditSummary] | BaseException, | ||
| ) -> tuple[ApplyWorkspaceEditResult, WorkspaceEditSummary]: | ||
| if isinstance(x, BaseException): | ||
| return cast('ApplyWorkspaceEditResult', {}), cast('WorkspaceEditSummary', {}) |
There was a problem hiding this comment.
Forcing types could lead to crashes. This might be better:
| return cast('ApplyWorkspaceEditResult', {}), cast('WorkspaceEditSummary', {}) | |
| return {"applied": False}, {"created_files": 0, "edited_files": 0, "total_changes": 0} |
This PR switches the codebase to using
async deffunctions andasyncio. The loop provider issublime_aio.close #2863.
should be merged (and released) at the same time as:
The main driver for doing this is to decrease the thread usage of this plugin from O(n) to O(1) threads, where
nis the number of language servers running. The secondary driver is syntax sugar.Why is this PR so large? Please read: What color is your function?
Continuation of: #2880