ruff/crates/ruff_server/src/edit.rs
Jane Lewis 3ab7a8da73
Add Jupyter Notebook document change snapshot test (#11944)
## Summary

Closes #11914.

This PR introduces a snapshot test that replays the LSP requests made
during a document formatting request, and confirms that the notebook
document is updated in the expected way.
2024-06-21 05:29:27 +00:00

174 lines
5.9 KiB
Rust

//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion.
mod notebook;
mod range;
mod replacement;
mod text_document;
use std::collections::HashMap;
use lsp_types::{PositionEncodingKind, Url};
pub use notebook::NotebookDocument;
pub(crate) use range::{NotebookRange, RangeExt, ToRangeExt};
pub(crate) use replacement::Replacement;
pub(crate) use text_document::DocumentVersion;
pub use text_document::TextDocument;
use crate::{fix::Fixes, 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)]
pub enum PositionEncoding {
/// UTF 16 is the encoding supported by all LSP clients.
#[default]
UTF16,
/// Second choice because UTF32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy)
UTF32,
/// Ruff's preferred encoding
UTF8,
}
/// A unique document ID, derived from a URL passed as part of an LSP request.
/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook.
#[derive(Clone, Debug)]
pub enum DocumentKey {
Notebook(Url),
NotebookCell(Url),
Text(Url),
}
impl DocumentKey {
/// Converts the key back into its original URL.
pub(crate) fn into_url(self) -> Url {
match self {
DocumentKey::NotebookCell(url)
| DocumentKey::Notebook(url)
| DocumentKey::Text(url) => url,
}
}
}
impl std::fmt::Display for DocumentKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotebookCell(url) | Self::Notebook(url) | Self::Text(url) => url.fmt(f),
}
}
}
/// 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<Url, Vec<lsp_types::TextEdit>>),
}
impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
fn from(value: PositionEncoding) -> Self {
match value {
PositionEncoding::UTF8 => lsp_types::PositionEncodingKind::UTF8,
PositionEncoding::UTF16 => lsp_types::PositionEncodingKind::UTF16,
PositionEncoding::UTF32 => lsp_types::PositionEncodingKind::UTF32,
}
}
}
impl TryFrom<&lsp_types::PositionEncodingKind> for PositionEncoding {
type Error = ();
fn try_from(value: &PositionEncodingKind) -> Result<Self, Self::Error> {
Ok(if value == &PositionEncodingKind::UTF8 {
PositionEncoding::UTF8
} else if value == &PositionEncodingKind::UTF16 {
PositionEncoding::UTF16
} else if value == &PositionEncodingKind::UTF32 {
PositionEncoding::UTF32
} else {
return Err(());
})
}
}
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 a series of [`Fixes`] for a text or notebook document.
pub(crate) fn set_fixes_for_document(
&mut self,
fixes: Fixes,
version: DocumentVersion,
) -> crate::Result<()> {
for (uri, edits) in fixes {
self.set_edits_for_document(uri, version, edits)?;
}
Ok(())
}
/// 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: 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,
// TODO(jane): Re-enable versioned edits after investigating whether it could work with notebook cells
version: None,
},
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),
}
}
}