Skip to content

Commit ddb5fdb

Browse files
committed
Optimize on_did_change() to rebuild LineIndex as few times as possible
1 parent 9becb0a commit ddb5fdb

File tree

3 files changed

+86
-99
lines changed

3 files changed

+86
-99
lines changed

crates/lsp/src/documents.rs

Lines changed: 77 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
//
66
//
77

8-
use settings::LineEnding;
98
use std::ops::Range;
9+
10+
use settings::LineEnding;
1011
use tower_lsp::lsp_types;
1112

1213
use crate::line_index::LineIndex;
@@ -73,9 +74,7 @@ impl Document {
7374
// select `LineEndings::Auto`, and then pass that to `LineIndex`.
7475

7576
// Create line index to keep track of newline offsets
76-
let line_index = LineIndex {
77-
index: triomphe::Arc::new(biome_line_index::LineIndex::new(&contents)),
78-
};
77+
let line_index = LineIndex::new(&contents);
7978

8079
// Parse document immediately for now
8180
let parse = air_r_parser::parse(&contents, Default::default());
@@ -103,9 +102,16 @@ impl Document {
103102
(doc, range)
104103
}
105104

106-
pub fn on_did_change(&mut self, mut params: lsp_types::DidChangeTextDocumentParams) {
107-
let new_version = params.text_document.version;
108-
105+
// --- source
106+
// authors = ["rust-analyzer team"]
107+
// license = "MIT OR Apache-2.0"
108+
// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/utils.rs"
109+
// ---
110+
pub fn on_did_change(
111+
&mut self,
112+
mut changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
113+
new_version: i32,
114+
) {
109115
// Check for out-of-order change notifications
110116
if let Some(old_version) = self.version {
111117
// According to the spec, versions might not be consecutive but they must be monotonically
@@ -119,78 +125,69 @@ impl Document {
119125
}
120126
}
121127

122-
// Normalize line endings. Changing the line length of inserted or
123-
// replaced text can't invalidate the text change events, even those
124-
// applied subsequently, since those changes are specified with [line,
125-
// col] coordinates.
126-
for event in &mut params.content_changes {
127-
let text = std::mem::take(&mut event.text);
128-
event.text = line_ending::normalize(text);
129-
}
130-
131-
let contents = Self::apply_document_changes(
132-
self.position_encoding,
133-
&self.contents,
134-
params.content_changes,
135-
);
136-
137-
// No incrementality for now
138-
let parse = air_r_parser::parse(&contents, Default::default());
128+
// If at least one of the changes is a full document change, use the last of them
129+
// as the starting point and ignore all previous changes. We then know that all
130+
// changes after this (if any!) are incremental changes.
131+
//
132+
// If we do have a full document change, that implies the `last_start_line`
133+
// corresponding to that change is line 0, which will correctly force a rebuild
134+
// of the line index before applying any incremental changes. We don't go ahead
135+
// and rebuild the line index here, because it is guaranteed to be rebuilt for
136+
// us on the way out.
137+
let (changes, mut last_start_line) =
138+
match changes.iter().rposition(|change| change.range.is_none()) {
139+
Some(idx) => {
140+
let incremental = changes.split_off(idx + 1);
141+
// Unwrap: `rposition()` confirmed this index contains a full document change
142+
let change = changes.pop().unwrap();
143+
self.contents = line_ending::normalize(change.text);
144+
(incremental, 0)
145+
}
146+
None => (changes, u32::MAX),
147+
};
148+
149+
// Handle all incremental changes after the last full document change. We don't
150+
// typically get >1 incremental change as the user types, but we do get them in a
151+
// batch after a find-and-replace, or after a format-on-save request.
152+
//
153+
// Some editors like VS Code send the edits in reverse order (from the bottom of
154+
// file -> top of file). We can take advantage of this, because applying an edit
155+
// on, say, line 10, doesn't invalidate the `line_index` if we then need to apply
156+
// an additional edit on line 5. That said, we may still have edits that cross
157+
// lines, so rebuilding the `line_index` is not always unavoidable.
158+
//
159+
// We also normalize line endings. Changing the line length of inserted or
160+
// replaced text can't invalidate the text change events since the location of the
161+
// change itself is specified with [line, col] coordinates, separate from the
162+
// actual contents of the change.
163+
for change in changes {
164+
let range = change
165+
.range
166+
.expect("`None` case already handled by finding the last full document change.");
167+
168+
// If the end of this change is at or past the start of the last change, then
169+
// the `line_index` needed to apply this change is now invalid, so we have to
170+
// rebuild it.
171+
if range.end.line >= last_start_line {
172+
self.line_index = LineIndex::new(&self.contents);
173+
}
174+
last_start_line = range.start.line;
139175

140-
self.parse = parse;
141-
self.contents = contents;
142-
self.line_index.index =
143-
triomphe::Arc::new(biome_line_index::LineIndex::new(&self.contents));
144-
self.version = Some(new_version);
145-
}
176+
// This is a panic if we can't convert. It means we can't keep the document up
177+
// to date and something is very wrong.
178+
let range: Range<usize> =
179+
from_proto::text_range(range, &self.line_index.index, self.position_encoding)
180+
.expect("Can convert `range` from `Position` to `TextRange`.")
181+
.into();
146182

147-
// --- source
148-
// authors = ["rust-analyzer team"]
149-
// license = "MIT OR Apache-2.0"
150-
// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/utils.rs"
151-
// ---
152-
fn apply_document_changes(
153-
encoding: PositionEncoding,
154-
file_contents: &str,
155-
mut content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
156-
) -> String {
157-
// If at least one of the changes is a full document change, use the last
158-
// of them as the starting point and ignore all previous changes.
159-
let (mut text, content_changes) = match content_changes
160-
.iter()
161-
.rposition(|change| change.range.is_none())
162-
{
163-
Some(idx) => {
164-
let text = std::mem::take(&mut content_changes[idx].text);
165-
(text, &content_changes[idx + 1..])
166-
}
167-
None => (file_contents.to_owned(), &content_changes[..]),
168-
};
169-
if content_changes.is_empty() {
170-
return text;
183+
self.contents
184+
.replace_range(range, &line_ending::normalize(change.text));
171185
}
172186

173-
let mut line_index = biome_line_index::LineIndex::new(&text);
174-
175-
// The changes we got must be applied sequentially, but can cross lines so we
176-
// have to keep our line index updated.
177-
// Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we
178-
// remember the last valid line in the index and only rebuild it if needed.
179-
// The VFS will normalize the end of lines to `\n`.
180-
let mut index_valid = !0u32;
181-
for change in content_changes {
182-
// The None case can't happen as we have handled it above already
183-
if let Some(range) = change.range {
184-
if index_valid <= range.end.line {
185-
line_index = biome_line_index::LineIndex::new(&text);
186-
}
187-
index_valid = range.start.line;
188-
if let Ok(range) = from_proto::text_range(range, &line_index, encoding) {
189-
text.replace_range(Range::<usize>::from(range), &change.text);
190-
}
191-
}
192-
}
193-
text
187+
// Rebuild the `line_index` after applying the final edit, and sync other fields
188+
self.line_index = LineIndex::new(&self.contents);
189+
self.parse = air_r_parser::parse(&self.contents, Default::default());
190+
self.version = Some(new_version);
194191
}
195192

196193
/// Convenient accessor that returns an annotated `SyntaxNode` type
@@ -209,13 +206,6 @@ mod tests {
209206

210207
use super::*;
211208

212-
fn dummy_versioned_doc() -> lsp_types::VersionedTextDocumentIdentifier {
213-
lsp_types::VersionedTextDocumentIdentifier {
214-
uri: url::Url::parse("file:///foo").unwrap(),
215-
version: 1,
216-
}
217-
}
218-
219209
#[test]
220210
fn test_document_starts_at_0_with_leading_whitespace() {
221211
let document = Document::doodle("\n\n# hi there");
@@ -244,12 +234,7 @@ mod tests {
244234
doc.endings,
245235
)
246236
.unwrap();
247-
248-
let params = lsp_types::DidChangeTextDocumentParams {
249-
text_document: dummy_versioned_doc(),
250-
content_changes: edits,
251-
};
252-
doc.on_did_change(params);
237+
doc.on_did_change(edits, 1);
253238

254239
let updated_syntax: RSyntaxNode = doc.parse.syntax();
255240
insta::assert_debug_snapshot!(updated_syntax);
@@ -281,33 +266,27 @@ mod tests {
281266
},
282267
};
283268

284-
let mut utf8_replace_params = lsp_types::DidChangeTextDocumentParams {
285-
text_document: dummy_versioned_doc(),
286-
content_changes: vec![],
287-
};
288-
let mut utf16_replace_params = utf8_replace_params.clone();
289-
290-
utf8_replace_params.content_changes = vec![lsp_types::TextDocumentContentChangeEvent {
269+
let utf8_content_changes = vec![lsp_types::TextDocumentContentChangeEvent {
291270
range: Some(utf8_range),
292271
range_length: None,
293272
text: String::from("bar"),
294273
}];
295-
utf16_replace_params.content_changes = vec![lsp_types::TextDocumentContentChangeEvent {
274+
let utf16_content_changes = vec![lsp_types::TextDocumentContentChangeEvent {
296275
range: Some(utf16_range),
297276
range_length: None,
298277
text: String::from("bar"),
299278
}];
300279

301280
let mut document = Document::new("a𐐀b".into(), None, PositionEncoding::Utf8);
302-
document.on_did_change(utf8_replace_params);
281+
document.on_did_change(utf8_content_changes, 1);
303282
assert_eq!(document.contents, "a𐐀bar");
304283

305284
let mut document = Document::new(
306285
"a𐐀b".into(),
307286
None,
308287
PositionEncoding::Wide(biome_line_index::WideEncoding::Utf16),
309288
);
310-
document.on_did_change(utf16_replace_params);
289+
document.on_did_change(utf16_content_changes, 1);
311290
assert_eq!(document.contents, "a𐐀bar");
312291
}
313292
}

crates/lsp/src/handlers_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ pub(crate) fn did_change(
168168
) -> anyhow::Result<()> {
169169
let uri = &params.text_document.uri;
170170
let doc = state.get_document_mut_or_error(uri)?;
171-
doc.on_did_change(params);
171+
doc.on_did_change(params.content_changes, params.text_document.version);
172172

173173
Ok(())
174174
}

crates/lsp/src/line_index.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ use triomphe::Arc;
1010
pub struct LineIndex {
1111
pub index: Arc<biome_line_index::LineIndex>,
1212
}
13+
14+
impl LineIndex {
15+
pub fn new(text: &str) -> Self {
16+
Self {
17+
index: Arc::new(biome_line_index::LineIndex::new(text)),
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)