ruff server now supports commands for auto-fixing, organizing imports, and formatting (#10654)

## Summary

This builds off of the work in
https://github.com/astral-sh/ruff/pull/10652 to implement a command
executor, backwards compatible with the commands from the previous LSP
(`ruff.applyAutofix`, `ruff.applyFormat` and
`ruff.applyOrganizeImports`).

This involved a lot of refactoring and tweaks to the code action
resolution code - the most notable change is that workspace edits are
specified in a slightly different way, using the more general `changes`
field instead of the `document_changes` field (which isn't supported on
all LSP clients). Additionally, the API for synchronous request handlers
has been updated to include access to the `Requester`, which we use to
send a `workspace/applyEdit` request to the client.

## Test Plan



7932e30f-d944-4e35-b828-1d81aa56c087
This commit is contained in:
Jane Lewis 2024-04-05 16:27:35 -07:00 committed by GitHub
parent 1b31d4e9f1
commit c11e6d709c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 379 additions and 96 deletions

View file

@ -0,0 +1,153 @@
use std::str::FromStr;
use crate::edit::WorkspaceEditTracker;
use crate::server::api::LSPResult;
use crate::server::client;
use crate::server::schedule::Task;
use crate::session::Session;
use crate::DIAGNOSTIC_NAME;
use crate::{edit::DocumentVersion, server};
use lsp_server::ErrorCode;
use lsp_types::{self as types, request as req};
use serde::Deserialize;
#[derive(Debug)]
enum Command {
Format,
FixAll,
OrganizeImports,
}
pub(crate) struct ExecuteCommand;
#[derive(Deserialize)]
struct TextDocumentArgument {
uri: types::Url,
version: DocumentVersion,
}
impl super::RequestHandler for ExecuteCommand {
type RequestType = req::ExecuteCommand;
}
impl super::SyncRequestHandler for ExecuteCommand {
fn run(
session: &mut Session,
_notifier: client::Notifier,
requester: &mut client::Requester,
params: types::ExecuteCommandParams,
) -> server::Result<Option<serde_json::Value>> {
let command =
Command::from_str(&params.command).with_failure_code(ErrorCode::InvalidParams)?;
// check if we can apply a workspace edit
if !session.resolved_client_capabilities().apply_edit {
return Err(anyhow::anyhow!("Cannot execute the '{}' command: the client does not support `workspace/applyEdit`", command.label())).with_failure_code(ErrorCode::InternalError);
}
let mut arguments: Vec<TextDocumentArgument> = params
.arguments
.into_iter()
.map(|value| serde_json::from_value(value).with_failure_code(ErrorCode::InvalidParams))
.collect::<server::Result<_>>()?;
arguments.sort_by(|a, b| a.uri.cmp(&b.uri));
arguments.dedup_by(|a, b| a.uri == b.uri);
let mut edit_tracker = WorkspaceEditTracker::new(session.resolved_client_capabilities());
for TextDocumentArgument { uri, version } in arguments {
let snapshot = session
.take_snapshot(&uri)
.ok_or(anyhow::anyhow!("Document snapshot not available for {uri}",))
.with_failure_code(ErrorCode::InternalError)?;
match command {
Command::FixAll => {
let edits = super::code_action_resolve::fix_all_edit(
snapshot.document(),
&snapshot.configuration().linter,
snapshot.encoding(),
)
.with_failure_code(ErrorCode::InternalError)?;
edit_tracker
.set_edits_for_document(uri, version, edits)
.with_failure_code(ErrorCode::InternalError)?;
}
Command::Format => {
let response = super::format::format_document(&snapshot)?;
if let Some(edits) = response {
edit_tracker
.set_edits_for_document(uri, version, edits)
.with_failure_code(ErrorCode::InternalError)?;
}
}
Command::OrganizeImports => {
let edits = super::code_action_resolve::organize_imports_edit(
snapshot.document(),
&snapshot.configuration().linter,
snapshot.encoding(),
)
.with_failure_code(ErrorCode::InternalError)?;
edit_tracker
.set_edits_for_document(uri, version, edits)
.with_failure_code(ErrorCode::InternalError)?;
}
}
}
if !edit_tracker.is_empty() {
apply_edit(
requester,
command.label(),
edit_tracker.into_workspace_edit(),
)
.with_failure_code(ErrorCode::InternalError)?;
}
Ok(None)
}
}
impl Command {
fn label(&self) -> &str {
match self {
Self::FixAll => "Fix all auto-fixable problems",
Self::Format => "Format document",
Self::OrganizeImports => "Format imports",
}
}
}
impl FromStr for Command {
type Err = anyhow::Error;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Ok(match name {
"ruff.applyAutofix" => Self::FixAll,
"ruff.applyFormat" => Self::Format,
"ruff.applyOrganizeImports" => Self::OrganizeImports,
_ => return Err(anyhow::anyhow!("Invalid command `{name}`")),
})
}
}
fn apply_edit(
requester: &mut client::Requester,
label: &str,
edit: types::WorkspaceEdit,
) -> crate::Result<()> {
requester.request::<req::ApplyWorkspaceEdit>(
types::ApplyWorkspaceEditParams {
label: Some(format!("{DIAGNOSTIC_NAME}: {label}")),
edit,
},
|response| {
if !response.applied {
let reason = response
.failure_reason
.unwrap_or_else(|| String::from("unspecified reason"));
tracing::error!("Failed to apply workspace edit: {}", reason);
}
Task::nothing()
},
)
}