PurpleMyst / sansio-lsp-client

An implementation of the client side of the LSP protocol, useful for embedding easily in your editor.
MIT License
23 stars 9 forks source link

Requests handling is not async safe #67

Open nitanmarcel opened 2 days ago

nitanmarcel commented 2 days ago

Sending and handling requests will block the loop while they are processing. An workaround I found was inheriting lsp.Client to handle some of the work asynchronous.

# https://github.com/dsanders11/chromium-include-cleanup/blob/25ff1861ae8ce9cd0ebd4caa2a5c9c524def9b23/clangd_lsp.py#L60
class AsyncSendLspClient(lsp.Client):
    def _ensure_send_buf_is_queue(self):
        if not isinstance(self._send_buf, asyncio.Queue):
            self._send_buf: asyncio.Queue[bytes] = asyncio.Queue()

    def _send_request(self, method: str, params: Optional[JSONDict] = None) -> int:
        self._ensure_send_buf_is_queue()

        id = self._id_counter
        self._id_counter += 1

        self._send_buf.put_nowait(_make_request(method=method, params=params, id=id))
        self._unanswered_requests[id] = Request(id=id, method=method, params=params)
        return id

    def _send_notification(self, method: str, params: Optional[JSONDict] = None) -> None:
        self._ensure_send_buf_is_queue()
        self._send_buf.put_nowait(_make_request(method=method, params=params))

    def _send_response(
        self,
        id: int,
        result: Optional[JSONDict] = None,
        error: Optional[JSONDict] = None,
    ) -> None:
        self._ensure_send_buf_is_queue()
        self._send_buf.put_nowait(_make_response(id=id, result=result, error=error))

    def _handle_request(self, request: lsp.Request) -> lsp.Event:
        # TODO - This is copied from sansio-lsp-client
        def parse_request(event_cls: Type[lsp.Event]) -> lsp.Event:
            if issubclass(event_cls, lsp.ServerRequest):
                event = parse_obj_as(event_cls, request.params)
                assert request.id is not None
                event._id = request.id
                event._client = self
                return event
            elif issubclass(event_cls, lsp.ServerNotification):
                return parse_obj_as(event_cls, request.params)
            else:
                raise TypeError("`event_cls` must be a subclass of ServerRequest" " or ServerNotification")

        return super()._handle_request(request)

    async def async_send(self) -> bytes:
        return await self._send_buf.get()
PurpleMyst commented 2 days ago

I'm not sure what the issue that you're bringing up is. The fact that the class is "sans io" means you have to handle the sending in some way, perhaps with asyncio, but this can be done from the original class. Could you please elaborate?

nitanmarcel commented 2 days ago

I'm not sure what the issue that you're bringing up is. The fact that the class is "sans io" means you have to handle the sending in some way, perhaps with asyncio, but this can be done from the original class. Could you please elaborate?

I was requesting for completions each type a character was typed in the text area in an editor I did, each time the request was made the editor will freeze for a second until the request was sent and received.

So somewhere around those class modified functions takes to long to process thus blocking the loop until it's done. No idea if that's also the case without asyncio