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:
Jane Lewis 2024-04-05 16:27:35 -07:00 committed by GitHub
parent 1b31d4e9f1
commit c11e6d709c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 379 additions and 96 deletions

View file

@ -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<lsp_types::TextDocumentEdit>),
Changes(HashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>),
}
impl From<PositionEncoding> 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<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),
}
}
}

View file

@ -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<lsp_types::TextDocumentEdit>,
pub(crate) edits: Vec<lsp_types::TextEdit>,
}
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<lsp_types::Diagnostic>,
) -> crate::Result<Vec<DiagnosticFix>> {
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)

View file

@ -41,6 +41,7 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
BackgroundSchedule::LatencySensitive,
)
}
request::ExecuteCommand::METHOD => local_request_task::<request::ExecuteCommand>(req),
request::Format::METHOD => {
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>(
req: server::Request,
) -> super::Result<Task<'a>> {
let (id, params) = cast_request::<R>(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::<R>(id, result, &responder);
}))
}
@ -119,7 +119,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>(
notif: server::Notification,
) -> super::Result<Task<'a>> {
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) {
tracing::error!("An error occurred while running {id}: {err}");
}

View file

@ -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;

View file

@ -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<types::Diagnostic>,
) -> crate::Result<impl Iterator<Item = CodeActionOrCommand> + '_> {
) -> crate::Result<Vec<CodeActionOrCommand>> {
let document = snapshot.document();
let fixes = fixes_for_diagnostics(
document,
snapshot.url(),
snapshot.encoding(),
document.version(),
diagnostics,
)?;
let fixes = fixes_for_diagnostics(document, snapshot.encoding(), 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())),
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<CodeActionOrCommand> {
@ -92,9 +97,11 @@ fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
(
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<CodeActionOrCo
(
Some(resolve_edit_for_organize_imports(
document,
snapshot.resolved_client_capabilities(),
snapshot.url(),
&snapshot.configuration().linter,
snapshot.encoding(),
document.version(),
)?),
None,
)

View file

@ -1,9 +1,10 @@
use std::borrow::Cow;
use crate::edit::{DocumentVersion, WorkspaceEditTracker};
use crate::server::api::LSPResult;
use crate::server::SupportedCodeAction;
use crate::server::{client::Notifier, Result};
use crate::session::DocumentSnapshot;
use crate::session::{DocumentSnapshot, ResolvedClientCapabilities};
use crate::PositionEncoding;
use lsp_server::ErrorCode;
use lsp_types::{self as types, request as req};
@ -42,18 +43,22 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
SupportedCodeAction::SourceFixAll => 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<types::WorkspaceEdit> {
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<Vec<types::TextEdit>> {
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<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();
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)
}

View 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(&params.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()
},
)
}

View file

@ -19,31 +19,35 @@ impl super::BackgroundDocumentRequestHandler for Format {
_notifier: Notifier,
_params: types::DocumentFormattingParams,
) -> Result<super::FormatResponse> {
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 unformatted_index = doc.index();
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(),
}]))
format_document(&snapshot)
}
}
pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::FormatResponse> {
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 unformatted_index = doc.index();
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(),
}]))
}

View file

@ -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: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
}

View file

@ -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 {

View file

@ -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<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>;
@ -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 |_, _, _, _| {})
}
}

View file

@ -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
}

View file

@ -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,
}
}
}