Skip to content

Commit 23c8896

Browse files
bors[bot]lnicola
andauthored
Merge #4153
4153: Add support for incremental text synchronization r=matklad a=lnicola Fixes #3762. This still needs a `ra_vfs` PR, but I want to know I'm on the right track. I tested the change and it didn't crash horribly, but YMMV. Co-authored-by: Laurențiu Nicola <lnicola@dend.ro>
2 parents 745bd45 + 1a2d4e2 commit 23c8896

File tree

4 files changed

+125
-13
lines changed

4 files changed

+125
-13
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rust-analyzer/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ ra_prof = { path = "../ra_prof" }
3939
ra_project_model = { path = "../ra_project_model" }
4040
ra_syntax = { path = "../ra_syntax" }
4141
ra_text_edit = { path = "../ra_text_edit" }
42-
ra_vfs = "0.5.2"
42+
ra_vfs = "0.6.0"
4343

4444
# This should only be used in CLI
4545
ra_db = { path = "../ra_db" }

crates/rust-analyzer/src/caps.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub fn server_capabilities() -> ServerCapabilities {
1616
ServerCapabilities {
1717
text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
1818
open_close: Some(true),
19-
change: Some(TextDocumentSyncKind::Full),
19+
change: Some(TextDocumentSyncKind::Incremental),
2020
will_save: None,
2121
will_save_wait_until: None,
2222
save: Some(SaveOptions::default()),

crates/rust-analyzer/src/main_loop.rs

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ mod subscriptions;
66
pub(crate) mod pending_requests;
77

88
use std::{
9+
borrow::Cow,
910
env,
1011
error::Error,
11-
fmt, panic,
12+
fmt,
13+
ops::Range,
14+
panic,
1215
path::PathBuf,
1316
sync::Arc,
1417
time::{Duration, Instant},
@@ -18,11 +21,12 @@ use crossbeam_channel::{never, select, unbounded, RecvError, Sender};
1821
use itertools::Itertools;
1922
use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response};
2023
use lsp_types::{
21-
NumberOrString, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams,
22-
WorkDoneProgressEnd, WorkDoneProgressReport,
24+
DidChangeTextDocumentParams, NumberOrString, TextDocumentContentChangeEvent, WorkDoneProgress,
25+
WorkDoneProgressBegin, WorkDoneProgressCreateParams, WorkDoneProgressEnd,
26+
WorkDoneProgressReport,
2327
};
2428
use ra_flycheck::{url_from_path_with_drive_lowercasing, CheckTask};
25-
use ra_ide::{Canceled, FileId, LibraryData, SourceRootId};
29+
use ra_ide::{Canceled, FileId, LibraryData, LineIndex, SourceRootId};
2630
use ra_prof::profile;
2731
use ra_project_model::{PackageRoot, ProjectWorkspace};
2832
use ra_vfs::{VfsFile, VfsTask, Watch};
@@ -33,6 +37,7 @@ use threadpool::ThreadPool;
3337

3438
use crate::{
3539
config::{Config, FilesWatcher},
40+
conv::{ConvWith, TryConvWith},
3641
diagnostics::DiagnosticTask,
3742
main_loop::{
3843
pending_requests::{PendingRequest, PendingRequests},
@@ -579,12 +584,16 @@ fn on_notification(
579584
Err(not) => not,
580585
};
581586
let not = match notification_cast::<req::DidChangeTextDocument>(not) {
582-
Ok(mut params) => {
583-
let uri = params.text_document.uri;
587+
Ok(params) => {
588+
let DidChangeTextDocumentParams { text_document, content_changes } = params;
589+
let world = state.snapshot();
590+
let file_id = text_document.try_conv_with(&world)?;
591+
let line_index = world.analysis().file_line_index(file_id)?;
592+
let uri = text_document.uri;
584593
let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
585-
let text =
586-
params.content_changes.pop().ok_or_else(|| "empty changes".to_string())?.text;
587-
state.vfs.write().change_file_overlay(path.as_path(), text);
594+
state.vfs.write().change_file_overlay(&path, |old_text| {
595+
apply_document_changes(old_text, Cow::Borrowed(&line_index), content_changes);
596+
});
588597
return Ok(());
589598
}
590599
Err(not) => not,
@@ -653,6 +662,48 @@ fn on_notification(
653662
Ok(())
654663
}
655664

665+
fn apply_document_changes(
666+
old_text: &mut String,
667+
mut line_index: Cow<'_, LineIndex>,
668+
content_changes: Vec<TextDocumentContentChangeEvent>,
669+
) {
670+
// The changes we got must be applied sequentially, but can cross lines so we
671+
// have to keep our line index updated.
672+
// Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we
673+
// remember the last valid line in the index and only rebuild it if needed.
674+
enum IndexValid {
675+
All,
676+
UpToLine(u64),
677+
}
678+
679+
impl IndexValid {
680+
fn covers(&self, line: u64) -> bool {
681+
match *self {
682+
IndexValid::UpToLine(to) => to >= line,
683+
_ => true,
684+
}
685+
}
686+
}
687+
688+
let mut index_valid = IndexValid::All;
689+
for change in content_changes {
690+
match change.range {
691+
Some(range) => {
692+
if !index_valid.covers(range.start.line) {
693+
line_index = Cow::Owned(LineIndex::new(&old_text));
694+
}
695+
index_valid = IndexValid::UpToLine(range.start.line);
696+
let range = range.conv_with(&line_index);
697+
old_text.replace_range(Range::<usize>::from(range), &change.text);
698+
}
699+
None => {
700+
*old_text = change.text;
701+
index_valid = IndexValid::UpToLine(0);
702+
}
703+
}
704+
}
705+
}
706+
656707
fn on_check_task(
657708
task: CheckTask,
658709
world_state: &mut WorldState,
@@ -958,3 +1009,64 @@ where
9581009
{
9591010
Request::new(id, R::METHOD.to_string(), params)
9601011
}
1012+
1013+
#[cfg(test)]
1014+
mod tests {
1015+
use std::borrow::Cow;
1016+
1017+
use lsp_types::{Position, Range, TextDocumentContentChangeEvent};
1018+
use ra_ide::LineIndex;
1019+
1020+
#[test]
1021+
fn apply_document_changes() {
1022+
fn run(text: &mut String, changes: Vec<TextDocumentContentChangeEvent>) {
1023+
let line_index = Cow::Owned(LineIndex::new(&text));
1024+
super::apply_document_changes(text, line_index, changes);
1025+
}
1026+
1027+
macro_rules! c {
1028+
[$($sl:expr, $sc:expr; $el:expr, $ec:expr => $text:expr),+] => {
1029+
vec![$(TextDocumentContentChangeEvent {
1030+
range: Some(Range {
1031+
start: Position { line: $sl, character: $sc },
1032+
end: Position { line: $el, character: $ec },
1033+
}),
1034+
range_length: None,
1035+
text: String::from($text),
1036+
}),+]
1037+
};
1038+
}
1039+
1040+
let mut text = String::new();
1041+
run(&mut text, vec![]);
1042+
assert_eq!(text, "");
1043+
run(
1044+
&mut text,
1045+
vec![TextDocumentContentChangeEvent {
1046+
range: None,
1047+
range_length: None,
1048+
text: String::from("the"),
1049+
}],
1050+
);
1051+
assert_eq!(text, "the");
1052+
run(&mut text, c![0, 3; 0, 3 => " quick"]);
1053+
assert_eq!(text, "the quick");
1054+
run(&mut text, c![0, 0; 0, 4 => "", 0, 5; 0, 5 => " foxes"]);
1055+
assert_eq!(text, "quick foxes");
1056+
run(&mut text, c![0, 11; 0, 11 => "\ndream"]);
1057+
assert_eq!(text, "quick foxes\ndream");
1058+
run(&mut text, c![1, 0; 1, 0 => "have "]);
1059+
assert_eq!(text, "quick foxes\nhave dream");
1060+
run(&mut text, c![0, 0; 0, 0 => "the ", 1, 4; 1, 4 => " quiet", 1, 16; 1, 16 => "s\n"]);
1061+
assert_eq!(text, "the quick foxes\nhave quiet dreams\n");
1062+
run(&mut text, c![0, 15; 0, 15 => "\n", 2, 17; 2, 17 => "\n"]);
1063+
assert_eq!(text, "the quick foxes\n\nhave quiet dreams\n\n");
1064+
run(
1065+
&mut text,
1066+
c![1, 0; 1, 0 => "DREAM", 2, 0; 2, 0 => "they ", 3, 0; 3, 0 => "DON'T THEY?"],
1067+
);
1068+
assert_eq!(text, "the quick foxes\nDREAM\nthey have quiet dreams\nDON'T THEY?\n");
1069+
run(&mut text, c![0, 10; 1, 5 => "", 2, 0; 2, 12 => ""]);
1070+
assert_eq!(text, "the quick \nthey have quiet dreams\n");
1071+
}
1072+
}

0 commit comments

Comments
 (0)