diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8ed57efa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +# Development version + +- The LSP gains range formatting support (#63). + +- The `air format` command has been improved and is now able to take multiple files and directories. + + +# 0.1.0 + +- Initial release. diff --git a/Cargo.lock b/Cargo.lock index 226e1976..a17078ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1307,6 +1307,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" name = "lsp" version = "0.0.0" dependencies = [ + "air_r_factory", "air_r_formatter", "air_r_parser", "air_r_syntax", @@ -1315,6 +1316,7 @@ dependencies = [ "biome_formatter", "biome_lsp_converters", "biome_parser", + "biome_rowan", "biome_text_size", "bytes", "cargo_metadata", diff --git a/crates/air_r_factory/src/lib.rs b/crates/air_r_factory/src/lib.rs index 8203124b..ed0f7a8d 100644 --- a/crates/air_r_factory/src/lib.rs +++ b/crates/air_r_factory/src/lib.rs @@ -1,2 +1,3 @@ mod generated; -pub use crate::generated::RSyntaxFactory; +pub use crate::generated::node_factory::*; +pub use crate::generated::*; diff --git a/crates/air_r_formatter/src/r/auxiliary/binary_expression.rs b/crates/air_r_formatter/src/r/auxiliary/binary_expression.rs index cd069a41..8d0cf3d3 100644 --- a/crates/air_r_formatter/src/r/auxiliary/binary_expression.rs +++ b/crates/air_r_formatter/src/r/auxiliary/binary_expression.rs @@ -117,6 +117,7 @@ fn fmt_binary( ) } +#[derive(Debug)] struct TailPiece { operator: SyntaxToken, right: AnyRExpression, diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 5bf1e568..3c726341 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] +air_r_factory.workspace = true air_r_formatter.workspace = true air_r_parser.workspace = true air_r_syntax.workspace = true @@ -19,6 +20,7 @@ anyhow.workspace = true biome_formatter.workspace = true biome_lsp_converters.workspace = true biome_parser.workspace = true +biome_rowan.workspace = true biome_text_size.workspace = true crossbeam.workspace = true dissimilar.workspace = true diff --git a/crates/lsp/src/documents.rs b/crates/lsp/src/documents.rs index 56ed960e..d2406dae 100644 --- a/crates/lsp/src/documents.rs +++ b/crates/lsp/src/documents.rs @@ -89,6 +89,13 @@ impl Document { Self::new(contents.into(), None, PositionEncoding::Utf8) } + #[cfg(test)] + pub fn doodle_and_range(contents: &str) -> (Self, biome_text_size::TextRange) { + let (contents, range) = crate::test_utils::extract_marked_range(contents); + let doc = Self::new(contents, None, PositionEncoding::Utf8); + (doc, range) + } + pub fn on_did_change(&mut self, mut params: lsp_types::DidChangeTextDocumentParams) { let new_version = params.text_document.version; diff --git a/crates/lsp/src/handlers.rs b/crates/lsp/src/handlers.rs index fff87adf..53cb5ebc 100644 --- a/crates/lsp/src/handlers.rs +++ b/crates/lsp/src/handlers.rs @@ -46,13 +46,6 @@ pub(crate) async fn handle_initialized( regs.append(&mut config_diagnostics_regs); } - // TODO! Abstract this in a method - regs.push(lsp_types::Registration { - id: String::from("air_formatting"), - method: String::from("textDocument/formatting"), - register_options: None, - }); - client .register_capability(regs) .instrument(span.exit()) diff --git a/crates/lsp/src/handlers_format.rs b/crates/lsp/src/handlers_format.rs index ada97f36..c9de94b3 100644 --- a/crates/lsp/src/handlers_format.rs +++ b/crates/lsp/src/handlers_format.rs @@ -6,11 +6,14 @@ // use air_r_formatter::{context::RFormatOptions, format_node}; +use air_r_syntax::{RExpressionList, RSyntaxKind, RSyntaxNode, WalkEvent}; use biome_formatter::{IndentStyle, LineWidth}; +use biome_rowan::{AstNode, Language, SyntaxElement}; +use biome_text_size::{TextRange, TextSize}; use tower_lsp::lsp_types; use crate::state::WorldState; -use crate::to_proto; +use crate::{from_proto, to_proto}; #[tracing::instrument(level = "info", skip_all)] pub(crate) fn document_formatting( @@ -41,6 +44,193 @@ pub(crate) fn document_formatting( Ok(Some(edits)) } +#[tracing::instrument(level = "info", skip_all)] +pub(crate) fn document_range_formatting( + params: lsp_types::DocumentRangeFormattingParams, + state: &WorldState, +) -> anyhow::Result>> { + let doc = state.get_document(¶ms.text_document.uri)?; + + let line_width = LineWidth::try_from(80).map_err(|err| anyhow::anyhow!("{err}"))?; + let range = + from_proto::text_range(&doc.line_index.index, params.range, doc.line_index.encoding)?; + + // TODO: Handle FormattingOptions + let options = RFormatOptions::default() + .with_indent_style(IndentStyle::Space) + .with_line_width(line_width); + + let logical_lines = find_deepest_enclosing_logical_lines(doc.parse.syntax(), range); + if logical_lines.is_empty() { + tracing::warn!("Can't find logical line"); + return Ok(None); + }; + + // Find the overall formatting range by concatenating the ranges of the logical lines. + // We use the "non-whitespace-range" as that corresponds to what Biome will format. + let format_range = logical_lines + .iter() + .map(text_non_whitespace_range) + .reduce(|acc, new| acc.cover(new)) + .expect("`logical_lines` is non-empty"); + + // We need to wrap in an `RRoot` otherwise the comments get attached too + // deep in the tree. See `CommentsBuilderVisitor` in biome_formatter and the + // `is_root` logic. Note that `node` needs to be wrapped in at least two + // other nodes in order to fix this problem, and here we have an `RRoot` and + // `RExpressionList` that do the job. + // + // Since we only format logical lines, it is fine to wrap in an expression list. + let Some(exprs): Option> = logical_lines + .into_iter() + .map(air_r_syntax::AnyRExpression::cast) + .collect() + else { + tracing::warn!("Can't cast to `AnyRExpression`"); + return Ok(None); + }; + + let list = air_r_factory::r_expression_list(exprs); + let eof = air_r_syntax::RSyntaxToken::new_detached(RSyntaxKind::EOF, "", vec![], vec![]); + let root = air_r_factory::r_root(list, eof).build(); + + let format_info = biome_formatter::format_sub_tree( + root.syntax(), + air_r_formatter::RFormatLanguage::new(options), + )?; + + if format_info.range().is_none() { + // Happens in edge cases when biome returns a `Printed::new_empty()` + return Ok(None); + }; + + let mut format_text = format_info.into_code(); + + // Remove last hard break line from our artifical expression list + format_text.pop(); + let edits = to_proto::replace_range_edit(&doc.line_index, format_range, format_text)?; + + Ok(Some(edits)) +} + +// From biome_formatter +fn text_non_whitespace_range(elem: &E) -> TextRange +where + E: Into> + Clone, + L: Language, +{ + let elem: SyntaxElement = elem.clone().into(); + + let start = elem + .leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .find_map(|piece| { + if piece.is_whitespace() || piece.is_newline() { + None + } else { + Some(piece.text_range().start()) + } + }) + .unwrap_or_else(|| elem.text_trimmed_range().start()); + + let end = elem + .trailing_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces().rev()) + .find_map(|piece| { + if piece.is_whitespace() || piece.is_newline() { + None + } else { + Some(piece.text_range().end()) + } + }) + .unwrap_or_else(|| elem.text_trimmed_range().end()); + + TextRange::new(start, end) +} + +/// Finds consecutive logical lines. Currently that's only expressions at +/// top-level or in a braced list. +fn find_deepest_enclosing_logical_lines(node: RSyntaxNode, range: TextRange) -> Vec { + let start_lists = find_expression_lists(&node, range.start(), false); + let end_lists = find_expression_lists(&node, range.end(), true); + + // Both vectors of lists should have a common prefix, starting from the + // program's expression list. As soon as the lists diverge we stop. + let Some(list) = start_lists + .into_iter() + .zip(end_lists) + .take_while(|pair| pair.0 == pair.1) + .map(|pair| pair.0) + .last() + else { + // Should not happen as the range is always included in the program's expression list + tracing::warn!("Can't find common list parent"); + return vec![]; + }; + + let Some(list) = RExpressionList::cast(list) else { + tracing::warn!("Can't cast to expression list"); + return vec![]; + }; + + let iter = list.into_iter(); + + // We've chosen to be liberal about user selections and always widen the + // range to include the selection bounds. If we wanted to be conservative + // instead, we could use this `filter()` instead of the `skip_while()` and + // `take_while()`: + // + // ```rust + // .filter(|node| range.contains_range(node.text_trimmed_range())) + // ``` + let logical_lines: Vec = iter + .map(|expr| expr.into_syntax()) + .skip_while(|node| !node.text_range().contains(range.start())) + .take_while(|node| node.text_trimmed_range().start() <= range.end()) + .collect(); + + logical_lines +} + +fn find_expression_lists(node: &RSyntaxNode, offset: TextSize, end: bool) -> Vec { + let mut preorder = node.preorder(); + let mut nodes: Vec = vec![]; + + while let Some(event) = preorder.next() { + match event { + WalkEvent::Enter(node) => { + let Some(parent) = node.parent() else { + continue; + }; + + let is_contained = if end { + let trimmed_node_range = node.text_trimmed_range(); + trimmed_node_range.contains_inclusive(offset) + } else { + let node_range = node.text_range(); + node_range.contains(offset) + }; + + if !is_contained { + preorder.skip_subtree(); + continue; + } + + if parent.kind() == RSyntaxKind::R_EXPRESSION_LIST { + nodes.push(parent.clone()); + continue; + } + } + + WalkEvent::Leave(_) => {} + } + } + + nodes +} + #[cfg(test)] mod tests { use crate::{ @@ -87,4 +277,238 @@ mod tests { client } + + #[tests_macros::lsp_test] + async fn test_format_range_none() { + let mut client = init_test_client().await; + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<<>>", + ); + + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<< +>>", + ); + + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<<1 +>>", + ); + + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + client + } + + #[tests_macros::lsp_test] + async fn test_format_range_logical_lines() { + let mut client = init_test_client().await; + + // 2+2 is the logical line to format + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +<<2+2>> +", + ); + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +# +<<2+2>> +", + ); + + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + // The element in the braced expression is a logical line + // FIXME: Should this be the whole `{2+2}` instead? + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +{<<2+2>>} +", + ); + + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +<<{2+2}>> +", + ); + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + // The deepest element in the braced expression is our target + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +{ + 2+2 + { + <<3+3>> + } +} +", + ); + + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + client + } + + #[tests_macros::lsp_test] + async fn test_format_range_mismatched_indent() { + let mut client = init_test_client().await; + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1 + <<2+2>> +", + ); + + // We don't change indentation when `2+2` is formatted + let output = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output); + + // Debatable: Should we make an effort to remove unneeded indentation + // when it's part of the range? + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1 +<< 2+2>> +", + ); + let output_wide = client.format_document_range(&doc, range).await; + assert_eq!(output, output_wide); + + client + } + + #[tests_macros::lsp_test] + async fn test_format_range_multiple_lines() { + let mut client = init_test_client().await; + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +<<# +2+2>> +", + ); + + let output1 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output1); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<<1+1 +# +2+2>> +", + ); + let output2 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output2); + + client + } + + #[tests_macros::lsp_test] + async fn test_format_range_unmatched_lists() { + let mut client = init_test_client().await; + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"0+0 +<<1+1 +{ + 2+2>> +} +3+3 +", + ); + + let output1 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output1); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"0+0 +<<1+1 +{ +>> 2+2 +} +3+3 +", + ); + let output2 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output2); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"0+0 +<<1+1 +{ + 2+2 +} +>>3+3 +", + ); + let output3 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output3); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"0+0 +1+1 +{ +<< 2+2 +} +>>3+3 +", + ); + let output4 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output4); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<<1+1>> +2+2 +", + ); + + let output5 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output5); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"1+1 +<<2+2>> +", + ); + + let output6 = client.format_document_range(&doc, range).await; + insta::assert_snapshot!(output6); + + client + } } diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs index 211eb1b2..a570cf31 100644 --- a/crates/lsp/src/handlers_state.rs +++ b/crates/lsp/src/handlers_state.rs @@ -133,6 +133,7 @@ pub(crate) fn initialize( file_operations: None, }), document_formatting_provider: Some(OneOf::Left(true)), + document_range_formatting_provider: Some(OneOf::Left(true)), ..ServerCapabilities::default() }, }) diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 30c20aac..65451b9f 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -19,5 +19,7 @@ pub mod state; pub mod to_proto; pub mod tower_lsp; +#[cfg(test)] +pub mod test_utils; #[cfg(test)] pub mod tower_lsp_test_client; diff --git a/crates/lsp/src/main_loop.rs b/crates/lsp/src/main_loop.rs index 4f07eb21..780a635b 100644 --- a/crates/lsp/src/main_loop.rs +++ b/crates/lsp/src/main_loop.rs @@ -338,6 +338,9 @@ impl GlobalState { LspRequest::DocumentFormatting(params) => { respond(tx, handlers_format::document_formatting(params, &self.world), LspResponse::DocumentFormatting)?; }, + LspRequest::DocumentRangeFormatting(params) => { + respond(tx, handlers_format::document_range_formatting(params, &self.world), LspResponse::DocumentRangeFormatting)?; + }, LspRequest::AirViewFile(params) => { respond(tx, handlers_ext::view_file(params, &self.world), LspResponse::AirViewFile)?; }, diff --git a/crates/lsp/src/rust_analyzer/to_proto.rs b/crates/lsp/src/rust_analyzer/to_proto.rs index 4ec929da..1eab5351 100644 --- a/crates/lsp/src/rust_analyzer/to_proto.rs +++ b/crates/lsp/src/rust_analyzer/to_proto.rs @@ -1,7 +1,7 @@ // --- source // authors = ["rust-analyzer team"] // license = "MIT OR Apache-2.0" -// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/from_proto.rs" +// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/to_proto.rs" // --- //! Conversion of rust-analyzer specific types to lsp_types equivalents. diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap new file mode 100644 index 00000000..ec60bfb9 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1+1 +# +2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap new file mode 100644 index 00000000..f5bf150f --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1+1 +{2 + 2} diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap new file mode 100644 index 00000000..cda77a86 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap @@ -0,0 +1,8 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1+1 +{ + 2 + 2 +} diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap new file mode 100644 index 00000000..df9e8931 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap @@ -0,0 +1,11 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1+1 +{ + 2+2 + { + 3 + 3 + } +} diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap new file mode 100644 index 00000000..cf743f29 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1+1 +2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap new file mode 100644 index 00000000..9681b5cd --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1 + 2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap new file mode 100644 index 00000000..cc0b2f04 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output2 +--- +1 + 1 +# +2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap new file mode 100644 index 00000000..f63c36ef --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap @@ -0,0 +1,7 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output1 +--- +1+1 +# +2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap new file mode 100644 index 00000000..f3257fac --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- + diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap new file mode 100644 index 00000000..0fb74fc7 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap @@ -0,0 +1,5 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +1 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap new file mode 100644 index 00000000..f3257fac --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap @@ -0,0 +1,5 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- + diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap new file mode 100644 index 00000000..776dab17 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output2 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3+3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap new file mode 100644 index 00000000..5d9e27d0 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap @@ -0,0 +1,10 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output3 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap new file mode 100644 index 00000000..4234f400 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap @@ -0,0 +1,10 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output4 +--- +0+0 +1+1 +{ + 2 + 2 +} +3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap new file mode 100644 index 00000000..2055d4bb --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output5 +--- +1 + 1 +2+2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap new file mode 100644 index 00000000..2388991a --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output6 +--- +1+1 +2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap new file mode 100644 index 00000000..33752478 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap @@ -0,0 +1,10 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output1 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3+3 diff --git a/crates/lsp/src/test_utils.rs b/crates/lsp/src/test_utils.rs new file mode 100644 index 00000000..fe60c0c9 --- /dev/null +++ b/crates/lsp/src/test_utils.rs @@ -0,0 +1,27 @@ +use biome_text_size::{TextRange, TextSize}; + +pub(crate) fn extract_marked_range(input: &str) -> (String, TextRange) { + let mut output = String::new(); + let mut start = None; + let mut end = None; + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '<' && chars.peek() == Some(&'<') { + chars.next(); + start = Some(TextSize::from(output.len() as u32)); + } else if c == '>' && chars.peek() == Some(&'>') { + chars.next(); + end = Some(TextSize::from(output.len() as u32)); + } else { + output.push(c); + } + } + + let range = match (start, end) { + (Some(start), Some(end)) => TextRange::new(start, end), + _ => panic!("Missing range markers"), + }; + + (output, range) +} diff --git a/crates/lsp/src/to_proto.rs b/crates/lsp/src/to_proto.rs index e0e9f638..4b4c5f5e 100644 --- a/crates/lsp/src/to_proto.rs +++ b/crates/lsp/src/to_proto.rs @@ -9,7 +9,11 @@ pub(crate) use rust_analyzer::to_proto::text_edit_vec; +#[cfg(test)] +pub(crate) use biome_lsp_converters::to_proto::range; + use crate::rust_analyzer::{self, line_index::LineIndex, text_edit::TextEdit}; +use biome_text_size::TextRange; use tower_lsp::lsp_types; pub(crate) fn doc_edit_vec( @@ -28,6 +32,15 @@ pub(crate) fn doc_edit_vec( .collect()) } +pub(crate) fn replace_range_edit( + line_index: &LineIndex, + range: TextRange, + replace_with: String, +) -> anyhow::Result> { + let edit = TextEdit::replace(range, replace_with); + text_edit_vec(line_index, edit) +} + pub(crate) fn replace_all_edit( line_index: &LineIndex, text: &str, diff --git a/crates/lsp/src/tower_lsp.rs b/crates/lsp/src/tower_lsp.rs index 5e824475..ba114988 100644 --- a/crates/lsp/src/tower_lsp.rs +++ b/crates/lsp/src/tower_lsp.rs @@ -62,6 +62,7 @@ pub(crate) enum LspRequest { Initialize(InitializeParams), DocumentFormatting(DocumentFormattingParams), Shutdown, + DocumentRangeFormatting(DocumentRangeFormattingParams), AirViewFile(ViewFileParams), } @@ -70,6 +71,7 @@ pub(crate) enum LspRequest { pub(crate) enum LspResponse { Initialize(InitializeResult), DocumentFormatting(Option>), + DocumentRangeFormatting(Option>), Shutdown(()), AirViewFile(String), } @@ -250,6 +252,17 @@ impl LanguageServer for Backend { LspResponse::DocumentFormatting ) } + + async fn range_formatting( + &self, + params: DocumentRangeFormattingParams, + ) -> Result>> { + cast_response!( + self.request(LspRequest::DocumentRangeFormatting(params)) + .await, + LspResponse::DocumentRangeFormatting + ) + } } pub async fn start_lsp(read: I, write: O) diff --git a/crates/lsp/src/tower_lsp_test_client.rs b/crates/lsp/src/tower_lsp_test_client.rs index 6d423718..cae23ed9 100644 --- a/crates/lsp/src/tower_lsp_test_client.rs +++ b/crates/lsp/src/tower_lsp_test_client.rs @@ -1,12 +1,20 @@ +use biome_text_size::TextRange; use lsp_test::lsp_client::TestClient; use tower_lsp::lsp_types; -use crate::{documents::Document, from_proto}; +use crate::{documents::Document, from_proto, to_proto}; pub(crate) trait TestClientExt { async fn open_document(&mut self, doc: &Document) -> lsp_types::TextDocumentItem; + async fn format_document(&mut self, doc: &Document) -> String; + async fn format_document_range(&mut self, doc: &Document, range: TextRange) -> String; async fn format_document_edits(&mut self, doc: &Document) -> Option>; + async fn format_document_range_edits( + &mut self, + doc: &Document, + range: TextRange, + ) -> Option>; } impl TestClientExt for TestClient { @@ -34,6 +42,13 @@ impl TestClientExt for TestClient { from_proto::apply_text_edits(doc, edits).unwrap() } + async fn format_document_range(&mut self, doc: &Document, range: TextRange) -> String { + let Some(edits) = self.format_document_range_edits(doc, range).await else { + return doc.contents.clone(); + }; + from_proto::apply_text_edits(doc, edits).unwrap() + } + async fn format_document_edits(&mut self, doc: &Document) -> Option> { let lsp_doc = self.open_document(doc).await; @@ -54,6 +69,49 @@ impl TestClientExt for TestClient { let response = self.recv_response().await; + if let Some(err) = response.error() { + panic!("Unexpected error: {}", err.message); + }; + + let value: Option> = + serde_json::from_value(response.result().unwrap().clone()).unwrap(); + + self.close_document(lsp_doc.uri).await; + + value + } + + async fn format_document_range_edits( + &mut self, + doc: &Document, + range: TextRange, + ) -> Option> { + let lsp_doc = self.open_document(doc).await; + + let options = lsp_types::FormattingOptions { + tab_size: 4, + insert_spaces: false, + ..Default::default() + }; + + let range = to_proto::range(&doc.line_index.index, range, doc.line_index.encoding).unwrap(); + + self.range_formatting(lsp_types::DocumentRangeFormattingParams { + text_document: lsp_types::TextDocumentIdentifier { + uri: lsp_doc.uri.clone(), + }, + range, + options, + work_done_progress_params: Default::default(), + }) + .await; + + let response = self.recv_response().await; + + if let Some(err) = response.error() { + panic!("Unexpected error: {}", err.message); + }; + let value: Option> = serde_json::from_value(response.result().unwrap().clone()).unwrap(); diff --git a/crates/lsp_test/src/lsp_client.rs b/crates/lsp_test/src/lsp_client.rs index 4d086590..dc6cd1da 100644 --- a/crates/lsp_test/src/lsp_client.rs +++ b/crates/lsp_test/src/lsp_client.rs @@ -152,4 +152,12 @@ impl TestClient { pub async fn formatting(&mut self, params: lsp_types::DocumentFormattingParams) -> i64 { self.request::(params).await } + + pub async fn range_formatting( + &mut self, + params: lsp_types::DocumentRangeFormattingParams, + ) -> i64 { + self.request::(params) + .await + } }