From 119530e3d3949c4427a5a795af192efed54d0c1d Mon Sep 17 00:00:00 2001 From: Jane Lewis Date: Fri, 5 Apr 2024 16:27:35 -0700 Subject: [PATCH] `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 https://github.com/astral-sh/ruff/assets/19577865/7932e30f-d944-4e35-b828-1d81aa56c087 --- crates/ruff_server/src/edit.rs | 79 +++++++++ crates/ruff_server/src/lint.rs | 18 +-- crates/ruff_server/src/server/api.rs | 8 +- crates/ruff_server/src/server/api/requests.rs | 4 +- .../src/server/api/requests/code_action.rs | 49 +++--- .../api/requests/code_action_resolve.rs | 63 +++++--- .../server/api/requests/execute_command.rs | 153 ++++++++++++++++++ .../src/server/api/requests/format.rs | 52 +++--- crates/ruff_server/src/server/api/traits.rs | 3 +- crates/ruff_server/src/server/schedule.rs | 7 +- .../ruff_server/src/server/schedule/task.rs | 13 +- crates/ruff_server/src/session.rs | 6 +- .../ruff_server/src/session/capabilities.rs | 18 +++ 13 files changed, 378 insertions(+), 95 deletions(-) create mode 100644 crates/ruff_server/src/server/api/requests/execute_command.rs diff --git a/crates/ruff_server/src/edit.rs b/crates/ruff_server/src/edit.rs index 9af598eaebcac8..ec41b22a944793 100644 --- a/crates/ruff_server/src/edit.rs +++ b/crates/ruff_server/src/edit.rs @@ -4,12 +4,16 @@ mod document; mod range; mod replacement; +use std::collections::HashMap; + pub use document::Document; pub(crate) use document::DocumentVersion; use lsp_types::PositionEncodingKind; pub(crate) use range::{RangeExt, ToRangeExt}; pub(crate) use replacement::Replacement; +use crate::session::ResolvedClientCapabilities; + /// 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. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -25,6 +29,14 @@ pub enum PositionEncoding { 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), + Changes(HashMap>), +} + impl From for lsp_types::PositionEncodingKind { fn from(value: PositionEncoding) -> Self { 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, + ) -> 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), + } + } +} diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index ba8bef1e26825c..355460913f1fb9 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -35,7 +35,7 @@ pub(crate) struct DiagnosticFix { pub(crate) fixed_diagnostic: lsp_types::Diagnostic, pub(crate) title: String, pub(crate) code: String, - pub(crate) document_edits: Vec, + pub(crate) edits: Vec, } pub(crate) fn check( @@ -90,11 +90,9 @@ pub(crate) fn check( .collect() } -pub(crate) fn fixes_for_diagnostics<'d>( - document: &'d crate::edit::Document, - url: &'d lsp_types::Url, +pub(crate) fn fixes_for_diagnostics( + document: &crate::edit::Document, encoding: PositionEncoding, - version: crate::edit::DocumentVersion, diagnostics: Vec, ) -> crate::Result> { diagnostics @@ -118,14 +116,6 @@ pub(crate) fn fixes_for_diagnostics<'d>( .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, @@ -133,7 +123,7 @@ pub(crate) fn fixes_for_diagnostics<'d>( .kind .suggestion .unwrap_or(associated_data.kind.name), - document_edits, + edits: edits.collect(), })) }) .filter_map(crate::Result::transpose) diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 90cf543a29fac7..d649379d483171 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -41,6 +41,7 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { BackgroundSchedule::LatencySensitive, ) } + request::ExecuteCommand::METHOD => local_request_task::(req), request::Format::METHOD => { background_request_task::(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>( req: server::Request, ) -> super::Result> { let (id, params) = cast_request::(req)?; - Ok(Task::local(|session, notifier, responder| { - let result = R::run(session, notifier, params); + Ok(Task::local(|session, notifier, requester, responder| { + let result = R::run(session, notifier, requester, params); respond::(id, result, &responder); })) } @@ -119,7 +119,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>( notif: server::Notification, ) -> super::Result> { let (id, params) = cast_notification::(notif)?; - Ok(Task::local(move |session, notifier, _| { + Ok(Task::local(move |session, notifier, _, _| { if let Err(err) = N::run(session, notifier, params) { tracing::error!("An error occurred while running {id}: {err}"); } diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index 3883c009bdec47..3713ef536f592e 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -1,16 +1,18 @@ mod code_action; mod code_action_resolve; mod diagnostic; +mod execute_command; mod format; mod format_range; use super::{ define_document_url, - traits::{BackgroundDocumentRequestHandler, RequestHandler}, + traits::{BackgroundDocumentRequestHandler, RequestHandler, SyncRequestHandler}, }; pub(super) use code_action::CodeActions; pub(super) use code_action_resolve::CodeActionResolve; pub(super) use diagnostic::DocumentDiagnostic; +pub(super) use execute_command::ExecuteCommand; pub(super) use format::Format; pub(super) use format_range::FormatRange; diff --git a/crates/ruff_server/src/server/api/requests/code_action.rs b/crates/ruff_server/src/server/api/requests/code_action.rs index e9bc8d1842d855..0fa462cd2b38b0 100644 --- a/crates/ruff_server/src/server/api/requests/code_action.rs +++ b/crates/ruff_server/src/server/api/requests/code_action.rs @@ -1,3 +1,4 @@ +use crate::edit::WorkspaceEditTracker; use crate::lint::fixes_for_diagnostics; use crate::server::api::LSPResult; use crate::server::SupportedCodeAction; @@ -50,30 +51,34 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { fn quick_fix( snapshot: &DocumentSnapshot, diagnostics: Vec, -) -> crate::Result + '_> { +) -> crate::Result> { 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())), + 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() - }), - 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 { @@ -92,9 +97,11 @@ fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result { ( Some(resolve_edit_for_fix_all( document, + snapshot.resolved_client_capabilities(), snapshot.url(), &snapshot.configuration().linter, snapshot.encoding(), + document.version(), )?), None, ) @@ -125,9 +132,11 @@ fn organize_imports(snapshot: &DocumentSnapshot) -> crate::Result Some( resolve_edit_for_fix_all( document, + snapshot.resolved_client_capabilities(), snapshot.url(), &snapshot.configuration().linter, snapshot.encoding(), + document.version(), ) .with_failure_code(ErrorCode::InternalError)?, ), SupportedCodeAction::SourceOrganizeImports => Some( resolve_edit_for_organize_imports( document, + snapshot.resolved_client_capabilities(), snapshot.url(), &snapshot.configuration().linter, snapshot.encoding(), + document.version(), ) .with_failure_code(ErrorCode::InternalError)?, ), @@ -71,29 +76,51 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve { pub(super) fn resolve_edit_for_fix_all( document: &crate::edit::Document, + client_capabilities: &ResolvedClientCapabilities, url: &types::Url, linter_settings: &LinterSettings, encoding: PositionEncoding, + version: DocumentVersion, ) -> crate::Result { - Ok(types::WorkspaceEdit { - changes: Some( - [( - url.clone(), - crate::fix::fix_all(document, linter_settings, encoding)?, - )] - .into_iter() - .collect(), - ), - ..Default::default() - }) + let mut tracker = WorkspaceEditTracker::new(client_capabilities); + tracker.set_edits_for_document( + url.clone(), + version, + fix_all_edit(document, linter_settings, encoding)?, + )?; + Ok(tracker.into_workspace_edit()) +} + +pub(super) fn fix_all_edit( + document: &crate::edit::Document, + linter_settings: &LinterSettings, + encoding: PositionEncoding, +) -> crate::Result> { + crate::fix::fix_all(document, linter_settings, encoding) } pub(super) fn resolve_edit_for_organize_imports( document: &crate::edit::Document, + client_capabilities: &ResolvedClientCapabilities, url: &types::Url, linter_settings: &ruff_linter::settings::LinterSettings, encoding: PositionEncoding, + version: DocumentVersion, ) -> crate::Result { + 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> { let mut linter_settings = linter_settings.clone(); linter_settings.rules = [ Rule::UnsortedImports, // I001 @@ -102,15 +129,5 @@ pub(super) fn resolve_edit_for_organize_imports( .into_iter() .collect(); - Ok(types::WorkspaceEdit { - changes: Some( - [( - url.clone(), - crate::fix::fix_all(document, &linter_settings, encoding)?, - )] - .into_iter() - .collect(), - ), - ..Default::default() - }) + crate::fix::fix_all(document, &linter_settings, encoding) } diff --git a/crates/ruff_server/src/server/api/requests/execute_command.rs b/crates/ruff_server/src/server/api/requests/execute_command.rs new file mode 100644 index 00000000000000..c8cdb7fdec05a7 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/execute_command.rs @@ -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> { + 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 = params + .arguments + .into_iter() + .map(|value| serde_json::from_value(value).with_failure_code(ErrorCode::InvalidParams)) + .collect::>()?; + + 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 { + 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::( + 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() + }, + ) +} diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 566e3609054bfa..05a4485f594e7e 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -19,31 +19,35 @@ impl super::BackgroundDocumentRequestHandler for Format { _notifier: Notifier, _params: types::DocumentFormattingParams, ) -> Result { - let doc = snapshot.document(); - let source = doc.contents(); - let formatted = crate::format::format(doc, &snapshot.configuration().formatter) - .with_failure_code(lsp_server::ErrorCode::InternalError)?; - // fast path - if the code is the same, return early - if formatted == source { - return Ok(None); - } - let formatted_index: LineIndex = LineIndex::from_source_text(&formatted); + format_document(&snapshot) + } +} - let unformatted_index = doc.index(); +pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { + let doc = snapshot.document(); + let source = doc.contents(); + let formatted = crate::format::format(doc, &snapshot.configuration().formatter) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + // fast path - if the code is the same, return early + if formatted == source { + return Ok(None); + } + let formatted_index: LineIndex = LineIndex::from_source_text(&formatted); - let Replacement { - source_range, - modified_range: formatted_range, - } = Replacement::between( - source, - unformatted_index.line_starts(), - &formatted, - formatted_index.line_starts(), - ); + let unformatted_index = doc.index(); - Ok(Some(vec![TextEdit { - range: source_range.to_range(source, unformatted_index, snapshot.encoding()), - new_text: formatted[formatted_range].to_owned(), - }])) - } + let Replacement { + source_range, + modified_range: formatted_range, + } = Replacement::between( + source, + unformatted_index.line_starts(), + &formatted, + formatted_index.line_starts(), + ); + + Ok(Some(vec![TextEdit { + range: source_range.to_range(source, unformatted_index, snapshot.encoding()), + new_text: formatted[formatted_range].to_owned(), + }])) } diff --git a/crates/ruff_server/src/server/api/traits.rs b/crates/ruff_server/src/server/api/traits.rs index 7a980ebfca8e7b..59da1624e39241 100644 --- a/crates/ruff_server/src/server/api/traits.rs +++ b/crates/ruff_server/src/server/api/traits.rs @@ -1,6 +1,6 @@ //! 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 lsp_types::notification::Notification as LSPNotification; @@ -20,6 +20,7 @@ pub(super) trait SyncRequestHandler: RequestHandler { fn run( session: &mut Session, notifier: Notifier, + requester: &mut Requester, params: <::RequestType as Request>::Params, ) -> super::Result<<::RequestType as Request>::Result>; } diff --git a/crates/ruff_server/src/server/schedule.rs b/crates/ruff_server/src/server/schedule.rs index 3e5ecbd35a7817..fe8cc5c18c4e0b 100644 --- a/crates/ruff_server/src/server/schedule.rs +++ b/crates/ruff_server/src/server/schedule.rs @@ -80,10 +80,13 @@ impl<'s> Scheduler<'s> { pub(super) fn dispatch(&mut self, task: task::Task<'s>) { match task { Task::Sync(SyncTask { func }) => { + let notifier = self.client.notifier(); + let responder = self.client.responder(); func( self.session, - self.client.notifier(), - self.client.responder(), + notifier, + &mut self.client.requester, + responder, ); } Task::Background(BackgroundTaskBuilder { diff --git a/crates/ruff_server/src/server/schedule/task.rs b/crates/ruff_server/src/server/schedule/task.rs index b4de2d8c97b0a3..fdba5e3991d9a6 100644 --- a/crates/ruff_server/src/server/schedule/task.rs +++ b/crates/ruff_server/src/server/schedule/task.rs @@ -2,11 +2,11 @@ use lsp_server::RequestId; use serde::Serialize; use crate::{ - server::client::{Notifier, Responder}, + server::client::{Notifier, Requester, Responder}, session::Session, }; -type LocalFn<'s> = Box; +type LocalFn<'s> = Box; type BackgroundFn = Box; @@ -68,7 +68,9 @@ impl<'s> Task<'s> { }) } /// 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 { func: Box::new(func), }) @@ -79,14 +81,15 @@ impl<'s> Task<'s> { where R: Serialize + Send + 'static, { - Self::local(move |_, _, responder| { + Self::local(move |_, _, _, responder| { if let Err(err) = responder.respond(id, result) { tracing::error!("Unable to send immediate response: {err}"); } }) } + /// Creates a local task that does nothing. pub(crate) fn nothing() -> Self { - Self::local(move |_, _, _| {}) + Self::local(move |_, _, _, _| {}) } } diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index 208ad923fd5cfc..c7e1c6454bc6f2 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -15,7 +15,7 @@ use rustc_hash::FxHashMap; use crate::edit::{Document, DocumentVersion}; use crate::PositionEncoding; -use self::capabilities::ResolvedClientCapabilities; +pub(crate) use self::capabilities::ResolvedClientCapabilities; use self::settings::ResolvedClientSettings; pub(crate) use self::settings::{AllSettings, ClientSettings}; @@ -140,6 +140,10 @@ impl Session { Ok(()) } + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + pub(crate) fn encoding(&self) -> PositionEncoding { self.position_encoding } diff --git a/crates/ruff_server/src/session/capabilities.rs b/crates/ruff_server/src/session/capabilities.rs index f415aa75411692..563737542cf510 100644 --- a/crates/ruff_server/src/session/capabilities.rs +++ b/crates/ruff_server/src/session/capabilities.rs @@ -3,6 +3,8 @@ use lsp_types::ClientCapabilities; #[derive(Debug, Clone, PartialEq, Eq, Default)] pub(crate) struct ResolvedClientCapabilities { pub(crate) code_action_deferred_edit_resolution: bool, + pub(crate) apply_edit: bool, + pub(crate) document_changes: bool, } impl ResolvedClientCapabilities { @@ -17,9 +19,25 @@ impl ResolvedClientCapabilities { 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())); + + 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 { code_action_deferred_edit_resolution: code_action_data_support && code_action_edit_resolution, + apply_edit, + document_changes, } } }