Skip to content

Commit f54c313

Browse files
committed
Auto merge of rust-lang#13547 - Veykril:line-index, r=Veykril
internal: Optimize `apply_document_changes` a bit cc rust-lang/rust-analyzer#13538
2 parents 7742077 + 28afe57 commit f54c313

File tree

4 files changed

+117
-67
lines changed

4 files changed

+117
-67
lines changed

crates/ide-db/src/line_index.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ impl LineIndex {
5858
let mut utf16_lines = NoHashHashMap::default();
5959
let mut utf16_chars = Vec::new();
6060

61-
let mut newlines = vec![0.into()];
62-
let mut curr_row @ mut curr_col = 0.into();
61+
let mut newlines = Vec::with_capacity(16);
62+
newlines.push(TextSize::from(0));
63+
64+
let mut curr_row = 0.into();
65+
let mut curr_col = 0.into();
6366
let mut line = 0;
6467
for c in text.chars() {
6568
let c_len = TextSize::of(c);

crates/rust-analyzer/src/line_index.rs

+49-11
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ pub(crate) enum LineEndings {
2727
impl LineEndings {
2828
/// Replaces `\r\n` with `\n` in-place in `src`.
2929
pub(crate) fn normalize(src: String) -> (String, LineEndings) {
30-
if !src.as_bytes().contains(&b'\r') {
31-
return (src, LineEndings::Unix);
32-
}
33-
3430
// We replace `\r\n` with `\n` in-place, which doesn't break utf-8 encoding.
3531
// While we *can* call `as_mut_vec` and do surgery on the live string
3632
// directly, let's rather steal the contents of `src`. This makes the code
@@ -39,10 +35,19 @@ impl LineEndings {
3935
let mut buf = src.into_bytes();
4036
let mut gap_len = 0;
4137
let mut tail = buf.as_mut_slice();
38+
let mut crlf_seen = false;
39+
40+
let find_crlf = |src: &[u8]| src.windows(2).position(|it| it == b"\r\n");
41+
4242
loop {
4343
let idx = match find_crlf(&tail[gap_len..]) {
44-
None => tail.len(),
45-
Some(idx) => idx + gap_len,
44+
None if crlf_seen => tail.len(),
45+
// SAFETY: buf is unchanged and therefor still contains utf8 data
46+
None => return (unsafe { String::from_utf8_unchecked(buf) }, LineEndings::Unix),
47+
Some(idx) => {
48+
crlf_seen = true;
49+
idx + gap_len
50+
}
4651
};
4752
tail.copy_within(gap_len..idx, 0);
4853
tail = &mut tail[idx - gap_len..];
@@ -54,15 +59,48 @@ impl LineEndings {
5459

5560
// Account for removed `\r`.
5661
// After `set_len`, `buf` is guaranteed to contain utf-8 again.
57-
let new_len = buf.len() - gap_len;
5862
let src = unsafe {
63+
let new_len = buf.len() - gap_len;
5964
buf.set_len(new_len);
6065
String::from_utf8_unchecked(buf)
6166
};
62-
return (src, LineEndings::Dos);
67+
(src, LineEndings::Dos)
68+
}
69+
}
6370

64-
fn find_crlf(src: &[u8]) -> Option<usize> {
65-
src.windows(2).position(|it| it == b"\r\n")
66-
}
71+
#[cfg(test)]
72+
mod tests {
73+
use super::*;
74+
75+
#[test]
76+
fn unix() {
77+
let src = "a\nb\nc\n\n\n\n";
78+
let (res, endings) = LineEndings::normalize(src.into());
79+
assert_eq!(endings, LineEndings::Unix);
80+
assert_eq!(res, src);
81+
}
82+
83+
#[test]
84+
fn dos() {
85+
let src = "\r\na\r\n\r\nb\r\nc\r\n\r\n\r\n\r\n";
86+
let (res, endings) = LineEndings::normalize(src.into());
87+
assert_eq!(endings, LineEndings::Dos);
88+
assert_eq!(res, "\na\n\nb\nc\n\n\n\n");
89+
}
90+
91+
#[test]
92+
fn mixed() {
93+
let src = "a\r\nb\r\nc\r\n\n\r\n\n";
94+
let (res, endings) = LineEndings::normalize(src.into());
95+
assert_eq!(endings, LineEndings::Dos);
96+
assert_eq!(res, "a\nb\nc\n\n\n\n");
97+
}
98+
99+
#[test]
100+
fn none() {
101+
let src = "abc";
102+
let (res, endings) = LineEndings::normalize(src.into());
103+
assert_eq!(endings, LineEndings::Unix);
104+
assert_eq!(res, src);
67105
}
68106
}

crates/rust-analyzer/src/lsp_utils.rs

+59-52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! Utilities for LSP-related boilerplate code.
2-
use std::{ops::Range, sync::Arc};
2+
use std::{mem, ops::Range, sync::Arc};
33

44
use lsp_server::Notification;
55

@@ -133,11 +133,37 @@ impl GlobalState {
133133
}
134134

135135
pub(crate) fn apply_document_changes(
136-
old_text: &mut String,
137-
content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
138-
) {
136+
file_contents: impl FnOnce() -> String,
137+
mut content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
138+
) -> String {
139+
// Skip to the last full document change, as it invalidates all previous changes anyways.
140+
let mut start = content_changes
141+
.iter()
142+
.rev()
143+
.position(|change| change.range.is_none())
144+
.map(|idx| content_changes.len() - idx - 1)
145+
.unwrap_or(0);
146+
147+
let mut text: String = match content_changes.get_mut(start) {
148+
// peek at the first content change as an optimization
149+
Some(lsp_types::TextDocumentContentChangeEvent { range: None, text, .. }) => {
150+
let text = mem::take(text);
151+
start += 1;
152+
153+
// The only change is a full document update
154+
if start == content_changes.len() {
155+
return text;
156+
}
157+
text
158+
}
159+
Some(_) => file_contents(),
160+
// we received no content changes
161+
None => return file_contents(),
162+
};
163+
139164
let mut line_index = LineIndex {
140-
index: Arc::new(ide::LineIndex::new(old_text)),
165+
// the index will be overwritten in the bottom loop's first iteration
166+
index: Arc::new(ide::LineIndex::new(&text)),
141167
// We don't care about line endings or offset encoding here.
142168
endings: LineEndings::Unix,
143169
encoding: PositionEncoding::Utf16,
@@ -148,38 +174,20 @@ pub(crate) fn apply_document_changes(
148174
// Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we
149175
// remember the last valid line in the index and only rebuild it if needed.
150176
// The VFS will normalize the end of lines to `\n`.
151-
enum IndexValid {
152-
All,
153-
UpToLineExclusive(u32),
154-
}
155-
156-
impl IndexValid {
157-
fn covers(&self, line: u32) -> bool {
158-
match *self {
159-
IndexValid::UpToLineExclusive(to) => to > line,
160-
_ => true,
161-
}
162-
}
163-
}
164-
165-
let mut index_valid = IndexValid::All;
177+
let mut index_valid = !0u32;
166178
for change in content_changes {
167-
match change.range {
168-
Some(range) => {
169-
if !index_valid.covers(range.end.line) {
170-
line_index.index = Arc::new(ide::LineIndex::new(old_text));
171-
}
172-
index_valid = IndexValid::UpToLineExclusive(range.start.line);
173-
if let Ok(range) = from_proto::text_range(&line_index, range) {
174-
old_text.replace_range(Range::<usize>::from(range), &change.text);
175-
}
179+
// The None case can't happen as we have handled it above already
180+
if let Some(range) = change.range {
181+
if index_valid <= range.end.line {
182+
*Arc::make_mut(&mut line_index.index) = ide::LineIndex::new(&text);
176183
}
177-
None => {
178-
*old_text = change.text;
179-
index_valid = IndexValid::UpToLineExclusive(0);
184+
index_valid = range.start.line;
185+
if let Ok(range) = from_proto::text_range(&line_index, range) {
186+
text.replace_range(Range::<usize>::from(range), &change.text);
180187
}
181188
}
182189
}
190+
text
183191
}
184192

185193
/// Checks that the edits inside the completion and the additional edits do not overlap.
@@ -242,51 +250,50 @@ mod tests {
242250
};
243251
}
244252

245-
let mut text = String::new();
246-
apply_document_changes(&mut text, vec![]);
253+
let text = apply_document_changes(|| String::new(), vec![]);
247254
assert_eq!(text, "");
248-
apply_document_changes(
249-
&mut text,
255+
let text = apply_document_changes(
256+
|| text,
250257
vec![TextDocumentContentChangeEvent {
251258
range: None,
252259
range_length: None,
253260
text: String::from("the"),
254261
}],
255262
);
256263
assert_eq!(text, "the");
257-
apply_document_changes(&mut text, c![0, 3; 0, 3 => " quick"]);
264+
let text = apply_document_changes(|| text, c![0, 3; 0, 3 => " quick"]);
258265
assert_eq!(text, "the quick");
259-
apply_document_changes(&mut text, c![0, 0; 0, 4 => "", 0, 5; 0, 5 => " foxes"]);
266+
let text = apply_document_changes(|| text, c![0, 0; 0, 4 => "", 0, 5; 0, 5 => " foxes"]);
260267
assert_eq!(text, "quick foxes");
261-
apply_document_changes(&mut text, c![0, 11; 0, 11 => "\ndream"]);
268+
let text = apply_document_changes(|| text, c![0, 11; 0, 11 => "\ndream"]);
262269
assert_eq!(text, "quick foxes\ndream");
263-
apply_document_changes(&mut text, c![1, 0; 1, 0 => "have "]);
270+
let text = apply_document_changes(|| text, c![1, 0; 1, 0 => "have "]);
264271
assert_eq!(text, "quick foxes\nhave dream");
265-
apply_document_changes(
266-
&mut text,
272+
let text = apply_document_changes(
273+
|| text,
267274
c![0, 0; 0, 0 => "the ", 1, 4; 1, 4 => " quiet", 1, 16; 1, 16 => "s\n"],
268275
);
269276
assert_eq!(text, "the quick foxes\nhave quiet dreams\n");
270-
apply_document_changes(&mut text, c![0, 15; 0, 15 => "\n", 2, 17; 2, 17 => "\n"]);
277+
let text = apply_document_changes(|| text, c![0, 15; 0, 15 => "\n", 2, 17; 2, 17 => "\n"]);
271278
assert_eq!(text, "the quick foxes\n\nhave quiet dreams\n\n");
272-
apply_document_changes(
273-
&mut text,
279+
let text = apply_document_changes(
280+
|| text,
274281
c![1, 0; 1, 0 => "DREAM", 2, 0; 2, 0 => "they ", 3, 0; 3, 0 => "DON'T THEY?"],
275282
);
276283
assert_eq!(text, "the quick foxes\nDREAM\nthey have quiet dreams\nDON'T THEY?\n");
277-
apply_document_changes(&mut text, c![0, 10; 1, 5 => "", 2, 0; 2, 12 => ""]);
284+
let text = apply_document_changes(|| text, c![0, 10; 1, 5 => "", 2, 0; 2, 12 => ""]);
278285
assert_eq!(text, "the quick \nthey have quiet dreams\n");
279286

280-
text = String::from("❤️");
281-
apply_document_changes(&mut text, c![0, 0; 0, 0 => "a"]);
287+
let text = String::from("❤️");
288+
let text = apply_document_changes(|| text, c![0, 0; 0, 0 => "a"]);
282289
assert_eq!(text, "a❤️");
283290

284-
text = String::from("a\nb");
285-
apply_document_changes(&mut text, c![0, 1; 1, 0 => "\nțc", 0, 1; 1, 1 => "d"]);
291+
let text = String::from("a\nb");
292+
let text = apply_document_changes(|| text, c![0, 1; 1, 0 => "\nțc", 0, 1; 1, 1 => "d"]);
286293
assert_eq!(text, "adcb");
287294

288-
text = String::from("a\nb");
289-
apply_document_changes(&mut text, c![0, 1; 1, 0 => \nc", 0, 2; 0, 2 => "c"]);
295+
let text = String::from("a\nb");
296+
let text = apply_document_changes(|| text, c![0, 1; 1, 0 => \nc", 0, 2; 0, 2 => "c"]);
290297
assert_eq!(text, "ațc\ncb");
291298
}
292299

crates/rust-analyzer/src/main_loop.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,10 @@ impl GlobalState {
759759

760760
let vfs = &mut this.vfs.write().0;
761761
let file_id = vfs.file_id(&path).unwrap();
762-
let mut text = String::from_utf8(vfs.file_contents(file_id).to_vec()).unwrap();
763-
apply_document_changes(&mut text, params.content_changes);
762+
let text = apply_document_changes(
763+
|| std::str::from_utf8(vfs.file_contents(file_id)).unwrap().into(),
764+
params.content_changes,
765+
);
764766

765767
vfs.set_file_contents(path, Some(text.into_bytes()));
766768
}

0 commit comments

Comments
 (0)