mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:33:50 +00:00
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:
parent
d467aa78c2
commit
257964a8bc
15 changed files with 564 additions and 191 deletions
|
@ -2,11 +2,13 @@
|
|||
|
||||
mod document;
|
||||
mod range;
|
||||
mod replacement;
|
||||
|
||||
pub use document::Document;
|
||||
pub(crate) use document::DocumentVersion;
|
||||
use lsp_types::PositionEncodingKind;
|
||||
pub(crate) use range::{RangeExt, ToRangeExt};
|
||||
pub(crate) use replacement::Replacement;
|
||||
|
||||
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
|
||||
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
|
||||
|
|
98
crates/ruff_server/src/edit/replacement.rs
Normal file
98
crates/ruff_server/src/edit/replacement.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
pub(crate) struct Replacement {
|
||||
pub(crate) source_range: TextRange,
|
||||
pub(crate) modified_range: TextRange,
|
||||
}
|
||||
|
||||
impl Replacement {
|
||||
/// Creates a [`Replacement`] that describes the `source_range` of `source` to replace
|
||||
/// with `modified` sliced by `modified_range`.
|
||||
pub(crate) fn between(
|
||||
source: &str,
|
||||
source_line_starts: &[TextSize],
|
||||
modified: &str,
|
||||
modified_line_starts: &[TextSize],
|
||||
) -> Self {
|
||||
let mut source_start = TextSize::default();
|
||||
let mut replaced_start = TextSize::default();
|
||||
let mut source_end = source.text_len();
|
||||
let mut replaced_end = modified.text_len();
|
||||
let mut line_iter = source_line_starts
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(modified_line_starts.iter().copied());
|
||||
for (source_line_start, modified_line_start) in line_iter.by_ref() {
|
||||
if source_line_start != modified_line_start
|
||||
|| source[TextRange::new(source_start, source_line_start)]
|
||||
!= modified[TextRange::new(replaced_start, modified_line_start)]
|
||||
{
|
||||
break;
|
||||
}
|
||||
source_start = source_line_start;
|
||||
replaced_start = modified_line_start;
|
||||
}
|
||||
|
||||
let mut line_iter = line_iter.rev();
|
||||
|
||||
for (old_line_start, new_line_start) in line_iter.by_ref() {
|
||||
if old_line_start <= source_start
|
||||
|| new_line_start <= replaced_start
|
||||
|| source[TextRange::new(old_line_start, source_end)]
|
||||
!= modified[TextRange::new(new_line_start, replaced_end)]
|
||||
{
|
||||
break;
|
||||
}
|
||||
source_end = old_line_start;
|
||||
replaced_end = new_line_start;
|
||||
}
|
||||
|
||||
Replacement {
|
||||
source_range: TextRange::new(source_start, source_end),
|
||||
modified_range: TextRange::new(replaced_start, replaced_end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_source_file::LineIndex;
|
||||
|
||||
use super::Replacement;
|
||||
|
||||
#[test]
|
||||
fn find_replacement_range_works() {
|
||||
let original = r#"
|
||||
aaaa
|
||||
bbbb
|
||||
cccc
|
||||
dddd
|
||||
eeee
|
||||
"#;
|
||||
let original_index = LineIndex::from_source_text(original);
|
||||
let new = r#"
|
||||
bb
|
||||
cccc
|
||||
dd
|
||||
"#;
|
||||
let new_index = LineIndex::from_source_text(new);
|
||||
let expected = r#"
|
||||
bb
|
||||
cccc
|
||||
dd
|
||||
"#;
|
||||
let replacement = Replacement::between(
|
||||
original,
|
||||
original_index.line_starts(),
|
||||
new,
|
||||
new_index.line_starts(),
|
||||
);
|
||||
let mut test = original.to_string();
|
||||
test.replace_range(
|
||||
replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(),
|
||||
&new[replacement.modified_range],
|
||||
);
|
||||
|
||||
assert_eq!(expected, &test);
|
||||
}
|
||||
}
|
79
crates/ruff_server/src/fix.rs
Normal file
79
crates/ruff_server/src/fix.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use ruff_linter::{
|
||||
linter::{FixerResult, LinterResult},
|
||||
settings::{flags, types::UnsafeFixes, LinterSettings},
|
||||
source_kind::SourceKind,
|
||||
};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_source_file::LineIndex;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use crate::{
|
||||
edit::{Replacement, ToRangeExt},
|
||||
PositionEncoding,
|
||||
};
|
||||
|
||||
pub(crate) fn fix_all(
|
||||
document: &crate::edit::Document,
|
||||
linter_settings: &LinterSettings,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<Vec<lsp_types::TextEdit>> {
|
||||
let source = document.contents();
|
||||
|
||||
let source_type = PySourceType::default();
|
||||
|
||||
// TODO(jane): Support Jupyter Notebooks
|
||||
let source_kind = SourceKind::Python(source.to_string());
|
||||
|
||||
// We need to iteratively apply all safe fixes onto a single file and then
|
||||
// create a diff between the modified file and the original source to use as a single workspace
|
||||
// edit.
|
||||
// If we simply generated the diagnostics with `check_path` and then applied fixes individually,
|
||||
// there's a possibility they could overlap or introduce new problems that need to be fixed,
|
||||
// which is inconsistent with how `ruff check --fix` works.
|
||||
let FixerResult {
|
||||
transformed,
|
||||
result: LinterResult { error, .. },
|
||||
..
|
||||
} = ruff_linter::linter::lint_fix(
|
||||
Path::new("<filename>"),
|
||||
None,
|
||||
flags::Noqa::Enabled,
|
||||
UnsafeFixes::Disabled,
|
||||
linter_settings,
|
||||
&source_kind,
|
||||
source_type,
|
||||
)?;
|
||||
|
||||
if let Some(error) = error {
|
||||
// abort early if a parsing error occurred
|
||||
return Err(anyhow::anyhow!(
|
||||
"A parsing error occurred during `fix_all`: {error}"
|
||||
));
|
||||
}
|
||||
|
||||
// fast path: if `transformed` is still borrowed, no changes were made and we can return early
|
||||
if let Cow::Borrowed(_) = transformed {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let modified = transformed.source_code();
|
||||
|
||||
let modified_index = LineIndex::from_source_text(modified);
|
||||
|
||||
let source_index = document.index();
|
||||
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
source,
|
||||
source_index.line_starts(),
|
||||
modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
|
||||
Ok(vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(source, source_index, encoding),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}])
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
//! ## The Ruff Language Server
|
||||
|
||||
pub use edit::{Document, PositionEncoding};
|
||||
use lsp_types::CodeActionKind;
|
||||
pub use server::Server;
|
||||
|
||||
mod edit;
|
||||
mod fix;
|
||||
mod format;
|
||||
mod lint;
|
||||
mod server;
|
||||
|
@ -12,6 +14,10 @@ mod session;
|
|||
pub(crate) const SERVER_NAME: &str = "ruff";
|
||||
pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff";
|
||||
|
||||
pub(crate) const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff");
|
||||
pub(crate) const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind =
|
||||
CodeActionKind::new("source.organizeImports.ruff");
|
||||
|
||||
/// A common result type used in most cases where a
|
||||
/// result type is needed.
|
||||
pub(crate) type Result<T> = anyhow::Result<T>;
|
||||
|
|
|
@ -16,14 +16,26 @@ use ruff_python_index::Indexer;
|
|||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::AsMode;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct DiagnosticFix {
|
||||
/// This is serialized on the diagnostic `data` field.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub(crate) struct AssociatedDiagnosticData {
|
||||
pub(crate) kind: DiagnosticKind,
|
||||
pub(crate) fix: Fix,
|
||||
pub(crate) code: String,
|
||||
}
|
||||
|
||||
/// Describes a fix for `fixed_diagnostic` that applies `document_edits` to the source.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DiagnosticFix {
|
||||
pub(crate) fixed_diagnostic: lsp_types::Diagnostic,
|
||||
pub(crate) title: String,
|
||||
pub(crate) code: String,
|
||||
pub(crate) document_edits: Vec<lsp_types::TextDocumentEdit>,
|
||||
}
|
||||
|
||||
pub(crate) fn check(
|
||||
|
@ -78,6 +90,56 @@ pub(crate) fn check(
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn fixes_for_diagnostics<'d>(
|
||||
document: &'d crate::edit::Document,
|
||||
url: &'d lsp_types::Url,
|
||||
encoding: PositionEncoding,
|
||||
version: crate::edit::DocumentVersion,
|
||||
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||
) -> crate::Result<Vec<DiagnosticFix>> {
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(move |mut diagnostic| {
|
||||
let Some(data) = diagnostic.data.take() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let fixed_diagnostic = diagnostic;
|
||||
let associated_data: crate::lint::AssociatedDiagnosticData =
|
||||
serde_json::from_value(data).map_err(|err| {
|
||||
anyhow::anyhow!("failed to deserialize diagnostic data: {err}")
|
||||
})?;
|
||||
let edits = associated_data
|
||||
.fix
|
||||
.edits()
|
||||
.iter()
|
||||
.map(|edit| lsp_types::TextEdit {
|
||||
range: edit
|
||||
.range()
|
||||
.to_range(document.contents(), document.index(), encoding),
|
||||
new_text: edit.content().unwrap_or_default().to_string(),
|
||||
});
|
||||
|
||||
let document_edits = vec![lsp_types::TextDocumentEdit {
|
||||
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier::new(
|
||||
url.clone(),
|
||||
version,
|
||||
),
|
||||
edits: edits.map(lsp_types::OneOf::Left).collect(),
|
||||
}];
|
||||
Ok(Some(DiagnosticFix {
|
||||
fixed_diagnostic,
|
||||
code: associated_data.code,
|
||||
title: associated_data
|
||||
.kind
|
||||
.suggestion
|
||||
.unwrap_or(associated_data.kind.name),
|
||||
document_edits,
|
||||
}))
|
||||
})
|
||||
.filter_map(crate::Result::transpose)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_lsp_diagnostic(
|
||||
diagnostic: Diagnostic,
|
||||
document: &crate::edit::Document,
|
||||
|
@ -92,9 +154,10 @@ fn to_lsp_diagnostic(
|
|||
let data = fix.and_then(|fix| {
|
||||
fix.applies(Applicability::Unsafe)
|
||||
.then(|| {
|
||||
serde_json::to_value(&DiagnosticFix {
|
||||
serde_json::to_value(&AssociatedDiagnosticData {
|
||||
kind: kind.clone(),
|
||||
fix,
|
||||
code: rule.noqa_code().to_string(),
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
|
|
|
@ -72,10 +72,10 @@ impl Server {
|
|||
|
||||
Ok(Self {
|
||||
conn,
|
||||
client_capabilities,
|
||||
threads,
|
||||
worker_threads,
|
||||
session: Session::new(&server_capabilities, &workspaces)?,
|
||||
session: Session::new(&client_capabilities, &server_capabilities, &workspaces)?,
|
||||
client_capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -192,14 +192,15 @@ impl Server {
|
|||
position_encoding: Some(position_encoding.into()),
|
||||
code_action_provider: Some(types::CodeActionProviderCapability::Options(
|
||||
CodeActionOptions {
|
||||
code_action_kinds: Some(vec![
|
||||
CodeActionKind::QUICKFIX,
|
||||
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
||||
]),
|
||||
code_action_kinds: Some(
|
||||
SupportedCodeAction::all()
|
||||
.flat_map(|action| action.kinds().into_iter())
|
||||
.collect(),
|
||||
),
|
||||
work_done_progress_options: WorkDoneProgressOptions {
|
||||
work_done_progress: Some(true),
|
||||
},
|
||||
resolve_provider: Some(false),
|
||||
resolve_provider: Some(true),
|
||||
},
|
||||
)),
|
||||
workspace: Some(types::WorkspaceServerCapabilities {
|
||||
|
@ -235,3 +236,56 @@ impl Server {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The code actions we support.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum SupportedCodeAction {
|
||||
/// Maps to the `quickfix` code action kind. Quick fix code actions are shown under
|
||||
/// their respective diagnostics. Quick fixes are only created where the fix applicability is
|
||||
/// at least [`ruff_diagnostics::Applicability::Unsafe`].
|
||||
QuickFix,
|
||||
/// Maps to the `source.fixAll` and `source.fixAll.ruff` code action kinds.
|
||||
/// This is a source action that applies all safe fixes to the currently open document.
|
||||
SourceFixAll,
|
||||
/// Maps to `source.organizeImports` and `source.organizeImports.ruff` code action kinds.
|
||||
/// This is a source action that applies import sorting fixes to the currently open document.
|
||||
#[allow(dead_code)] // TODO: remove
|
||||
SourceOrganizeImports,
|
||||
}
|
||||
|
||||
impl SupportedCodeAction {
|
||||
/// Returns the possible LSP code action kind(s) that map to this code action.
|
||||
fn kinds(self) -> Vec<CodeActionKind> {
|
||||
match self {
|
||||
Self::QuickFix => vec![CodeActionKind::QUICKFIX],
|
||||
Self::SourceFixAll => vec![CodeActionKind::SOURCE_FIX_ALL, crate::SOURCE_FIX_ALL_RUFF],
|
||||
Self::SourceOrganizeImports => vec![
|
||||
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
||||
crate::SOURCE_ORGANIZE_IMPORTS_RUFF,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all code actions kinds that the server currently supports.
|
||||
fn all() -> impl Iterator<Item = Self> {
|
||||
[
|
||||
Self::QuickFix,
|
||||
Self::SourceFixAll,
|
||||
// Self::SourceOrganizeImports,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CodeActionKind> for SupportedCodeAction {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(kind: CodeActionKind) -> std::result::Result<Self, Self::Error> {
|
||||
for supported_kind in Self::all() {
|
||||
if supported_kind.kinds().contains(&kind) {
|
||||
return Ok(supported_kind);
|
||||
}
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ use super::{client::Responder, schedule::BackgroundSchedule, Result};
|
|||
/// given the parameter type used by the implementer.
|
||||
macro_rules! define_document_url {
|
||||
($params:ident: &$p:ty) => {
|
||||
fn document_url($params: &$p) -> &lsp_types::Url {
|
||||
&$params.text_document.uri
|
||||
fn document_url($params: &$p) -> std::borrow::Cow<lsp_types::Url> {
|
||||
std::borrow::Cow::Borrowed(&$params.text_document.uri)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -28,10 +28,13 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
|
|||
let id = req.id.clone();
|
||||
|
||||
match req.method.as_str() {
|
||||
request::CodeAction::METHOD => background_request_task::<request::CodeAction>(
|
||||
request::CodeActions::METHOD => background_request_task::<request::CodeActions>(
|
||||
req,
|
||||
BackgroundSchedule::LatencySensitive,
|
||||
),
|
||||
request::CodeActionResolve::METHOD => {
|
||||
background_request_task::<request::CodeActionResolve>(req, BackgroundSchedule::Worker)
|
||||
}
|
||||
request::DocumentDiagnostic::METHOD => {
|
||||
background_request_task::<request::DocumentDiagnostic>(
|
||||
req,
|
||||
|
@ -102,7 +105,7 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
|
|||
let (id, params) = cast_request::<R>(req)?;
|
||||
Ok(Task::background(schedule, move |session: &Session| {
|
||||
// TODO(jane): we should log an error if we can't take a snapshot.
|
||||
let Some(snapshot) = session.take_snapshot(R::document_url(¶ms)) else {
|
||||
let Some(snapshot) = session.take_snapshot(&R::document_url(¶ms)) else {
|
||||
return Box::new(|_, _| {});
|
||||
};
|
||||
Box::new(move |notifier, responder| {
|
||||
|
@ -131,7 +134,7 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH
|
|||
let (id, params) = cast_notification::<N>(req)?;
|
||||
Ok(Task::background(schedule, move |session: &Session| {
|
||||
// TODO(jane): we should log an error if we can't take a snapshot.
|
||||
let Some(snapshot) = session.take_snapshot(N::document_url(¶ms)) else {
|
||||
let Some(snapshot) = session.take_snapshot(&N::document_url(¶ms)) else {
|
||||
return Box::new(|_, _| {});
|
||||
};
|
||||
Box::new(move |notifier, _| {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod code_action;
|
||||
mod code_action_resolve;
|
||||
mod diagnostic;
|
||||
mod format;
|
||||
mod format_range;
|
||||
|
@ -7,7 +8,8 @@ use super::{
|
|||
define_document_url,
|
||||
traits::{BackgroundDocumentRequestHandler, RequestHandler},
|
||||
};
|
||||
pub(super) use code_action::CodeAction;
|
||||
pub(super) use code_action::CodeActions;
|
||||
pub(super) use code_action_resolve::CodeActionResolve;
|
||||
pub(super) use diagnostic::DocumentDiagnostic;
|
||||
pub(super) use format::Format;
|
||||
pub(super) use format_range::FormatRange;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::SupportedCodeAction;
|
||||
use crate::server::{client::Notifier, Result};
|
||||
use crate::session::DocumentSnapshot;
|
||||
use crate::PositionEncoding;
|
||||
use lsp_server::ErrorCode;
|
||||
use lsp_types::{self as types, request as req};
|
||||
use ruff_linter::settings::LinterSettings;
|
||||
|
||||
pub(crate) struct CodeActionResolve;
|
||||
|
||||
impl super::RequestHandler for CodeActionResolve {
|
||||
type RequestType = req::CodeActionResolveRequest;
|
||||
}
|
||||
|
||||
impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
|
||||
fn document_url(params: &types::CodeAction) -> Cow<types::Url> {
|
||||
let uri: lsp_types::Url = serde_json::from_value(params.data.clone().unwrap_or_default())
|
||||
.expect("code actions should have a URI in their data fields");
|
||||
std::borrow::Cow::Owned(uri)
|
||||
}
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
_notifier: Notifier,
|
||||
mut action: types::CodeAction,
|
||||
) -> Result<types::CodeAction> {
|
||||
let document = snapshot.document();
|
||||
|
||||
let action_kind: SupportedCodeAction = action
|
||||
.kind
|
||||
.clone()
|
||||
.ok_or(anyhow::anyhow!("No kind was given for code action"))
|
||||
.with_failure_code(ErrorCode::InvalidParams)?
|
||||
.try_into()
|
||||
.map_err(|()| anyhow::anyhow!("Code action was of an invalid kind"))
|
||||
.with_failure_code(ErrorCode::InvalidParams)?;
|
||||
|
||||
action.edit = match action_kind {
|
||||
SupportedCodeAction::SourceFixAll => Some(
|
||||
resolve_edit_for_fix_all(
|
||||
document,
|
||||
snapshot.url(),
|
||||
&snapshot.configuration().linter,
|
||||
snapshot.encoding(),
|
||||
)
|
||||
.with_failure_code(ErrorCode::InternalError)?,
|
||||
),
|
||||
SupportedCodeAction::SourceOrganizeImports => {
|
||||
todo!("Support `source.organizeImports`")
|
||||
}
|
||||
SupportedCodeAction::QuickFix => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Got a code action that should not need additional resolution: {action_kind:?}"
|
||||
))
|
||||
.with_failure_code(ErrorCode::InvalidParams)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_edit_for_fix_all(
|
||||
document: &crate::edit::Document,
|
||||
url: &types::Url,
|
||||
linter_settings: &LinterSettings,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<types::WorkspaceEdit> {
|
||||
Ok(types::WorkspaceEdit {
|
||||
changes: Some(
|
||||
[(
|
||||
url.clone(),
|
||||
crate::fix::fix_all(document, linter_settings, encoding)?,
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
use crate::edit::ToRangeExt;
|
||||
use crate::edit::{Replacement, ToRangeExt};
|
||||
use crate::server::api::LSPResult;
|
||||
use crate::server::{client::Notifier, Result};
|
||||
use crate::session::DocumentSnapshot;
|
||||
use lsp_types::{self as types, request as req};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use types::TextEdit;
|
||||
|
||||
pub(crate) struct Format;
|
||||
|
@ -33,8 +32,8 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
|||
let unformatted_index = doc.index();
|
||||
|
||||
let Replacement {
|
||||
source_range: replace_range,
|
||||
formatted_range: replacement_text_range,
|
||||
source_range,
|
||||
modified_range: formatted_range,
|
||||
} = Replacement::between(
|
||||
source,
|
||||
unformatted_index.line_starts(),
|
||||
|
@ -43,105 +42,8 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
|||
);
|
||||
|
||||
Ok(Some(vec![TextEdit {
|
||||
range: replace_range.to_range(source, unformatted_index, snapshot.encoding()),
|
||||
new_text: formatted[replacement_text_range].to_owned(),
|
||||
range: source_range.to_range(source, unformatted_index, snapshot.encoding()),
|
||||
new_text: formatted[formatted_range].to_owned(),
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
struct Replacement {
|
||||
source_range: TextRange,
|
||||
formatted_range: TextRange,
|
||||
}
|
||||
|
||||
impl Replacement {
|
||||
/// Creates a [`Replacement`] that describes the `replace_range` of `old_text` to replace
|
||||
/// with `new_text` sliced by `replacement_text_range`.
|
||||
fn between(
|
||||
source: &str,
|
||||
source_line_starts: &[TextSize],
|
||||
formatted: &str,
|
||||
formatted_line_starts: &[TextSize],
|
||||
) -> Self {
|
||||
let mut source_start = TextSize::default();
|
||||
let mut formatted_start = TextSize::default();
|
||||
let mut source_end = source.text_len();
|
||||
let mut formatted_end = formatted.text_len();
|
||||
let mut line_iter = source_line_starts
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(formatted_line_starts.iter().copied());
|
||||
for (source_line_start, formatted_line_start) in line_iter.by_ref() {
|
||||
if source_line_start != formatted_line_start
|
||||
|| source[TextRange::new(source_start, source_line_start)]
|
||||
!= formatted[TextRange::new(formatted_start, formatted_line_start)]
|
||||
{
|
||||
break;
|
||||
}
|
||||
source_start = source_line_start;
|
||||
formatted_start = formatted_line_start;
|
||||
}
|
||||
|
||||
let mut line_iter = line_iter.rev();
|
||||
|
||||
for (old_line_start, new_line_start) in line_iter.by_ref() {
|
||||
if old_line_start <= source_start
|
||||
|| new_line_start <= formatted_start
|
||||
|| source[TextRange::new(old_line_start, source_end)]
|
||||
!= formatted[TextRange::new(new_line_start, formatted_end)]
|
||||
{
|
||||
break;
|
||||
}
|
||||
source_end = old_line_start;
|
||||
formatted_end = new_line_start;
|
||||
}
|
||||
|
||||
Replacement {
|
||||
source_range: TextRange::new(source_start, source_end),
|
||||
formatted_range: TextRange::new(formatted_start, formatted_end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_source_file::LineIndex;
|
||||
|
||||
use crate::server::api::requests::format::Replacement;
|
||||
|
||||
#[test]
|
||||
fn find_replacement_range_works() {
|
||||
let original = r#"
|
||||
aaaa
|
||||
bbbb
|
||||
cccc
|
||||
dddd
|
||||
eeee
|
||||
"#;
|
||||
let original_index = LineIndex::from_source_text(original);
|
||||
let new = r#"
|
||||
bb
|
||||
cccc
|
||||
dd
|
||||
"#;
|
||||
let new_index = LineIndex::from_source_text(new);
|
||||
let expected = r#"
|
||||
bb
|
||||
cccc
|
||||
dd
|
||||
"#;
|
||||
let replacement = Replacement::between(
|
||||
original,
|
||||
original_index.line_starts(),
|
||||
new,
|
||||
new_index.line_starts(),
|
||||
);
|
||||
let mut test = original.to_string();
|
||||
test.replace_range(
|
||||
replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(),
|
||||
&new[replacement.formatted_range],
|
||||
);
|
||||
|
||||
assert_eq!(expected, &test);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
|
|||
/// implementation.
|
||||
fn document_url(
|
||||
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> &lsp_types::Url;
|
||||
) -> std::borrow::Cow<lsp_types::Url>;
|
||||
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
|
@ -66,7 +66,7 @@ pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler {
|
|||
/// implementation.
|
||||
fn document_url(
|
||||
params: &<<Self as NotificationHandler>::NotificationType as LSPNotification>::Params,
|
||||
) -> &lsp_types::Url;
|
||||
) -> std::borrow::Cow<lsp_types::Url>;
|
||||
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
//! Data model, state management, and configuration resolution.
|
||||
|
||||
mod types;
|
||||
mod settings;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use lsp_types::{ServerCapabilities, Url};
|
||||
use lsp_types::{ClientCapabilities, ServerCapabilities, Url};
|
||||
use ruff_workspace::resolver::{ConfigurationTransformer, Relativity};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::edit::{Document, DocumentVersion};
|
||||
use crate::PositionEncoding;
|
||||
|
||||
use self::settings::ResolvedClientCapabilities;
|
||||
|
||||
/// The global state for the LSP
|
||||
pub(crate) struct Session {
|
||||
/// Workspace folders in the current session, which contain the state of all open files.
|
||||
workspaces: Workspaces,
|
||||
/// The global position encoding, negotiated during LSP initialization.
|
||||
position_encoding: PositionEncoding,
|
||||
/// Extension-specific settings, set by the client, that apply to all workspace folders.
|
||||
#[allow(dead_code)]
|
||||
lsp_settings: types::ExtensionSettings,
|
||||
/// Tracks what LSP features the client supports and doesn't support.
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
}
|
||||
|
||||
/// An immutable snapshot of `Session` that references
|
||||
/// a specific document.
|
||||
pub(crate) struct DocumentSnapshot {
|
||||
configuration: Arc<RuffConfiguration>,
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
document_ref: DocumentRef,
|
||||
position_encoding: PositionEncoding,
|
||||
url: Url,
|
||||
|
@ -70,6 +72,7 @@ pub(crate) struct DocumentRef {
|
|||
|
||||
impl Session {
|
||||
pub(crate) fn new(
|
||||
client_capabilities: &ClientCapabilities,
|
||||
server_capabilities: &ServerCapabilities,
|
||||
workspaces: &[Url],
|
||||
) -> crate::Result<Self> {
|
||||
|
@ -79,7 +82,9 @@ impl Session {
|
|||
.as_ref()
|
||||
.and_then(|encoding| encoding.try_into().ok())
|
||||
.unwrap_or_default(),
|
||||
lsp_settings: types::ExtensionSettings,
|
||||
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||
client_capabilities,
|
||||
)),
|
||||
workspaces: Workspaces::new(workspaces)?,
|
||||
})
|
||||
}
|
||||
|
@ -87,6 +92,7 @@ impl Session {
|
|||
pub(crate) fn take_snapshot(&self, url: &Url) -> Option<DocumentSnapshot> {
|
||||
Some(DocumentSnapshot {
|
||||
configuration: self.workspaces.configuration(url)?.clone(),
|
||||
resolved_client_capabilities: self.resolved_client_capabilities.clone(),
|
||||
document_ref: self.workspaces.snapshot(url)?,
|
||||
position_encoding: self.position_encoding,
|
||||
url: url.clone(),
|
||||
|
@ -196,6 +202,10 @@ impl DocumentSnapshot {
|
|||
&self.configuration
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
|
||||
&self.resolved_client_capabilities
|
||||
}
|
||||
|
||||
pub(crate) fn document(&self) -> &DocumentRef {
|
||||
&self.document_ref
|
||||
}
|
||||
|
|
25
crates/ruff_server/src/session/settings.rs
Normal file
25
crates/ruff_server/src/session/settings.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use lsp_types::ClientCapabilities;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct ResolvedClientCapabilities {
|
||||
pub(crate) code_action_deferred_edit_resolution: bool,
|
||||
}
|
||||
|
||||
impl ResolvedClientCapabilities {
|
||||
pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
|
||||
let code_action_settings = client_capabilities
|
||||
.text_document
|
||||
.as_ref()
|
||||
.and_then(|doc_settings| doc_settings.code_action.as_ref());
|
||||
let code_action_data_support = code_action_settings
|
||||
.and_then(|code_action_settings| code_action_settings.data_support)
|
||||
.unwrap_or_default();
|
||||
let code_action_edit_resolution = code_action_settings
|
||||
.and_then(|code_action_settings| code_action_settings.resolve_support.as_ref())
|
||||
.is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into()));
|
||||
Self {
|
||||
code_action_deferred_edit_resolution: code_action_data_support
|
||||
&& code_action_edit_resolution,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#[allow(dead_code)] // TODO(jane): get this wired up after the pre-release
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct ExtensionSettings;
|
Loading…
Add table
Add a link
Reference in a new issue