diff --git a/crates/lsp/src/rust_analyzer/diff.rs b/crates/lsp/src/diff.rs similarity index 100% rename from crates/lsp/src/rust_analyzer/diff.rs rename to crates/lsp/src/diff.rs diff --git a/crates/lsp/src/documents.rs b/crates/lsp/src/documents.rs index 87286bdb..ef723d85 100644 --- a/crates/lsp/src/documents.rs +++ b/crates/lsp/src/documents.rs @@ -6,11 +6,12 @@ // use settings::LineEnding; +use std::ops::Range; use tower_lsp::lsp_types; +use crate::line_index::LineIndex; +use crate::proto::from_proto; use crate::proto::PositionEncoding; -use crate::rust_analyzer::line_index::LineIndex; -use crate::rust_analyzer::utils::apply_document_changes; use crate::settings::DocumentSettings; #[derive(Clone)] @@ -26,6 +27,12 @@ pub struct Document { /// correctly formatted text. pub line_index: LineIndex, + /// Original line endings, before normalization to Unix line endings + pub endings: LineEnding, + + /// Encoding used by [tower_lsp::Position] `character` offsets + pub position_encoding: PositionEncoding, + /// We store the syntax tree in the document for now. /// We will think about laziness and incrementality in the future. pub parse: air_r_parser::Parse, @@ -68,8 +75,6 @@ impl Document { // Create line index to keep track of newline offsets let line_index = LineIndex { index: triomphe::Arc::new(biome_line_index::LineIndex::new(&contents)), - endings, - encoding: position_encoding, }; // Parse document immediately for now @@ -78,6 +83,8 @@ impl Document { Self { contents, line_index, + endings, + position_encoding, parse, version, settings: Default::default(), @@ -121,8 +128,8 @@ impl Document { event.text = line_ending::normalize(text); } - let contents = apply_document_changes( - self.line_index.encoding, + let contents = Self::apply_document_changes( + self.position_encoding, &self.contents, params.content_changes, ); @@ -137,6 +144,55 @@ impl Document { self.version = Some(new_version); } + // --- 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/utils.rs" + // --- + fn apply_document_changes( + encoding: PositionEncoding, + file_contents: &str, + mut content_changes: Vec, + ) -> String { + // If at least one of the changes is a full document change, use the last + // of them as the starting point and ignore all previous changes. + let (mut text, content_changes) = match content_changes + .iter() + .rposition(|change| change.range.is_none()) + { + Some(idx) => { + let text = std::mem::take(&mut content_changes[idx].text); + (text, &content_changes[idx + 1..]) + } + None => (file_contents.to_owned(), &content_changes[..]), + }; + if content_changes.is_empty() { + return text; + } + + let mut line_index = biome_line_index::LineIndex::new(&text); + + // The changes we got must be applied sequentially, but can cross lines so we + // have to keep our line index updated. + // Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we + // remember the last valid line in the index and only rebuild it if needed. + // The VFS will normalize the end of lines to `\n`. + let mut index_valid = !0u32; + for change in content_changes { + // The None case can't happen as we have handled it above already + if let Some(range) = change.range { + if index_valid <= range.end.line { + line_index = biome_line_index::LineIndex::new(&text); + } + index_valid = range.start.line; + if let Ok(range) = from_proto::text_range(range, &line_index, encoding) { + text.replace_range(Range::::from(range), &change.text); + } + } + } + text + } + /// Convenient accessor that returns an annotated `SyntaxNode` type pub fn syntax(&self) -> air_r_syntax::RSyntaxNode { self.parse.syntax() @@ -148,8 +204,8 @@ mod tests { use air_r_syntax::RSyntaxNode; use biome_text_size::{TextRange, TextSize}; - use crate::rust_analyzer::text_edit::TextEdit; - use crate::to_proto; + use crate::proto::to_proto; + use crate::text_edit::TextEdit; use super::*; @@ -181,7 +237,13 @@ mod tests { TextRange::new(TextSize::from(4_u32), TextSize::from(7)), String::from("1 + 2"), ); - let edits = to_proto::doc_edit_vec(&doc.line_index, edit).unwrap(); + let edits = to_proto::doc_edit_vec( + edit, + &doc.line_index.index, + doc.position_encoding, + doc.endings, + ) + .unwrap(); let params = lsp_types::DidChangeTextDocumentParams { text_document: dummy_versioned_doc(), diff --git a/crates/lsp/src/handlers_format.rs b/crates/lsp/src/handlers_format.rs index a600dcdc..ff7a8818 100644 --- a/crates/lsp/src/handlers_format.rs +++ b/crates/lsp/src/handlers_format.rs @@ -14,8 +14,8 @@ use workspace::format::FormattedSource; use crate::file_patterns::is_document_excluded_from_formatting; use crate::main_loop::LspState; +use crate::proto::{from_proto, to_proto}; use crate::state::WorldState; -use crate::{from_proto, to_proto}; #[tracing::instrument(level = "info", skip_all)] pub(crate) fn document_formatting( @@ -55,9 +55,11 @@ pub(crate) fn document_formatting( match format_source_with_parse(&doc.contents, &doc.parse, format_options)? { FormattedSource::Changed(formatted) => Ok(Some(to_proto::replace_all_edit( - &doc.line_index, &doc.contents, &formatted, + &doc.line_index.index, + doc.position_encoding, + doc.endings, )?)), FormattedSource::Unchanged => Ok(None), } @@ -97,8 +99,7 @@ pub(crate) fn document_range_formatting( return Ok(None); } - let range = - from_proto::text_range(&doc.line_index.index, params.range, doc.line_index.encoding)?; + let range = from_proto::text_range(params.range, &doc.line_index.index, doc.position_encoding)?; let logical_lines = find_deepest_enclosing_logical_lines(doc.parse.syntax(), range); if logical_lines.is_empty() { @@ -150,7 +151,13 @@ pub(crate) fn document_range_formatting( // 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)?; + let edits = to_proto::replace_range_edit( + format_range, + format_text, + &doc.line_index.index, + doc.position_encoding, + doc.endings, + )?; Ok(Some(edits)) } diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 6b05b322..90057feb 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -1,22 +1,22 @@ pub use tower_lsp::start_lsp; pub mod capabilities; +pub mod diff; pub mod documents; pub mod file_patterns; -pub mod from_proto; pub mod handlers; pub mod handlers_ext; pub mod handlers_format; pub mod handlers_state; +pub mod line_index; pub mod logging; pub mod main_loop; pub mod notifications; pub mod proto; -pub mod rust_analyzer; pub mod settings; pub mod settings_vsc; pub mod state; -pub mod to_proto; +pub mod text_edit; pub mod tower_lsp; pub mod workspaces; diff --git a/crates/lsp/src/rust_analyzer/line_index.rs b/crates/lsp/src/line_index.rs similarity index 57% rename from crates/lsp/src/rust_analyzer/line_index.rs rename to crates/lsp/src/line_index.rs index b8009b5e..16cc9801 100644 --- a/crates/lsp/src/rust_analyzer/line_index.rs +++ b/crates/lsp/src/line_index.rs @@ -4,17 +4,9 @@ // origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/line_index.rs" // --- -//! Enhances `ide::LineIndex` with additional info required to convert offsets -//! into lsp positions. - -use settings::LineEnding; use triomphe::Arc; -use crate::proto::PositionEncoding; - #[derive(Debug, Clone)] pub struct LineIndex { pub index: Arc, - pub endings: LineEnding, - pub encoding: PositionEncoding, } diff --git a/crates/lsp/src/proto.rs b/crates/lsp/src/proto.rs index 26289746..c2d19d90 100644 --- a/crates/lsp/src/proto.rs +++ b/crates/lsp/src/proto.rs @@ -1,3 +1,8 @@ +pub mod from_proto; +pub mod to_proto; + +/// Our representation of [tower_lsp::PositionEncodingKind] +/// From `biome_lsp_converters::PositionEncoding` #[derive(Clone, Copy, Debug)] pub enum PositionEncoding { Utf8, diff --git a/crates/lsp/src/from_proto.rs b/crates/lsp/src/proto/from_proto.rs similarity index 58% rename from crates/lsp/src/from_proto.rs rename to crates/lsp/src/proto/from_proto.rs index 585d6373..36d2ce72 100644 --- a/crates/lsp/src/from_proto.rs +++ b/crates/lsp/src/proto/from_proto.rs @@ -4,14 +4,13 @@ use biome_line_index::LineIndex; use biome_line_index::WideLineCol; use tower_lsp::lsp_types; -use crate::documents::Document; use crate::proto::PositionEncoding; /// The function is used to convert a LSP position to TextSize. /// From `biome_lsp_converters::from_proto::offset()`. pub(crate) fn offset( - line_index: &LineIndex, position: lsp_types::Position, + line_index: &LineIndex, position_encoding: PositionEncoding, ) -> anyhow::Result { let line_col = match position_encoding { @@ -36,42 +35,11 @@ pub(crate) fn offset( /// The function is used to convert a LSP range to TextRange. /// From `biome_lsp_converters::from_proto::text_range()`. pub(crate) fn text_range( - line_index: &LineIndex, range: lsp_types::Range, + line_index: &LineIndex, position_encoding: PositionEncoding, ) -> anyhow::Result { - let start = offset(line_index, range.start, position_encoding)?; - let end = offset(line_index, range.end, position_encoding)?; + let start = offset(range.start, line_index, position_encoding)?; + let end = offset(range.end, line_index, position_encoding)?; Ok(biome_text_size::TextRange::new(start, end)) } - -pub fn apply_text_edits( - doc: &Document, - mut edits: Vec, -) -> anyhow::Result { - let mut text = doc.contents.clone(); - - // Apply edits from bottom to top to avoid inserted newlines to invalidate - // positions in earlier parts of the doc (they are sent in reading order - // accorder to the LSP protocol) - edits.reverse(); - - for edit in edits { - let start: usize = offset( - &doc.line_index.index, - edit.range.start, - doc.line_index.encoding, - )? - .into(); - let end: usize = offset( - &doc.line_index.index, - edit.range.end, - doc.line_index.encoding, - )? - .into(); - - text.replace_range(start..end, &edit.new_text); - } - - Ok(text) -} diff --git a/crates/lsp/src/to_proto.rs b/crates/lsp/src/proto/to_proto.rs similarity index 59% rename from crates/lsp/src/to_proto.rs rename to crates/lsp/src/proto/to_proto.rs index 7d979e9e..22fd67b3 100644 --- a/crates/lsp/src/to_proto.rs +++ b/crates/lsp/src/proto/to_proto.rs @@ -8,27 +8,24 @@ // Utilites for converting internal types to LSP types use anyhow::Context; -pub(crate) use rust_analyzer::to_proto::text_edit_vec; - -use crate::proto::PositionEncoding; -use crate::rust_analyzer::{self, line_index::LineIndex, text_edit::TextEdit}; +use biome_line_index::LineIndex; use biome_text_size::TextRange; use biome_text_size::TextSize; +use settings::LineEnding; use tower_lsp::lsp_types; -// TODO!: We use `rust_analyzer::LineIndex` here, but `biome_line_index::LineIndex` -// in `from_proto.rs`. We should use `biome_line_index::LineIndex` everywhere, and -// consider getting rid of `rust_analyzer::LineIndex` entirely. +use crate::proto::PositionEncoding; +use crate::text_edit::Indel; +use crate::text_edit::TextEdit; /// The function is used to convert TextSize to a LSP position. /// From `biome_lsp_converters::to_proto::position()`. -pub fn position( - line_index: &LineIndex, +pub(crate) fn position( offset: TextSize, + line_index: &LineIndex, position_encoding: PositionEncoding, ) -> anyhow::Result { let line_col = line_index - .index .line_col(offset) .with_context(|| format!("Could not convert offset {offset:?} into a line-column index"))?; @@ -36,7 +33,6 @@ pub fn position( PositionEncoding::Utf8 => lsp_types::Position::new(line_col.line, line_col.col), PositionEncoding::Wide(enc) => { let line_col = line_index - .index .to_wide(enc, line_col) .with_context(|| format!("Could not convert {line_col:?} into wide line column"))?; lsp_types::Position::new(line_col.line, line_col.col) @@ -48,22 +44,50 @@ pub fn position( /// The function is used to convert TextRange to a LSP range. /// From `biome_lsp_converters::to_proto::range()`. -pub fn range( - line_index: &LineIndex, +pub(crate) fn range( range: TextRange, + line_index: &LineIndex, position_encoding: PositionEncoding, ) -> anyhow::Result { - let start = position(line_index, range.start(), position_encoding)?; - let end = position(line_index, range.end(), position_encoding)?; + let start = position(range.start(), line_index, position_encoding)?; + let end = position(range.end(), line_index, position_encoding)?; Ok(lsp_types::Range::new(start, end)) } +pub(crate) fn text_edit( + indel: Indel, + line_index: &LineIndex, + position_encoding: PositionEncoding, + endings: LineEnding, +) -> anyhow::Result { + let range = range(indel.delete, line_index, position_encoding)?; + let new_text = match endings { + LineEnding::Lf => indel.insert, + LineEnding::Crlf => indel.insert.replace('\n', "\r\n"), + }; + Ok(lsp_types::TextEdit { range, new_text }) +} + +pub(crate) fn text_edit_vec( + text_edit: TextEdit, + line_index: &LineIndex, + position_encoding: PositionEncoding, + endings: LineEnding, +) -> anyhow::Result> { + text_edit + .into_iter() + .map(|indel| self::text_edit(indel, line_index, position_encoding, endings)) + .collect() +} + #[cfg(test)] pub(crate) fn doc_edit_vec( - line_index: &LineIndex, text_edit: TextEdit, + line_index: &LineIndex, + position_encoding: PositionEncoding, + endings: LineEnding, ) -> anyhow::Result> { - let edits = text_edit_vec(line_index, text_edit)?; + let edits = text_edit_vec(text_edit, line_index, position_encoding, endings)?; Ok(edits .into_iter() @@ -76,19 +100,23 @@ pub(crate) fn doc_edit_vec( } pub(crate) fn replace_range_edit( - line_index: &LineIndex, range: TextRange, replace_with: String, + line_index: &LineIndex, + position_encoding: PositionEncoding, + endings: LineEnding, ) -> anyhow::Result> { let edit = TextEdit::replace(range, replace_with); - text_edit_vec(line_index, edit) + text_edit_vec(edit, line_index, position_encoding, endings) } pub(crate) fn replace_all_edit( - line_index: &LineIndex, text: &str, replace_with: &str, + line_index: &LineIndex, + position_encoding: PositionEncoding, + endings: LineEnding, ) -> anyhow::Result> { - let edit = TextEdit::diff(text, replace_with); - text_edit_vec(line_index, edit) + let edit = crate::diff::diff(text, replace_with); + text_edit_vec(edit, line_index, position_encoding, endings) } diff --git a/crates/lsp/src/rust_analyzer/mod.rs b/crates/lsp/src/rust_analyzer/mod.rs deleted file mode 100644 index bfb231cc..00000000 --- a/crates/lsp/src/rust_analyzer/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod diff; -pub mod line_index; -pub mod text_edit; -pub mod to_proto; -pub mod utils; diff --git a/crates/lsp/src/rust_analyzer/to_proto.rs b/crates/lsp/src/rust_analyzer/to_proto.rs deleted file mode 100644 index e1ffd914..00000000 --- a/crates/lsp/src/rust_analyzer/to_proto.rs +++ /dev/null @@ -1,36 +0,0 @@ -// --- 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/to_proto.rs" -// --- - -//! Conversion of rust-analyzer specific types to lsp_types equivalents. - -use super::{ - line_index::LineIndex, - text_edit::{Indel, TextEdit}, -}; -use settings::LineEnding; -use tower_lsp::lsp_types; - -pub(crate) fn text_edit( - line_index: &LineIndex, - indel: Indel, -) -> anyhow::Result { - let range = crate::to_proto::range(line_index, indel.delete, line_index.encoding)?; - let new_text = match line_index.endings { - LineEnding::Lf => indel.insert, - LineEnding::Crlf => indel.insert.replace('\n', "\r\n"), - }; - Ok(lsp_types::TextEdit { range, new_text }) -} - -pub(crate) fn text_edit_vec( - line_index: &LineIndex, - text_edit: TextEdit, -) -> anyhow::Result> { - text_edit - .into_iter() - .map(|indel| self::text_edit(line_index, indel)) - .collect() -} diff --git a/crates/lsp/src/rust_analyzer/utils.rs b/crates/lsp/src/rust_analyzer/utils.rs deleted file mode 100644 index dcf4f7a9..00000000 --- a/crates/lsp/src/rust_analyzer/utils.rs +++ /dev/null @@ -1,56 +0,0 @@ -// --- 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/utils.rs" -// --- - -use std::ops::Range; - -use tower_lsp::lsp_types; - -use crate::from_proto; -use crate::proto::PositionEncoding; - -pub(crate) fn apply_document_changes( - encoding: PositionEncoding, - file_contents: &str, - mut content_changes: Vec, -) -> String { - // If at least one of the changes is a full document change, use the last - // of them as the starting point and ignore all previous changes. - let (mut text, content_changes) = match content_changes - .iter() - .rposition(|change| change.range.is_none()) - { - Some(idx) => { - let text = std::mem::take(&mut content_changes[idx].text); - (text, &content_changes[idx + 1..]) - } - None => (file_contents.to_owned(), &content_changes[..]), - }; - if content_changes.is_empty() { - return text; - } - - let mut line_index = biome_line_index::LineIndex::new(&text); - - // The changes we got must be applied sequentially, but can cross lines so we - // have to keep our line index updated. - // Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we - // remember the last valid line in the index and only rebuild it if needed. - // The VFS will normalize the end of lines to `\n`. - let mut index_valid = !0u32; - for change in content_changes { - // The None case can't happen as we have handled it above already - if let Some(range) = change.range { - if index_valid <= range.end.line { - line_index = biome_line_index::LineIndex::new(&text); - } - index_valid = range.start.line; - if let Ok(range) = from_proto::text_range(&line_index, range, encoding) { - text.replace_range(Range::::from(range), &change.text); - } - } - } - text -} diff --git a/crates/lsp/src/test/client_ext.rs b/crates/lsp/src/test/client_ext.rs index a7082fd2..0a3b8d70 100644 --- a/crates/lsp/src/test/client_ext.rs +++ b/crates/lsp/src/test/client_ext.rs @@ -2,7 +2,8 @@ use biome_text_size::TextRange; use lsp_test::lsp_client::TestClient; use tower_lsp::lsp_types; -use crate::{documents::Document, from_proto, to_proto}; +use crate::documents::Document; +use crate::proto::{from_proto, to_proto}; pub(crate) trait TestClientExt { async fn open_document( @@ -66,7 +67,7 @@ impl TestClientExt for TestClient { async fn format_document(&mut self, doc: &Document, filename: FileName) -> String { match self.format_document_edits(doc, filename).await { - Some(edits) => from_proto::apply_text_edits(doc, edits).unwrap(), + Some(edits) => apply_text_edits(doc, edits).unwrap(), None => doc.contents.clone(), } } @@ -80,7 +81,7 @@ impl TestClientExt for TestClient { let Some(edits) = self.format_document_range_edits(doc, filename, range).await else { return doc.contents.clone(); }; - from_proto::apply_text_edits(doc, edits).unwrap() + apply_text_edits(doc, edits).unwrap() } async fn format_document_edits( @@ -121,7 +122,7 @@ impl TestClientExt for TestClient { ) -> Option> { let lsp_doc = self.open_document(doc, filename).await; - let range = to_proto::range(&doc.line_index, range, doc.line_index.encoding).unwrap(); + let range = to_proto::range(range, &doc.line_index.index, doc.position_encoding).unwrap(); self.range_formatting(lsp_types::DocumentRangeFormattingParams { text_document: lsp_types::TextDocumentIdentifier { @@ -158,3 +159,22 @@ fn formatting_options(doc: &Document) -> lsp_types::FormattingOptions { ..Default::default() } } + +fn apply_text_edits(doc: &Document, mut edits: Vec) -> anyhow::Result { + let mut text = doc.contents.clone(); + + // Apply edits from bottom to top to avoid inserted newlines to invalidate + // positions in earlier parts of the doc (they are sent in reading order + // accorder to the LSP protocol) + edits.reverse(); + + for edit in edits { + let range = + from_proto::text_range(edit.range, &doc.line_index.index, doc.position_encoding)?; + let start: usize = range.start().into(); + let end: usize = range.end().into(); + text.replace_range(start..end, &edit.new_text); + } + + Ok(text) +} diff --git a/crates/lsp/src/rust_analyzer/text_edit.rs b/crates/lsp/src/text_edit.rs similarity index 98% rename from crates/lsp/src/rust_analyzer/text_edit.rs rename to crates/lsp/src/text_edit.rs index fa777e50..d91c836a 100644 --- a/crates/lsp/src/rust_analyzer/text_edit.rs +++ b/crates/lsp/src/text_edit.rs @@ -79,12 +79,6 @@ impl TextEdit { builder.finish() } - // --- Start Posit - pub fn diff(text: &str, replace_with: &str) -> TextEdit { - super::diff::diff(text, replace_with) - } - // --- End Posit - pub fn len(&self) -> usize { self.indels.len() }