Skip to content

Commit bdb651b

Browse files
committed
Move apply_content_changes() to own file
1 parent c98a59b commit bdb651b

3 files changed

Lines changed: 121 additions & 118 deletions

File tree

crates/ark/src/lsp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod code_action;
1212
pub mod comm;
1313
pub mod completions;
1414
mod config;
15+
mod content_changes;
1516
pub(crate) mod db;
1617
mod declarations;
1718
pub mod diagnostics;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use aether_lsp_utils::proto::from_proto;
2+
use aether_lsp_utils::proto::PositionEncoding;
3+
use tower_lsp::lsp_types;
4+
5+
// --- source
6+
// authors = ["rust-analyzer team"]
7+
// license = "MIT OR Apache-2.0"
8+
// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/utils.rs"
9+
// ---
10+
/// Apply a batch of LSP content changes to `contents`, returning the new text.
11+
pub(crate) fn apply_content_changes(
12+
contents: &str,
13+
content_changes: &[lsp_types::TextDocumentContentChangeEvent],
14+
encoding: PositionEncoding,
15+
) -> String {
16+
let mut contents = contents.to_string();
17+
let mut changes = content_changes.to_vec();
18+
19+
// If at least one of the changes is a full document change, use the last of them
20+
// as the starting point and ignore all previous changes. We then know that all
21+
// changes after this (if any!) are incremental changes.
22+
//
23+
// If we do have a full document change, that implies the `last_start_line`
24+
// corresponding to that change is line 0, which will correctly force a rebuild
25+
// of the line index before applying any incremental changes.
26+
let (changes, mut last_start_line) =
27+
match changes.iter().rposition(|change| change.range.is_none()) {
28+
Some(idx) => {
29+
let incremental = changes.split_off(idx + 1);
30+
// Unwrap: `rposition()` confirmed this index contains a full document change
31+
let change = changes.pop().unwrap();
32+
contents = change.text;
33+
(incremental, 0)
34+
},
35+
None => (changes, u32::MAX),
36+
};
37+
38+
let mut line_index = biome_line_index::LineIndex::new(&contents);
39+
40+
// Handle all incremental changes after the last full document change. We don't
41+
// typically get >1 incremental change as the user types, but we do get them in a
42+
// batch after a find-and-replace, or after a format-on-save request.
43+
//
44+
// Some editors like VS Code send the edits in reverse order (from the bottom of
45+
// file -> top of file). We can take advantage of this, because applying an edit
46+
// on, say, line 10, doesn't invalidate the `line_index` if we then need to apply
47+
// an additional edit on line 5. That said, we may still have edits that cross
48+
// lines, so rebuilding the `line_index` is not always unavoidable.
49+
for change in changes {
50+
let range = change
51+
.range
52+
.expect("`None` case already handled by finding the last full document change.");
53+
54+
// If the end of this change is at or past the start of the last change, then
55+
// the `line_index` needed to apply this change is now invalid, so we have to
56+
// rebuild it.
57+
if range.end.line >= last_start_line {
58+
line_index = biome_line_index::LineIndex::new(&contents);
59+
}
60+
last_start_line = range.start.line;
61+
62+
// This is a panic if we can't convert. It means we can't keep the document up
63+
// to date and something is very wrong.
64+
let range: std::ops::Range<usize> = from_proto::text_range(range, &line_index, encoding)
65+
.expect("Can convert `range` from `Position` to `TextRange`.")
66+
.into();
67+
68+
contents.replace_range(range, &change.text);
69+
}
70+
71+
contents
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use biome_line_index::WideEncoding;
77+
78+
use super::*;
79+
80+
const ENCODING: PositionEncoding = PositionEncoding::Wide(WideEncoding::Utf16);
81+
82+
fn insert(text: &str, line: u32, character: u32) -> lsp_types::TextDocumentContentChangeEvent {
83+
let position = lsp_types::Position::new(line, character);
84+
lsp_types::TextDocumentContentChangeEvent {
85+
range: Some(lsp_types::Range::new(position, position)),
86+
range_length: None,
87+
text: text.to_string(),
88+
}
89+
}
90+
91+
#[test]
92+
fn test_apply_content_changes_incremental_inserts() {
93+
// Type "lib" one character at a time, the way an editor streams it.
94+
let after_l = apply_content_changes("", &[insert("l", 0, 0)], ENCODING);
95+
assert_eq!(after_l, "l");
96+
97+
let after_i = apply_content_changes(&after_l, &[insert("i", 0, 1)], ENCODING);
98+
assert_eq!(after_i, "li");
99+
100+
let after_b = apply_content_changes(&after_i, &[insert("b", 0, 2)], ENCODING);
101+
assert_eq!(after_b, "lib");
102+
}
103+
104+
#[test]
105+
fn test_apply_content_changes_full_replacement_wins() {
106+
// A range-less change replaces the whole buffer; earlier changes in the
107+
// batch are discarded, later incremental ones apply on top of it.
108+
let changes = vec![
109+
insert("ignored", 0, 0),
110+
lsp_types::TextDocumentContentChangeEvent {
111+
range: None,
112+
range_length: None,
113+
text: "abc\n".to_string(),
114+
},
115+
insert("X", 0, 3),
116+
];
117+
assert_eq!(apply_content_changes("old", &changes, ENCODING), "abcX\n");
118+
}
119+
}

