mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 20:42:10 +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 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
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,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(),
|
||||
}]))
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 |_, _, _, _| {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue