mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 04:45:01 +00:00

## 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
173 lines
5.7 KiB
Rust
173 lines
5.7 KiB
Rust
use crate::edit::WorkspaceEditTracker;
|
|
use crate::lint::fixes_for_diagnostics;
|
|
use crate::server::api::LSPResult;
|
|
use crate::server::SupportedCodeAction;
|
|
use crate::server::{client::Notifier, Result};
|
|
use crate::session::DocumentSnapshot;
|
|
use crate::DIAGNOSTIC_NAME;
|
|
use lsp_server::ErrorCode;
|
|
use lsp_types::{self as types, request as req};
|
|
use rustc_hash::FxHashSet;
|
|
use types::{CodeActionKind, CodeActionOrCommand};
|
|
|
|
use super::code_action_resolve::{resolve_edit_for_fix_all, resolve_edit_for_organize_imports};
|
|
|
|
pub(crate) struct CodeActions;
|
|
|
|
impl super::RequestHandler for CodeActions {
|
|
type RequestType = req::CodeActionRequest;
|
|
}
|
|
|
|
impl super::BackgroundDocumentRequestHandler for CodeActions {
|
|
super::define_document_url!(params: &types::CodeActionParams);
|
|
fn run_with_snapshot(
|
|
snapshot: DocumentSnapshot,
|
|
_notifier: Notifier,
|
|
params: types::CodeActionParams,
|
|
) -> Result<Option<types::CodeActionResponse>> {
|
|
let mut response: types::CodeActionResponse = types::CodeActionResponse::default();
|
|
|
|
let supported_code_actions = supported_code_actions(params.context.only.clone());
|
|
|
|
if supported_code_actions.contains(&SupportedCodeAction::QuickFix) {
|
|
response.extend(
|
|
quick_fix(&snapshot, params.context.diagnostics.clone())
|
|
.with_failure_code(ErrorCode::InternalError)?,
|
|
);
|
|
}
|
|
|
|
if supported_code_actions.contains(&SupportedCodeAction::SourceFixAll) {
|
|
response.push(fix_all(&snapshot).with_failure_code(ErrorCode::InternalError)?);
|
|
}
|
|
|
|
if supported_code_actions.contains(&SupportedCodeAction::SourceOrganizeImports) {
|
|
response.push(organize_imports(&snapshot).with_failure_code(ErrorCode::InternalError)?);
|
|
}
|
|
|
|
Ok(Some(response))
|
|
}
|
|
}
|
|
|
|
fn quick_fix(
|
|
snapshot: &DocumentSnapshot,
|
|
diagnostics: Vec<types::Diagnostic>,
|
|
) -> crate::Result<Vec<CodeActionOrCommand>> {
|
|
let document = snapshot.document();
|
|
|
|
let fixes = fixes_for_diagnostics(document, snapshot.encoding(), diagnostics)?;
|
|
|
|
fixes
|
|
.into_iter()
|
|
.map(|fix| {
|
|
let mut tracker = WorkspaceEditTracker::new(snapshot.resolved_client_capabilities());
|
|
|
|
tracker.set_edits_for_document(
|
|
snapshot.url().clone(),
|
|
document.version(),
|
|
fix.edits,
|
|
)?;
|
|
|
|
Ok(types::CodeActionOrCommand::CodeAction(types::CodeAction {
|
|
title: format!("{DIAGNOSTIC_NAME} ({}): {}", fix.code, fix.title),
|
|
kind: Some(types::CodeActionKind::QUICKFIX),
|
|
edit: Some(tracker.into_workspace_edit()),
|
|
diagnostics: Some(vec![fix.fixed_diagnostic.clone()]),
|
|
data: Some(
|
|
serde_json::to_value(snapshot.url()).expect("document url to serialize"),
|
|
),
|
|
..Default::default()
|
|
}))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
|
let document = snapshot.document();
|
|
|
|
let (edit, data) = if snapshot
|
|
.resolved_client_capabilities()
|
|
.code_action_deferred_edit_resolution
|
|
{
|
|
// The editor will request the edit in a `CodeActionsResolve` request
|
|
(
|
|
None,
|
|
Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")),
|
|
)
|
|
} else {
|
|
(
|
|
Some(resolve_edit_for_fix_all(
|
|
document,
|
|
snapshot.resolved_client_capabilities(),
|
|
snapshot.url(),
|
|
&snapshot.configuration().linter,
|
|
snapshot.encoding(),
|
|
document.version(),
|
|
)?),
|
|
None,
|
|
)
|
|
};
|
|
let action = types::CodeAction {
|
|
title: format!("{DIAGNOSTIC_NAME}: Fix all auto-fixable problems"),
|
|
kind: Some(types::CodeActionKind::SOURCE_FIX_ALL),
|
|
edit,
|
|
data,
|
|
..Default::default()
|
|
};
|
|
Ok(types::CodeActionOrCommand::CodeAction(action))
|
|
}
|
|
|
|
fn organize_imports(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
|
let document = snapshot.document();
|
|
|
|
let (edit, data) = if snapshot
|
|
.resolved_client_capabilities()
|
|
.code_action_deferred_edit_resolution
|
|
{
|
|
// The edit will be resolved later in the `CodeActionsResolve` request
|
|
(
|
|
None,
|
|
Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")),
|
|
)
|
|
} else {
|
|
(
|
|
Some(resolve_edit_for_organize_imports(
|
|
document,
|
|
snapshot.resolved_client_capabilities(),
|
|
snapshot.url(),
|
|
&snapshot.configuration().linter,
|
|
snapshot.encoding(),
|
|
document.version(),
|
|
)?),
|
|
None,
|
|
)
|
|
};
|
|
let action = types::CodeAction {
|
|
title: format!("{DIAGNOSTIC_NAME}: Organize imports"),
|
|
kind: Some(types::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
|
|
edit,
|
|
data,
|
|
..Default::default()
|
|
};
|
|
Ok(types::CodeActionOrCommand::CodeAction(action))
|
|
}
|
|
|
|
/// If `action_filter` is `None`, this returns [`SupportedCodeActionKind::all()`]. Otherwise,
|
|
/// the list is filtered.
|
|
fn supported_code_actions(
|
|
action_filter: Option<Vec<CodeActionKind>>,
|
|
) -> FxHashSet<SupportedCodeAction> {
|
|
let Some(action_filter) = action_filter else {
|
|
return SupportedCodeAction::all().collect();
|
|
};
|
|
|
|
SupportedCodeAction::all()
|
|
.filter(move |action| {
|
|
action_filter.iter().any(|filter| {
|
|
action
|
|
.kinds()
|
|
.iter()
|
|
.any(|kind| kind.as_str().starts_with(filter.as_str()))
|
|
})
|
|
})
|
|
.collect()
|
|
}
|