Skip to content

Commit ba30145

Browse files
authored
Update cursor position after applying text edits (autozimu#961)
* Update cursor position after applying text edits * Handle CRLF and add more tests * fix a typo
1 parent 7c741d0 commit ba30145

File tree

2 files changed

+210
-12
lines changed

2 files changed

+210
-12
lines changed

src/language_server_protocol.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,22 @@ impl LanguageClient {
304304
match changes {
305305
DocumentChanges::Edits(ref changes) => {
306306
for e in changes {
307-
self.apply_TextEdits(&e.text_document.uri.filepath()?, &e.edits)?;
307+
position = self.apply_TextEdits(
308+
&e.text_document.uri.filepath()?,
309+
&e.edits,
310+
position,
311+
)?;
308312
}
309313
}
310314
DocumentChanges::Operations(ref ops) => {
311315
for op in ops {
312316
match op {
313317
Edit(ref e) => {
314-
self.apply_TextEdits(&e.text_document.uri.filepath()?, &e.edits)?
318+
position = self.apply_TextEdits(
319+
&e.text_document.uri.filepath()?,
320+
&e.edits,
321+
position,
322+
)?
315323
}
316324
Op(ref rop) => match rop {
317325
Create(file) => {
@@ -327,7 +335,7 @@ impl LanguageClient {
327335
}
328336
} else if let Some(ref changes) = edit.changes {
329337
for (uri, edits) in changes {
330-
self.apply_TextEdits(&uri.filepath()?, edits)?;
338+
position = self.apply_TextEdits(&uri.filepath()?, edits, position)?;
331339
}
332340
}
333341
self.edit(&None, &filename)?;
@@ -458,10 +466,15 @@ impl LanguageClient {
458466
Ok(())
459467
}
460468

461-
fn apply_TextEdits<P: AsRef<Path>>(&self, path: P, edits: &[TextEdit]) -> Fallible<()> {
469+
fn apply_TextEdits<P: AsRef<Path>>(
470+
&self,
471+
path: P,
472+
edits: &[TextEdit],
473+
position: Position,
474+
) -> Fallible<Position> {
462475
debug!("Begin apply TextEdits: {:?}", edits);
463476
if edits.is_empty() {
464-
return Ok(());
477+
return Ok(position);
465478
}
466479

467480
let mut edits = edits.to_vec();
@@ -484,7 +497,7 @@ impl LanguageClient {
484497
lines.push("".to_owned());
485498
}
486499

487-
let mut lines = apply_TextEdits(&lines, &edits)?;
500+
let (mut lines, position) = apply_TextEdits(&lines, &edits, &position)?;
488501

489502
if lines.last().map(String::is_empty) == Some(true) && fixendofline {
490503
lines.pop();
@@ -495,7 +508,7 @@ impl LanguageClient {
495508
}
496509
self.vim()?.rpcclient.notify("setline", json!([1, lines]))?;
497510
debug!("End apply TextEdits");
498-
Ok(())
511+
Ok(position)
499512
}
500513

501514
fn update_quickfixlist(&self) -> Fallible<()> {
@@ -3106,7 +3119,7 @@ impl LanguageClient {
31063119
return Ok(());
31073120
}
31083121

3109-
self.apply_TextEdits(filename, &edits)?;
3122+
let position = self.apply_TextEdits(filename, &edits, position)?;
31103123
self.vim()?
31113124
.cursor(position.line + 1, position.character + 1)
31123125
}

src/utils.rs

Lines changed: 189 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,89 @@ impl<P: AsRef<Path> + std::fmt::Debug> ToUrl for P {
130130
}
131131
}
132132

133-
pub fn apply_TextEdits(lines: &[String], edits: &[TextEdit]) -> Fallible<Vec<String>> {
133+
fn position_to_offset(lines: &[String], position: &Position) -> usize {
134+
if lines.is_empty() {
135+
return 0;
136+
}
137+
138+
let line = std::cmp::min(position.line as usize, lines.len() - 1);
139+
let character = std::cmp::min(position.character as usize, lines[line].len());
140+
141+
let chars_above: usize = lines[..line].iter().map(|text| text.len() + 1).sum();
142+
chars_above + character
143+
}
144+
145+
#[test]
146+
fn test_position_to_offset() {
147+
assert_eq!(position_to_offset(&[], &Position::new(0, 0)), 0);
148+
149+
let lines: Vec<String> = "\n".lines().map(ToOwned::to_owned).collect();
150+
assert_eq!(position_to_offset(&lines, &Position::new(0, 0)), 0);
151+
assert_eq!(position_to_offset(&lines, &Position::new(0, 1)), 0);
152+
assert_eq!(position_to_offset(&lines, &Position::new(1, 0)), 0);
153+
assert_eq!(position_to_offset(&lines, &Position::new(1, 1)), 0);
154+
155+
let lines: Vec<String> = "a\n".lines().map(ToOwned::to_owned).collect();
156+
assert_eq!(position_to_offset(&lines, &Position::new(0, 0)), 0);
157+
assert_eq!(position_to_offset(&lines, &Position::new(0, 1)), 1);
158+
assert_eq!(position_to_offset(&lines, &Position::new(0, 2)), 1);
159+
assert_eq!(position_to_offset(&lines, &Position::new(1, 0)), 0);
160+
assert_eq!(position_to_offset(&lines, &Position::new(1, 1)), 1);
161+
assert_eq!(position_to_offset(&lines, &Position::new(1, 2)), 1);
162+
163+
let lines: Vec<String> = "a\nbc\n".lines().map(ToOwned::to_owned).collect();
164+
assert_eq!(position_to_offset(&lines, &Position::new(1, 0)), 2);
165+
assert_eq!(position_to_offset(&lines, &Position::new(1, 1)), 3);
166+
assert_eq!(position_to_offset(&lines, &Position::new(1, 2)), 4);
167+
assert_eq!(position_to_offset(&lines, &Position::new(1, 3)), 4);
168+
}
169+
170+
fn offset_to_position(lines: &[String], offset: usize) -> Position {
171+
if lines.is_empty() {
172+
return Position::new(0, 0);
173+
}
174+
175+
let mut offset = offset;
176+
for (line, text) in lines.iter().enumerate() {
177+
if offset <= text.len() {
178+
return Position::new(line as u64, offset as u64);
179+
}
180+
181+
offset -= text.len() + 1;
182+
}
183+
184+
let last_line = lines.len() - 1;
185+
let last_character = lines[last_line].len();
186+
Position::new(last_line as u64, last_character as u64)
187+
}
188+
189+
#[test]
190+
fn test_offset_to_position() {
191+
assert_eq!(offset_to_position(&[], 0), Position::new(0, 0));
192+
193+
let lines: Vec<String> = "\n".lines().map(ToOwned::to_owned).collect();
194+
assert_eq!(offset_to_position(&lines, 0), Position::new(0, 0));
195+
assert_eq!(offset_to_position(&lines, 1), Position::new(0, 0));
196+
197+
let lines: Vec<String> = "a\n".lines().map(ToOwned::to_owned).collect();
198+
assert_eq!(offset_to_position(&lines, 0), Position::new(0, 0));
199+
assert_eq!(offset_to_position(&lines, 1), Position::new(0, 1));
200+
assert_eq!(offset_to_position(&lines, 2), Position::new(0, 1));
201+
202+
let lines: Vec<String> = "a\nbc\n".lines().map(ToOwned::to_owned).collect();
203+
assert_eq!(offset_to_position(&lines, 0), Position::new(0, 0));
204+
assert_eq!(offset_to_position(&lines, 1), Position::new(0, 1));
205+
assert_eq!(offset_to_position(&lines, 2), Position::new(1, 0));
206+
assert_eq!(offset_to_position(&lines, 3), Position::new(1, 1));
207+
assert_eq!(offset_to_position(&lines, 4), Position::new(1, 2));
208+
assert_eq!(offset_to_position(&lines, 5), Position::new(1, 2));
209+
}
210+
211+
pub fn apply_TextEdits(
212+
lines: &[String],
213+
edits: &[TextEdit],
214+
position: &Position,
215+
) -> Fallible<(Vec<String>, Position)> {
134216
// Edits are ordered from bottom to top, from right to left.
135217
let mut edits_by_index = vec![];
136218
for edit in edits {
@@ -153,13 +235,32 @@ pub fn apply_TextEdits(lines: &[String], edits: &[TextEdit]) -> Fallible<Vec<Str
153235
}
154236

155237
let mut text = lines.join("\n");
238+
let mut offset = position_to_offset(&lines, &position);
156239
for (start, end, new_text) in edits_by_index {
157240
let start = std::cmp::min(start, text.len());
158241
let end = std::cmp::min(end, text.len());
159242
text = String::new() + &text[..start] + new_text + &text[end..];
243+
244+
// Update offset only if the edit's entire range is before it.
245+
// Edits after the offset do not affect it.
246+
// Edits covering the offset cause unpredictable effect.
247+
if end <= offset {
248+
offset += new_text.len();
249+
offset -= new_text.matches("\r\n").count(); // line ending is counted as one offset
250+
offset -= std::cmp::min(offset, end - start);
251+
}
160252
}
161253

162-
Ok(text.lines().map(ToOwned::to_owned).collect())
254+
offset = std::cmp::min(offset, text.len());
255+
256+
let new_lines: Vec<String> = text.lines().map(ToOwned::to_owned).collect();
257+
let new_position = offset_to_position(&new_lines, offset);
258+
debug!(
259+
"Position change after applying text edits: {:?} -> {:?}",
260+
position, new_position
261+
);
262+
263+
Ok((new_lines, new_position))
163264
}
164265

165266
#[test]
@@ -198,7 +299,12 @@ fn test_apply_TextEdit() {
198299
.to_owned(),
199300
};
200301

201-
assert_eq!(apply_TextEdits(&lines, &[edit]).unwrap(), expect);
302+
let position = Position::new(0, 0);
303+
304+
// Ignore returned position since the edit's range covers current position and the new position
305+
// is undefined in this case
306+
let (result, _) = apply_TextEdits(&lines, &[edit], &position).unwrap();
307+
assert_eq!(result, expect);
202308
}
203309

204310
#[test]
@@ -221,7 +327,86 @@ fn test_apply_TextEdit_overlong_end() {
221327
new_text: r#"nb = 123"#.to_owned(),
222328
};
223329

224-
assert_eq!(apply_TextEdits(&lines, &[edit]).unwrap(), expect);
330+
let position = Position::new(0, 1);
331+
332+
let (result, _) = apply_TextEdits(&lines, &[edit], &position).unwrap();
333+
assert_eq!(result, expect);
334+
}
335+
336+
#[test]
337+
fn test_apply_TextEdit_position() {
338+
let lines: Vec<String> = "abc = 123".lines().map(|l| l.to_owned()).collect();
339+
340+
let expected_lines: Vec<String> = "newline\nabcde = 123"
341+
.lines()
342+
.map(|l| l.to_owned())
343+
.collect();
344+
345+
let edits = [
346+
TextEdit {
347+
range: Range {
348+
start: Position {
349+
line: 0,
350+
character: 1,
351+
},
352+
end: Position {
353+
line: 0,
354+
character: 3,
355+
},
356+
},
357+
new_text: "bcde".to_owned(),
358+
},
359+
TextEdit {
360+
range: Range {
361+
start: Position {
362+
line: 0,
363+
character: 0,
364+
},
365+
end: Position {
366+
line: 0,
367+
character: 0,
368+
},
369+
},
370+
new_text: "newline\n".to_owned(),
371+
},
372+
];
373+
374+
let position = Position::new(0, 4);
375+
let expected_position = Position::new(1, 6);
376+
377+
assert_eq!(
378+
apply_TextEdits(&lines, &edits, &position).unwrap(),
379+
(expected_lines, expected_position)
380+
);
381+
}
382+
383+
#[test]
384+
fn test_apply_TextEdit_CRLF() {
385+
let lines: Vec<String> = "abc = 123".lines().map(|l| l.to_owned()).collect();
386+
387+
let expected_lines: Vec<String> = "a\r\nbc = 123".lines().map(|l| l.to_owned()).collect();
388+
389+
let edit = TextEdit {
390+
range: Range {
391+
start: Position {
392+
line: 0,
393+
character: 0,
394+
},
395+
end: Position {
396+
line: 0,
397+
character: 1,
398+
},
399+
},
400+
new_text: "a\r\n".to_owned(),
401+
};
402+
403+
let position = Position::new(0, 2);
404+
let expected_position = Position::new(1, 1);
405+
406+
assert_eq!(
407+
apply_TextEdits(&lines, &[edit], &position).unwrap(),
408+
(expected_lines, expected_position)
409+
);
225410
}
226411

227412
pub trait Combine {

0 commit comments

Comments
 (0)