mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 14:21:24 +00:00
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:
parent
1b31d4e9f1
commit
c11e6d709c
13 changed files with 379 additions and 96 deletions
|
@ -4,12 +4,16 @@ mod document;
|
||||||
mod range;
|
mod range;
|
||||||
mod replacement;
|
mod replacement;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub use document::Document;
|
pub use document::Document;
|
||||||
pub(crate) use document::DocumentVersion;
|
pub(crate) use document::DocumentVersion;
|
||||||
use lsp_types::PositionEncodingKind;
|
use lsp_types::PositionEncodingKind;
|
||||||
pub(crate) use range::{RangeExt, ToRangeExt};
|
pub(crate) use range::{RangeExt, ToRangeExt};
|
||||||
pub(crate) use replacement::Replacement;
|
pub(crate) use replacement::Replacement;
|
||||||
|
|
||||||
|
use crate::session::ResolvedClientCapabilities;
|
||||||
|
|
||||||
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
|
/// 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.
|
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
|
||||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
@ -25,6 +29,14 @@ pub enum PositionEncoding {
|
||||||
UTF8,
|
UTF8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tracks multi-document edits to eventually merge into a `WorkspaceEdit`.
|
||||||
|
/// Compatible with clients that don't support `workspace.workspaceEdit.documentChanges`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum WorkspaceEditTracker {
|
||||||
|
DocumentChanges(Vec<lsp_types::TextDocumentEdit>),
|
||||||
|
Changes(HashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>),
|
||||||
|
}
|
||||||
|
|
||||||
impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
|
impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
|
||||||
fn from(value: PositionEncoding) -> Self {
|
fn from(value: PositionEncoding) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
@ -50,3 +62,70 @@ impl TryFrom<&lsp_types::PositionEncodingKind> for PositionEncoding {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WorkspaceEditTracker {
|
||||||
|
pub(crate) fn new(client_capabilities: &ResolvedClientCapabilities) -> Self {
|
||||||
|
if client_capabilities.document_changes {
|
||||||
|
Self::DocumentChanges(Vec::default())
|
||||||
|
} else {
|
||||||
|
Self::Changes(HashMap::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the edits made to a specific document. This should only be called
|
||||||
|
/// once for each document `uri`, and will fail if this is called for the same `uri`
|
||||||
|
/// multiple times.
|
||||||
|
pub(crate) fn set_edits_for_document(
|
||||||
|
&mut self,
|
||||||
|
uri: lsp_types::Url,
|
||||||
|
version: DocumentVersion,
|
||||||
|
edits: Vec<lsp_types::TextEdit>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::DocumentChanges(document_edits) => {
|
||||||
|
if document_edits
|
||||||
|
.iter()
|
||||||
|
.any(|document| document.text_document.uri == uri)
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Attempted to add edits for a document that was already edited"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
document_edits.push(lsp_types::TextDocumentEdit {
|
||||||
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
||||||
|
uri,
|
||||||
|
version: Some(version),
|
||||||
|
},
|
||||||
|
edits: edits.into_iter().map(lsp_types::OneOf::Left).collect(),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Self::Changes(changes) => {
|
||||||
|
if changes.get(&uri).is_some() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Attempted to add edits for a document that was already edited"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
changes.insert(uri, edits);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::DocumentChanges(document_edits) => document_edits.is_empty(),
|
||||||
|
Self::Changes(changes) => changes.is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_workspace_edit(self) -> lsp_types::WorkspaceEdit {
|
||||||
|
match self {
|
||||||
|
Self::DocumentChanges(document_edits) => lsp_types::WorkspaceEdit {
|
||||||
|
document_changes: Some(lsp_types::DocumentChanges::Edits(document_edits)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Self::Changes(changes) => lsp_types::WorkspaceEdit::new(changes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub(crate) struct DiagnosticFix {
|
||||||
pub(crate) fixed_diagnostic: lsp_types::Diagnostic,
|
pub(crate) fixed_diagnostic: lsp_types::Diagnostic,
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
pub(crate) code: String,
|
pub(crate) code: String,
|
||||||
pub(crate) document_edits: Vec<lsp_types::TextDocumentEdit>,
|
pub(crate) edits: Vec<lsp_types::TextEdit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check(
|
pub(crate) fn check(
|
||||||
|
@ -90,11 +90,9 @@ pub(crate) fn check(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fixes_for_diagnostics<'d>(
|
pub(crate) fn fixes_for_diagnostics(
|
||||||
document: &'d crate::edit::Document,
|
document: &crate::edit::Document,
|
||||||
url: &'d lsp_types::Url,
|
|
||||||
encoding: PositionEncoding,
|
encoding: PositionEncoding,
|
||||||
version: crate::edit::DocumentVersion,
|
|
||||||
diagnostics: Vec<lsp_types::Diagnostic>,
|
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||||
) -> crate::Result<Vec<DiagnosticFix>> {
|
) -> crate::Result<Vec<DiagnosticFix>> {
|
||||||
diagnostics
|
diagnostics
|
||||||
|
@ -118,14 +116,6 @@ pub(crate) fn fixes_for_diagnostics<'d>(
|
||||||
.to_range(document.contents(), document.index(), encoding),
|
.to_range(document.contents(), document.index(), encoding),
|
||||||
new_text: edit.content().unwrap_or_default().to_string(),
|
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 {
|
Ok(Some(DiagnosticFix {
|
||||||
fixed_diagnostic,
|
fixed_diagnostic,
|
||||||
code: associated_data.code,
|
code: associated_data.code,
|
||||||
|
@ -133,7 +123,7 @@ pub(crate) fn fixes_for_diagnostics<'d>(
|
||||||
.kind
|
.kind
|
||||||
.suggestion
|
.suggestion
|
||||||
.unwrap_or(associated_data.kind.name),
|
.unwrap_or(associated_data.kind.name),
|
||||||
document_edits,
|
edits: edits.collect(),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.filter_map(crate::Result::transpose)
|
.filter_map(crate::Result::transpose)
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
|
||||||
BackgroundSchedule::LatencySensitive,
|
BackgroundSchedule::LatencySensitive,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
request::ExecuteCommand::METHOD => local_request_task::<request::ExecuteCommand>(req),
|
||||||
request::Format::METHOD => {
|
request::Format::METHOD => {
|
||||||
background_request_task::<request::Format>(req, BackgroundSchedule::Fmt)
|
background_request_task::<request::Format>(req, BackgroundSchedule::Fmt)
|
||||||
}
|
}
|
||||||
|
@ -87,13 +88,12 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn local_request_task<'a, R: traits::SyncRequestHandler>(
|
fn local_request_task<'a, R: traits::SyncRequestHandler>(
|
||||||
req: server::Request,
|
req: server::Request,
|
||||||
) -> super::Result<Task<'a>> {
|
) -> super::Result<Task<'a>> {
|
||||||
let (id, params) = cast_request::<R>(req)?;
|
let (id, params) = cast_request::<R>(req)?;
|
||||||
Ok(Task::local(|session, notifier, responder| {
|
Ok(Task::local(|session, notifier, requester, responder| {
|
||||||
let result = R::run(session, notifier, params);
|
let result = R::run(session, notifier, requester, params);
|
||||||
respond::<R>(id, result, &responder);
|
respond::<R>(id, result, &responder);
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>(
|
||||||
notif: server::Notification,
|
notif: server::Notification,
|
||||||
) -> super::Result<Task<'a>> {
|
) -> super::Result<Task<'a>> {
|
||||||
let (id, params) = cast_notification::<N>(notif)?;
|
let (id, params) = cast_notification::<N>(notif)?;
|
||||||
Ok(Task::local(move |session, notifier, _| {
|
Ok(Task::local(move |session, notifier, _, _| {
|
||||||
if let Err(err) = N::run(session, notifier, params) {
|
if let Err(err) = N::run(session, notifier, params) {
|
||||||
tracing::error!("An error occurred while running {id}: {err}");
|
tracing::error!("An error occurred while running {id}: {err}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
mod code_action;
|
mod code_action;
|
||||||
mod code_action_resolve;
|
mod code_action_resolve;
|
||||||
mod diagnostic;
|
mod diagnostic;
|
||||||
|
mod execute_command;
|
||||||
mod format;
|
mod format;
|
||||||
mod format_range;
|
mod format_range;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
define_document_url,
|
define_document_url,
|
||||||
traits::{BackgroundDocumentRequestHandler, RequestHandler},
|
traits::{BackgroundDocumentRequestHandler, RequestHandler, SyncRequestHandler},
|
||||||
};
|
};
|
||||||
pub(super) use code_action::CodeActions;
|
pub(super) use code_action::CodeActions;
|
||||||
pub(super) use code_action_resolve::CodeActionResolve;
|
pub(super) use code_action_resolve::CodeActionResolve;
|
||||||
pub(super) use diagnostic::DocumentDiagnostic;
|
pub(super) use diagnostic::DocumentDiagnostic;
|
||||||
|
pub(super) use execute_command::ExecuteCommand;
|
||||||
pub(super) use format::Format;
|
pub(super) use format::Format;
|
||||||
pub(super) use format_range::FormatRange;
|
pub(super) use format_range::FormatRange;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::edit::WorkspaceEditTracker;
|
||||||
use crate::lint::fixes_for_diagnostics;
|
use crate::lint::fixes_for_diagnostics;
|
||||||
use crate::server::api::LSPResult;
|
use crate::server::api::LSPResult;
|
||||||
use crate::server::SupportedCodeAction;
|
use crate::server::SupportedCodeAction;
|
||||||
|
@ -50,30 +51,34 @@ impl super::BackgroundDocumentRequestHandler for CodeActions {
|
||||||
fn quick_fix(
|
fn quick_fix(
|
||||||
snapshot: &DocumentSnapshot,
|
snapshot: &DocumentSnapshot,
|
||||||
diagnostics: Vec<types::Diagnostic>,
|
diagnostics: Vec<types::Diagnostic>,
|
||||||
) -> crate::Result<impl Iterator<Item = CodeActionOrCommand> + '_> {
|
) -> crate::Result<Vec<CodeActionOrCommand>> {
|
||||||
let document = snapshot.document();
|
let document = snapshot.document();
|
||||||
|
|
||||||
let fixes = fixes_for_diagnostics(
|
let fixes = fixes_for_diagnostics(document, snapshot.encoding(), diagnostics)?;
|
||||||
document,
|
|
||||||
snapshot.url(),
|
fixes
|
||||||
snapshot.encoding(),
|
.into_iter()
|
||||||
|
.map(|fix| {
|
||||||
|
let mut tracker = WorkspaceEditTracker::new(snapshot.resolved_client_capabilities());
|
||||||
|
|
||||||
|
tracker.set_edits_for_document(
|
||||||
|
snapshot.url().clone(),
|
||||||
document.version(),
|
document.version(),
|
||||||
diagnostics,
|
fix.edits,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(fixes.into_iter().map(|fix| {
|
Ok(types::CodeActionOrCommand::CodeAction(types::CodeAction {
|
||||||
types::CodeActionOrCommand::CodeAction(types::CodeAction {
|
|
||||||
title: format!("{DIAGNOSTIC_NAME} ({}): {}", fix.code, fix.title),
|
title: format!("{DIAGNOSTIC_NAME} ({}): {}", fix.code, fix.title),
|
||||||
kind: Some(types::CodeActionKind::QUICKFIX),
|
kind: Some(types::CodeActionKind::QUICKFIX),
|
||||||
edit: Some(types::WorkspaceEdit {
|
edit: Some(tracker.into_workspace_edit()),
|
||||||
document_changes: Some(types::DocumentChanges::Edits(fix.document_edits.clone())),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
diagnostics: Some(vec![fix.fixed_diagnostic.clone()]),
|
diagnostics: Some(vec![fix.fixed_diagnostic.clone()]),
|
||||||
data: Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")),
|
data: Some(
|
||||||
|
serde_json::to_value(snapshot.url()).expect("document url to serialize"),
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
||||||
|
@ -92,9 +97,11 @@ fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
||||||
(
|
(
|
||||||
Some(resolve_edit_for_fix_all(
|
Some(resolve_edit_for_fix_all(
|
||||||
document,
|
document,
|
||||||
|
snapshot.resolved_client_capabilities(),
|
||||||
snapshot.url(),
|
snapshot.url(),
|
||||||
&snapshot.configuration().linter,
|
&snapshot.configuration().linter,
|
||||||
snapshot.encoding(),
|
snapshot.encoding(),
|
||||||
|
document.version(),
|
||||||
)?),
|
)?),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
@ -125,9 +132,11 @@ fn organize_imports(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCo
|
||||||
(
|
(
|
||||||
Some(resolve_edit_for_organize_imports(
|
Some(resolve_edit_for_organize_imports(
|
||||||
document,
|
document,
|
||||||
|
snapshot.resolved_client_capabilities(),
|
||||||
snapshot.url(),
|
snapshot.url(),
|
||||||
&snapshot.configuration().linter,
|
&snapshot.configuration().linter,
|
||||||
snapshot.encoding(),
|
snapshot.encoding(),
|
||||||
|
document.version(),
|
||||||
)?),
|
)?),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::edit::{DocumentVersion, WorkspaceEditTracker};
|
||||||
use crate::server::api::LSPResult;
|
use crate::server::api::LSPResult;
|
||||||
use crate::server::SupportedCodeAction;
|
use crate::server::SupportedCodeAction;
|
||||||
use crate::server::{client::Notifier, Result};
|
use crate::server::{client::Notifier, Result};
|
||||||
use crate::session::DocumentSnapshot;
|
use crate::session::{DocumentSnapshot, ResolvedClientCapabilities};
|
||||||
use crate::PositionEncoding;
|
use crate::PositionEncoding;
|
||||||
use lsp_server::ErrorCode;
|
use lsp_server::ErrorCode;
|
||||||
use lsp_types::{self as types, request as req};
|
use lsp_types::{self as types, request as req};
|
||||||
|
@ -42,18 +43,22 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
|
||||||
SupportedCodeAction::SourceFixAll => Some(
|
SupportedCodeAction::SourceFixAll => Some(
|
||||||
resolve_edit_for_fix_all(
|
resolve_edit_for_fix_all(
|
||||||
document,
|
document,
|
||||||
|
snapshot.resolved_client_capabilities(),
|
||||||
snapshot.url(),
|
snapshot.url(),
|
||||||
&snapshot.configuration().linter,
|
&snapshot.configuration().linter,
|
||||||
snapshot.encoding(),
|
snapshot.encoding(),
|
||||||
|
document.version(),
|
||||||
)
|
)
|
||||||
.with_failure_code(ErrorCode::InternalError)?,
|
.with_failure_code(ErrorCode::InternalError)?,
|
||||||
),
|
),
|
||||||
SupportedCodeAction::SourceOrganizeImports => Some(
|
SupportedCodeAction::SourceOrganizeImports => Some(
|
||||||
resolve_edit_for_organize_imports(
|
resolve_edit_for_organize_imports(
|
||||||
document,
|
document,
|
||||||
|
snapshot.resolved_client_capabilities(),
|
||||||
snapshot.url(),
|
snapshot.url(),
|
||||||
&snapshot.configuration().linter,
|
&snapshot.configuration().linter,
|
||||||
snapshot.encoding(),
|
snapshot.encoding(),
|
||||||
|
document.version(),
|
||||||
)
|
)
|
||||||
.with_failure_code(ErrorCode::InternalError)?,
|
.with_failure_code(ErrorCode::InternalError)?,
|
||||||
),
|
),
|
||||||
|
@ -71,29 +76,51 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
|
||||||
|
|
||||||
pub(super) fn resolve_edit_for_fix_all(
|
pub(super) fn resolve_edit_for_fix_all(
|
||||||
document: &crate::edit::Document,
|
document: &crate::edit::Document,
|
||||||
|
client_capabilities: &ResolvedClientCapabilities,
|
||||||
url: &types::Url,
|
url: &types::Url,
|
||||||
linter_settings: &LinterSettings,
|
linter_settings: &LinterSettings,
|
||||||
encoding: PositionEncoding,
|
encoding: PositionEncoding,
|
||||||
|
version: DocumentVersion,
|
||||||
) -> crate::Result<types::WorkspaceEdit> {
|
) -> crate::Result<types::WorkspaceEdit> {
|
||||||
Ok(types::WorkspaceEdit {
|
let mut tracker = WorkspaceEditTracker::new(client_capabilities);
|
||||||
changes: Some(
|
tracker.set_edits_for_document(
|
||||||
[(
|
|
||||||
url.clone(),
|
url.clone(),
|
||||||
crate::fix::fix_all(document, linter_settings, encoding)?,
|
version,
|
||||||
)]
|
fix_all_edit(document, linter_settings, encoding)?,
|
||||||
.into_iter()
|
)?;
|
||||||
.collect(),
|
Ok(tracker.into_workspace_edit())
|
||||||
),
|
}
|
||||||
..Default::default()
|
|
||||||
})
|
pub(super) fn fix_all_edit(
|
||||||
|
document: &crate::edit::Document,
|
||||||
|
linter_settings: &LinterSettings,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> crate::Result<Vec<types::TextEdit>> {
|
||||||
|
crate::fix::fix_all(document, linter_settings, encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn resolve_edit_for_organize_imports(
|
pub(super) fn resolve_edit_for_organize_imports(
|
||||||
document: &crate::edit::Document,
|
document: &crate::edit::Document,
|
||||||
|
client_capabilities: &ResolvedClientCapabilities,
|
||||||
url: &types::Url,
|
url: &types::Url,
|
||||||
linter_settings: &ruff_linter::settings::LinterSettings,
|
linter_settings: &ruff_linter::settings::LinterSettings,
|
||||||
encoding: PositionEncoding,
|
encoding: PositionEncoding,
|
||||||
|
version: DocumentVersion,
|
||||||
) -> crate::Result<types::WorkspaceEdit> {
|
) -> crate::Result<types::WorkspaceEdit> {
|
||||||
|
let mut tracker = WorkspaceEditTracker::new(client_capabilities);
|
||||||
|
tracker.set_edits_for_document(
|
||||||
|
url.clone(),
|
||||||
|
version,
|
||||||
|
organize_imports_edit(document, linter_settings, encoding)?,
|
||||||
|
)?;
|
||||||
|
Ok(tracker.into_workspace_edit())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn organize_imports_edit(
|
||||||
|
document: &crate::edit::Document,
|
||||||
|
linter_settings: &LinterSettings,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> crate::Result<Vec<types::TextEdit>> {
|
||||||
let mut linter_settings = linter_settings.clone();
|
let mut linter_settings = linter_settings.clone();
|
||||||
linter_settings.rules = [
|
linter_settings.rules = [
|
||||||
Rule::UnsortedImports, // I001
|
Rule::UnsortedImports, // I001
|
||||||
|
@ -102,15 +129,5 @@ pub(super) fn resolve_edit_for_organize_imports(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(types::WorkspaceEdit {
|
crate::fix::fix_all(document, &linter_settings, encoding)
|
||||||
changes: Some(
|
|
||||||
[(
|
|
||||||
url.clone(),
|
|
||||||
crate::fix::fix_all(document, &linter_settings, encoding)?,
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
153
crates/ruff_server/src/server/api/requests/execute_command.rs
Normal file
153
crates/ruff_server/src/server/api/requests/execute_command.rs
Normal 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(¶ms.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()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -19,6 +19,11 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||||
_notifier: Notifier,
|
_notifier: Notifier,
|
||||||
_params: types::DocumentFormattingParams,
|
_params: types::DocumentFormattingParams,
|
||||||
) -> Result<super::FormatResponse> {
|
) -> Result<super::FormatResponse> {
|
||||||
|
format_document(&snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::FormatResponse> {
|
||||||
let doc = snapshot.document();
|
let doc = snapshot.document();
|
||||||
let source = doc.contents();
|
let source = doc.contents();
|
||||||
let formatted = crate::format::format(doc, &snapshot.configuration().formatter)
|
let formatted = crate::format::format(doc, &snapshot.configuration().formatter)
|
||||||
|
@ -45,5 +50,4 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||||
range: source_range.to_range(source, unformatted_index, snapshot.encoding()),
|
range: source_range.to_range(source, unformatted_index, snapshot.encoding()),
|
||||||
new_text: formatted[formatted_range].to_owned(),
|
new_text: formatted[formatted_range].to_owned(),
|
||||||
}]))
|
}]))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! A stateful LSP implementation that calls into the Ruff API.
|
//! A stateful LSP implementation that calls into the Ruff API.
|
||||||
|
|
||||||
use crate::server::client::Notifier;
|
use crate::server::client::{Notifier, Requester};
|
||||||
use crate::session::{DocumentSnapshot, Session};
|
use crate::session::{DocumentSnapshot, Session};
|
||||||
|
|
||||||
use lsp_types::notification::Notification as LSPNotification;
|
use lsp_types::notification::Notification as LSPNotification;
|
||||||
|
@ -20,6 +20,7 @@ pub(super) trait SyncRequestHandler: RequestHandler {
|
||||||
fn run(
|
fn run(
|
||||||
session: &mut Session,
|
session: &mut Session,
|
||||||
notifier: Notifier,
|
notifier: Notifier,
|
||||||
|
requester: &mut Requester,
|
||||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||||
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,10 +80,13 @@ impl<'s> Scheduler<'s> {
|
||||||
pub(super) fn dispatch(&mut self, task: task::Task<'s>) {
|
pub(super) fn dispatch(&mut self, task: task::Task<'s>) {
|
||||||
match task {
|
match task {
|
||||||
Task::Sync(SyncTask { func }) => {
|
Task::Sync(SyncTask { func }) => {
|
||||||
|
let notifier = self.client.notifier();
|
||||||
|
let responder = self.client.responder();
|
||||||
func(
|
func(
|
||||||
self.session,
|
self.session,
|
||||||
self.client.notifier(),
|
notifier,
|
||||||
self.client.responder(),
|
&mut self.client.requester,
|
||||||
|
responder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Task::Background(BackgroundTaskBuilder {
|
Task::Background(BackgroundTaskBuilder {
|
||||||
|
|
|
@ -2,11 +2,11 @@ use lsp_server::RequestId;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
server::client::{Notifier, Responder},
|
server::client::{Notifier, Requester, Responder},
|
||||||
session::Session,
|
session::Session,
|
||||||
};
|
};
|
||||||
|
|
||||||
type LocalFn<'s> = Box<dyn FnOnce(&mut Session, Notifier, Responder) + 's>;
|
type LocalFn<'s> = Box<dyn FnOnce(&mut Session, Notifier, &mut Requester, Responder) + 's>;
|
||||||
|
|
||||||
type BackgroundFn = Box<dyn FnOnce(Notifier, Responder) + Send + 'static>;
|
type BackgroundFn = Box<dyn FnOnce(Notifier, Responder) + Send + 'static>;
|
||||||
|
|
||||||
|
@ -68,7 +68,9 @@ impl<'s> Task<'s> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/// Creates a new local task.
|
/// Creates a new local task.
|
||||||
pub(crate) fn local(func: impl FnOnce(&mut Session, Notifier, Responder) + 's) -> Self {
|
pub(crate) fn local(
|
||||||
|
func: impl FnOnce(&mut Session, Notifier, &mut Requester, Responder) + 's,
|
||||||
|
) -> Self {
|
||||||
Self::Sync(SyncTask {
|
Self::Sync(SyncTask {
|
||||||
func: Box::new(func),
|
func: Box::new(func),
|
||||||
})
|
})
|
||||||
|
@ -79,14 +81,15 @@ impl<'s> Task<'s> {
|
||||||
where
|
where
|
||||||
R: Serialize + Send + 'static,
|
R: Serialize + Send + 'static,
|
||||||
{
|
{
|
||||||
Self::local(move |_, _, responder| {
|
Self::local(move |_, _, _, responder| {
|
||||||
if let Err(err) = responder.respond(id, result) {
|
if let Err(err) = responder.respond(id, result) {
|
||||||
tracing::error!("Unable to send immediate response: {err}");
|
tracing::error!("Unable to send immediate response: {err}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a local task that does nothing.
|
/// Creates a local task that does nothing.
|
||||||
pub(crate) fn nothing() -> Self {
|
pub(crate) fn nothing() -> Self {
|
||||||
Self::local(move |_, _, _| {})
|
Self::local(move |_, _, _, _| {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use rustc_hash::FxHashMap;
|
||||||
use crate::edit::{Document, DocumentVersion};
|
use crate::edit::{Document, DocumentVersion};
|
||||||
use crate::PositionEncoding;
|
use crate::PositionEncoding;
|
||||||
|
|
||||||
use self::capabilities::ResolvedClientCapabilities;
|
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||||
use self::settings::ResolvedClientSettings;
|
use self::settings::ResolvedClientSettings;
|
||||||
pub(crate) use self::settings::{AllSettings, ClientSettings};
|
pub(crate) use self::settings::{AllSettings, ClientSettings};
|
||||||
|
|
||||||
|
@ -140,6 +140,10 @@ impl Session {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
|
||||||
|
&self.resolved_client_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn encoding(&self) -> PositionEncoding {
|
pub(crate) fn encoding(&self) -> PositionEncoding {
|
||||||
self.position_encoding
|
self.position_encoding
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ use lsp_types::ClientCapabilities;
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub(crate) struct ResolvedClientCapabilities {
|
pub(crate) struct ResolvedClientCapabilities {
|
||||||
pub(crate) code_action_deferred_edit_resolution: bool,
|
pub(crate) code_action_deferred_edit_resolution: bool,
|
||||||
|
pub(crate) apply_edit: bool,
|
||||||
|
pub(crate) document_changes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedClientCapabilities {
|
impl ResolvedClientCapabilities {
|
||||||
|
@ -17,9 +19,25 @@ impl ResolvedClientCapabilities {
|
||||||
let code_action_edit_resolution = code_action_settings
|
let code_action_edit_resolution = code_action_settings
|
||||||
.and_then(|code_action_settings| code_action_settings.resolve_support.as_ref())
|
.and_then(|code_action_settings| code_action_settings.resolve_support.as_ref())
|
||||||
.is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into()));
|
.is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into()));
|
||||||
|
|
||||||
|
let apply_edit = client_capabilities
|
||||||
|
.workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|workspace| workspace.apply_edit)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let document_changes = client_capabilities
|
||||||
|
.workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|workspace| workspace.workspace_edit.as_ref())
|
||||||
|
.and_then(|workspace_edit| workspace_edit.document_changes)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
code_action_deferred_edit_resolution: code_action_data_support
|
code_action_deferred_edit_resolution: code_action_data_support
|
||||||
&& code_action_edit_resolution,
|
&& code_action_edit_resolution,
|
||||||
|
apply_edit,
|
||||||
|
document_changes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue