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