Skip to content

Return an LspResult from LSP Request handlers#1028

Open
DavisVaughan wants to merge 1 commit intomainfrom
feature/statement-range-error
Open

Return an LspResult from LSP Request handlers#1028
DavisVaughan wants to merge 1 commit intomainfrom
feature/statement-range-error

Conversation

@DavisVaughan
Copy link
Contributor

I'm starting to look at posit-dev/positron#8350, and I believe that statement_range() is going to need to be able to return a structured jsonrpc::Error with a well defined code and message that we can pick up on on the frontend.

In particular, I'm likely going to have it return RequestFailed
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage

/**
	 * A request failed but it was syntactically correct, e.g the
	 * method name was known and the parameters were valid. The error
	 * message should contain human readable information about why
	 * the request failed.
	 *
	 * @since 3.17.0
	 */
	export const RequestFailed: [integer](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#integer) = -32803;

Currently all of our LSP Request handlers return an anyhow::Result, and at the very last second we convert any anyhow::Errors into a jsonrpc::Error with an arbitrary -1 code. This is no longer going to be good enough for cases where we need to return structured jsonrpc::Errors with a specific code.

Luckily I think we can get the best of both worlds, here's what I've come up with for the return type of all Request handlers:

pub(crate) type LspResult<T> = std::result::Result<T, LspError>;

#[derive(Debug)]
pub(crate) enum LspError {
    JsonRpc(jsonrpc::Error),
    Anyhow(anyhow::Error),
}

This comes with nice From methods

// For the ability to `?` a `jsonrpc::Error` into an `LspError`
impl From<jsonrpc::Error> for LspError {
    fn from(error: jsonrpc::Error) -> Self {
        Self::JsonRpc(error)
    }
}

// For the ability to `?` an `anyhow::Error` into an `LspError`
impl From<anyhow::Error> for LspError {
    fn from(error: anyhow::Error) -> Self {
        Self::Anyhow(error)
    }
}

which means there is very little code to change. We can still use ? on any helper method that returns an anyhow::Result, and the anyhow::Error will get wrapped into LspError::Anyhow automatically.

I haven't actually done anything in statement_range() yet, but I'm planning on returning something like this using this new LspResult feature:

// Unfortunately `tower_lsp::jsonrpc::ErrorCode::RequestFailed` isn't provided yet.
const REQUEST_FAILED: i64 = -32803;
return LspResult::Err(LspError::JsonRpc(jsonrpc::Error {
    code: jsonrpc::ErrorCode::ServerError(REQUEST_FAILED),
    message: Cow::Owned(String::from("Parse failure")),
    data: None,
}));

Disabled,
Crashed(anyhow::Result<LspResponse>),
Result(anyhow::Result<LspResponse>),
Crashed(anyhow::Error),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recognized along the way that Crashed is only ever intended to contain an anyhow::Error, i.e. it will never hold the Ok() variant of an anyhow::Result<>, so we can simplify this a little.

Comment on lines +80 to +83
RequestResponse::Result(Err(err)) => match err {
LspError::JsonRpc(err) => Err(err),
LspError::Anyhow(err) => Err(new_jsonrpc_error(format!("{err:?}"))),
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is most of the magic. We can now build a context aware jsonrpc::Error at the failure site and pipe it all the way through to be sent to the frontend. I think this will be useful for more than just statement_range() going forward.

Comment on lines 458 to 498
@@ -478,22 +480,19 @@ fn respond<T>(
// This creates an uninformative backtrace that is reported in the
// LSP logs. Note that the relevant backtrace is the one created by
// our panic hook and reported via the _kernel_ logs.
anyhow!("Panic occurred while handling request: {msg}")
})
// Unwrap nested Result
.and_then(|resp| resp);

let out = match response {
Ok(_) => Ok(()),
Err(ref err) => Err(anyhow!("Error while handling request:\n{err:?}")),
RequestResponse::Crashed(anyhow!("Panic occurred while handling request: {msg}"))
},
};

let response = response.map(into_lsp_response);

let response = if crashed {
RequestResponse::Crashed(response)
} else {
RequestResponse::Result(response)
let out = match response {
RequestResponse::Result(Ok(_)) => Ok(()),
RequestResponse::Result(Err(ref error)) => {
Err(anyhow!("Error while handling request:\n{error:?}"))
},
RequestResponse::Crashed(ref error) => {
Err(anyhow!("Crashed while handling request:\n{error:?}"))
},
RequestResponse::Disabled => Err(anyhow!("Received impossible `Disabled` response state")),
};

// Ignore errors from a closed channel. This indicates the request has
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit of fiddling to get this right, but I think it reads a little clearer in the end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant