vim: Multiline operation improvements (#24518)
Closes #15711 Discussed changes to match neovim in https://github.com/zed-industries/zed/pull/24481#issuecomment-2644504695 -- `vi{` matches neovim with treesitter instead of vanilla neovim. Change and delete matches standard neovim. Not sure if this is the best way to do it, implemented post processing to change and delete objects. I think another way would be adjust the range to trim the trailing newline char on change and delete operations, instead of having to add it back. ||Before|After| |---|---|---| |initial||| | `vi{` |  |  | | `ci{` |  |  | | `di{` |  |  | Release Notes: - vim: Improved multi-line operations
This commit is contained in:
parent
d292b7c96d
commit
69d415c8d0
1 changed files with 97 additions and 53 deletions
|
@ -407,6 +407,9 @@ impl Object {
|
|||
if let Some(range) = self.range(map, selection.clone(), around) {
|
||||
selection.start = range.start;
|
||||
selection.end = range.end;
|
||||
if !around && self.is_multiline() {
|
||||
preserve_indented_newline(map, selection);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -414,6 +417,49 @@ impl Object {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a range without the final newline char.
|
||||
///
|
||||
/// If the selection spans multiple lines and is preceded by an opening brace (`{`),
|
||||
/// this function will trim the selection to exclude the final newline
|
||||
/// in order to preserve a properly indented line.
|
||||
fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
|
||||
let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map));
|
||||
|
||||
if start_point.row == end_point.row {
|
||||
return;
|
||||
}
|
||||
|
||||
let start_offset = selection.start.to_offset(map, Bias::Left);
|
||||
let mut pos = start_offset;
|
||||
|
||||
while pos > 0 {
|
||||
pos -= 1;
|
||||
let current_char = map.buffer_chars_at(pos).next().map(|(ch, _)| ch);
|
||||
|
||||
match current_char {
|
||||
Some(ch) if !ch.is_whitespace() => break,
|
||||
Some('\n') if pos > 0 => {
|
||||
let prev_char = map.buffer_chars_at(pos - 1).next().map(|(ch, _)| ch);
|
||||
if prev_char == Some('{') {
|
||||
let end_pos = selection.end.to_offset(map, Bias::Left);
|
||||
for (ch, offset) in map.reverse_buffer_chars_at(end_pos) {
|
||||
match ch {
|
||||
'\n' => {
|
||||
selection.end = offset.to_display_point(map);
|
||||
break;
|
||||
}
|
||||
ch if !ch.is_whitespace() => break,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a range that surrounds the word `relative_to` is in.
|
||||
///
|
||||
/// If `relative_to` is at the start of a word, return the word.
|
||||
|
@ -1333,12 +1379,24 @@ fn surrounding_markers(
|
|||
}
|
||||
|
||||
if !around && search_across_lines {
|
||||
// Handle trailing newline after opening
|
||||
if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
|
||||
if ch == '\n' {
|
||||
opening.end = range.end
|
||||
opening.end = range.end;
|
||||
|
||||
// After newline, skip leading whitespace
|
||||
let mut chars = movement::chars_after(map, opening.end).peekable();
|
||||
while let Some((ch, range)) = chars.peek() {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
opening.end = range.end;
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle leading whitespace before closing
|
||||
let mut last_newline_end = None;
|
||||
for (ch, range) in movement::chars_before(map, closing.start) {
|
||||
if !ch.is_whitespace() {
|
||||
|
@ -1687,60 +1745,46 @@ mod test {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"func empty(a string) bool {
|
||||
if a == \"\" {
|
||||
return true
|
||||
}
|
||||
ˇreturn false
|
||||
}"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("v i {").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
func empty(a string) bool {
|
||||
« if a == \"\" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
ˇ»}"});
|
||||
cx.set_shared_state(indoc! {
|
||||
"func empty(a string) bool {
|
||||
if a == \"\" {
|
||||
ˇreturn true
|
||||
}
|
||||
return false
|
||||
}"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("v i {").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
func empty(a string) bool {
|
||||
if a == \"\" {
|
||||
« return true
|
||||
ˇ» }
|
||||
return false
|
||||
}"});
|
||||
cx.set_state(
|
||||
indoc! {
|
||||
"func empty(a string) bool {
|
||||
if a == \"\" {
|
||||
return true
|
||||
}
|
||||
ˇreturn false
|
||||
}"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v i {");
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"func empty(a string) bool {
|
||||
if a == \"\" ˇ{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("v i {").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
func empty(a string) bool {
|
||||
if a == \"\" {
|
||||
« return true
|
||||
ˇ» }
|
||||
return false
|
||||
}"});
|
||||
cx.set_state(
|
||||
indoc! {
|
||||
"func empty(a string) bool {
|
||||
if a == \"\" {
|
||||
ˇreturn true
|
||||
}
|
||||
return false
|
||||
}"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v i {");
|
||||
|
||||
cx.set_state(
|
||||
indoc! {
|
||||
"func empty(a string) bool {
|
||||
if a == \"\" ˇ{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v i {");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue