ruff server now supports source.fixAll source action (#10597)

## Summary

`ruff server` now has source action `source.fixAll` as an available code
action.

This also fixes https://github.com/astral-sh/ruff/issues/10593 in the
process of revising the code for quick fix code actions.

## Test Plan




f4c07425-e68a-445f-a4ed-949c9197a6be
This commit is contained in:
Jane Lewis 2024-04-03 09:22:17 -07:00 committed by GitHub
parent d467aa78c2
commit 257964a8bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 564 additions and 191 deletions

View file

@ -1,81 +1,131 @@
use crate::edit::ToRangeExt;
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 ruff_text_size::Ranged;
use rustc_hash::FxHashSet;
use types::{CodeActionKind, CodeActionOrCommand};
pub(crate) struct CodeAction;
use super::code_action_resolve::resolve_edit_for_fix_all;
impl super::RequestHandler for CodeAction {
pub(crate) struct CodeActions;
impl super::RequestHandler for CodeActions {
type RequestType = req::CodeActionRequest;
}
impl super::BackgroundDocumentRequestHandler for CodeAction {
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 document = snapshot.document();
let url = snapshot.url();
let encoding = snapshot.encoding();
let version = document.version();
let actions: Result<Vec<_>> = params
.context
.diagnostics
.into_iter()
.map(|diagnostic| {
let Some(data) = diagnostic.data else {
return Ok(None);
};
let diagnostic_fix: crate::lint::DiagnosticFix = serde_json::from_value(data)
.map_err(|err| anyhow::anyhow!("failed to deserialize diagnostic data: {err}"))
.with_failure_code(lsp_server::ErrorCode::ParseError)?;
let edits = diagnostic_fix
.fix
.edits()
.iter()
.map(|edit| types::TextEdit {
range: edit.range().to_range(
document.contents(),
document.index(),
encoding,
),
new_text: edit.content().unwrap_or_default().to_string(),
});
let mut response: types::CodeActionResponse = types::CodeActionResponse::default();
let changes = vec![types::TextDocumentEdit {
text_document: types::OptionalVersionedTextDocumentIdentifier::new(
url.clone(),
version,
),
edits: edits.map(types::OneOf::Left).collect(),
}];
let supported_code_actions = supported_code_actions(params.context.only);
let title = diagnostic_fix
.kind
.suggestion
.unwrap_or(diagnostic_fix.kind.name);
Ok(Some(types::CodeAction {
title,
kind: Some(types::CodeActionKind::QUICKFIX),
edit: Some(types::WorkspaceEdit {
document_changes: Some(types::DocumentChanges::Edits(changes)),
..Default::default()
}),
..Default::default()
}))
})
.collect();
if supported_code_actions.contains(&SupportedCodeAction::QuickFix) {
response.extend(
quick_fix(&snapshot, params.context.diagnostics)
.with_failure_code(ErrorCode::InternalError)?,
);
}
Ok(Some(
actions?
.into_iter()
.flatten()
.map(types::CodeActionOrCommand::CodeAction)
.collect(),
))
if supported_code_actions.contains(&SupportedCodeAction::SourceFixAll) {
response.push(fix_all(&snapshot).with_failure_code(ErrorCode::InternalError)?);
}
if supported_code_actions.contains(&SupportedCodeAction::SourceOrganizeImports) {
todo!("Implement the `source.organizeImports` code action");
}
Ok(Some(response))
}
}
fn quick_fix(
snapshot: &DocumentSnapshot,
diagnostics: Vec<types::Diagnostic>,
) -> crate::Result<impl Iterator<Item = CodeActionOrCommand> + '_> {
let document = snapshot.document();
let fixes = fixes_for_diagnostics(
document,
snapshot.url(),
snapshot.encoding(),
document.version(),
diagnostics,
)?;
Ok(fixes.into_iter().map(|fix| {
types::CodeActionOrCommand::CodeAction(types::CodeAction {
title: format!("{DIAGNOSTIC_NAME} ({}): {}", fix.code, fix.title),
kind: Some(types::CodeActionKind::QUICKFIX),
edit: Some(types::WorkspaceEdit {
document_changes: Some(types::DocumentChanges::Edits(fix.document_edits.clone())),
..Default::default()
}),
diagnostics: Some(vec![fix.fixed_diagnostic.clone()]),
data: Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")),
..Default::default()
})
}))
}
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.url(),
&snapshot.configuration().linter,
snapshot.encoding(),
)?),
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))
}
/// 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()
}