Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
80 changes: 71 additions & 9 deletions crates/lsp/src/documents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -78,6 +83,8 @@ impl Document {
Self {
contents,
line_index,
endings,
position_encoding,
parse,
version,
settings: Default::default(),
Expand Down Expand Up @@ -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,
);
Expand All @@ -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<lsp_types::TextDocumentContentChangeEvent>,
) -> 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::<usize>::from(range), &change.text);
}
}
}
text
}

/// Convenient accessor that returns an annotated `SyntaxNode` type
pub fn syntax(&self) -> air_r_syntax::RSyntaxNode {
self.parse.syntax()
Expand All @@ -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::*;

Expand Down Expand Up @@ -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(),
Expand Down
17 changes: 12 additions & 5 deletions crates/lsp/src/handlers_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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))
}
Expand Down
6 changes: 3 additions & 3 deletions crates/lsp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<biome_line_index::LineIndex>,
Comment on lines 10 to 11
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to argue that we should drop this wrapper entirely at this point. We currently have no reason to wrap this in an Arc. I know rust-analyzer does this, but I think we should simplify things until we start to use salsa, and let the implementation built around salsa guide whether or not we should arc this.

pub endings: LineEnding,
pub encoding: PositionEncoding,
}
Comment on lines 9 to 12
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because each conversion function now takes the biome_line_index::LineIndex, it doesn't make sense to have a wrapper that holds these other fields too.

Instead the Document itself is in charge of holding these fields.

I think this makes a lot of sense, because we throw away and rebuild the LineIndex regularly, but the endings and encoding of the Document never changes and never needs to be updated.

This also becomes apparent in my next PR to optimize on_did_change() a bit.

5 changes: 5 additions & 0 deletions crates/lsp/src/proto.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<biome_text_size::TextSize> {
let line_col = match position_encoding {
Expand All @@ -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<biome_text_size::TextRange> {
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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only actually used by the client during testing, so it moved there.

doc: &Document,
mut edits: Vec<lsp_types::TextEdit>,
) -> anyhow::Result<String> {
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)
}
Loading