crates/ark/src/lsp/state_handlers.rs

Lines changed: 1 addition & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
use std::collections::HashSet;
99
use std::path::PathBuf;
1010

11-
use aether_lsp_utils::proto::from_proto;
12-
use aether_lsp_utils::proto::PositionEncoding;
1311
use aether_path::FilePath;
1412
use anyhow::anyhow;
1513
use oak_scan::DbScan;
@@ -56,6 +54,7 @@ use crate::lsp::capabilities::Capabilities;
5654
use crate::lsp::config::indent_style_from_lsp;
5755
use crate::lsp::config::DOCUMENT_SETTINGS;
5856
use crate::lsp::config::GLOBAL_SETTINGS;
57+
use crate::lsp::content_changes::apply_content_changes;
5958
use crate::lsp::inputs::source_root::SourceRoot;
6059
use crate::lsp::main_loop::dispatch_scan_requests;
6160
use crate::lsp::main_loop::DidCloseVirtualDocumentParams;
@@ -285,75 +284,6 @@ pub(crate) fn did_change(
285284
Ok(())
286285
}
287286

288-
// --- source
289-
// authors = ["rust-analyzer team"]
290-
// license = "MIT OR Apache-2.0"
291-
// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/utils.rs"
292-
// ---
293-
/// Apply a batch of LSP content changes to `contents`, returning the new text.
294-
fn apply_content_changes(
295-
contents: &str,
296-
content_changes: &[lsp_types::TextDocumentContentChangeEvent],
297-
encoding: PositionEncoding,
298-
) -> String {
299-
let mut contents = contents.to_string();
300-
let mut changes = content_changes.to_vec();
301-
302-
// If at least one of the changes is a full document change, use the last of them
303-
// as the starting point and ignore all previous changes. We then know that all
304-
// changes after this (if any!) are incremental changes.
305-
//
306-
// If we do have a full document change, that implies the `last_start_line`
307-
// corresponding to that change is line 0, which will correctly force a rebuild
308-
// of the line index before applying any incremental changes.
309-
let (changes, mut last_start_line) =
310-
match changes.iter().rposition(|change| change.range.is_none()) {
311-
Some(idx) => {
312-
let incremental = changes.split_off(idx + 1);
313-
// Unwrap: `rposition()` confirmed this index contains a full document change
314-
let change = changes.pop().unwrap();
315-
contents = change.text;
316-
(incremental, 0)
317-
},
318-
None => (changes, u32::MAX),
319-
};
320-
321-
let mut line_index = biome_line_index::LineIndex::new(&contents);
322-
323-
// Handle all incremental changes after the last full document change. We don't
324-
// typically get >1 incremental change as the user types, but we do get them in a
325-
// batch after a find-and-replace, or after a format-on-save request.
326-
//
327-
// Some editors like VS Code send the edits in reverse order (from the bottom of
328-
// file -> top of file). We can take advantage of this, because applying an edit
329-
// on, say, line 10, doesn't invalidate the `line_index` if we then need to apply
330-
// an additional edit on line 5. That said, we may still have edits that cross
331-
// lines, so rebuilding the `line_index` is not always unavoidable.
332-
for change in changes {
333-
let range = change
334-
.range
335-
.expect("`None` case already handled by finding the last full document change.");
336-
337-
// If the end of this change is at or past the start of the last change, then
338-
// the `line_index` needed to apply this change is now invalid, so we have to
339-
// rebuild it.
340-
if range.end.line >= last_start_line {
341-
line_index = biome_line_index::LineIndex::new(&contents);
342-
}
343-
last_start_line = range.start.line;
344-
345-
// This is a panic if we can't convert. It means we can't keep the document up
346-
// to date and something is very wrong.
347-
let range: std::ops::Range<usize> = from_proto::text_range(range, &line_index, encoding)
348-
.expect("Can convert `range` from `Position` to `TextRange`.")
349-
.into();
350-
351-
contents.replace_range(range, &change.text);
352-
}
353-
354-
contents
355-
}
356-
357287
#[tracing::instrument(level = "info", skip_all)]
358288
pub(crate) fn did_close(
359289
params: DidCloseTextDocumentParams,
@@ -618,50 +548,3 @@ pub(crate) fn did_close_virtual_document(
618548
state.virtual_documents.remove(&params.uri);
619549
Ok(())
620550
}
621-
622-
#[cfg(test)]
623-
mod tests {
624-
use biome_line_index::WideEncoding;
625-
626-
use super::*;
627-
628-
const ENCODING: PositionEncoding = PositionEncoding::Wide(WideEncoding::Utf16);
629-
630-
fn insert(text: &str, line: u32, character: u32) -> lsp_types::TextDocumentContentChangeEvent {
631-
let position = lsp_types::Position::new(line, character);
632-
lsp_types::TextDocumentContentChangeEvent {
633-
range: Some(lsp_types::Range::new(position, position)),
634-
range_length: None,
635-
text: text.to_string(),
636-
}
637-
}
638-
639-
#[test]
640-
fn test_apply_content_changes_incremental_inserts() {
641-
// Type "lib" one character at a time, the way an editor streams it.
642-
let after_l = apply_content_changes("", &[insert("l", 0, 0)], ENCODING);
643-
assert_eq!(after_l, "l");
644-
645-
let after_i = apply_content_changes(&after_l, &[insert("i", 0, 1)], ENCODING);
646-
assert_eq!(after_i, "li");
647-
648-
let after_b = apply_content_changes(&after_i, &[insert("b", 0, 2)], ENCODING);
649-
assert_eq!(after_b, "lib");
650-
}
651-
652-
#[test]
653-
fn test_apply_content_changes_full_replacement_wins() {
654-
// A range-less change replaces the whole buffer; earlier changes in the
655-
// batch are discarded, later incremental ones apply on top of it.
656-
let changes = vec![
657-
insert("ignored", 0, 0),
658-
lsp_types::TextDocumentContentChangeEvent {
659-
range: None,
660-
range_length: None,
661-
text: "abc\n".to_string(),
662-
},
663-
insert("X", 0, 3),
664-
];
665-
assert_eq!(apply_content_changes("old", &changes, ENCODING), "abcX\n");
666-
}
667-
}

0 commit comments

Comments
 (0)