In vim, `c {count} w` diverges from the default `w` motion behavior: For
change operations, the `w` motion will be treated like the `e` motion if
the cursor is on a word, preserving whitespace after the N-th word. In
zed, this special case was only implemented for a count of 1, falling
back to incorrectly using the `w` motion if the count is greater than 1.
This PR generalizes this implementation to handle this special case for
any count.
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Closes #56563.
Release Notes:
- Fixed: vim `cw` with count greater than 1 not preserving whitespace
correctly
---------
Co-authored-by: dino <dinojoaocosta@gmail.com>
765 lines
22 KiB
Rust
765 lines
22 KiB
Rust
use crate::{
|
|
Vim,
|
|
motion::{self, Motion, MotionKind},
|
|
object::Object,
|
|
state::Mode,
|
|
};
|
|
use editor::{
|
|
Bias, DisplayPoint, EditPredictionRequestTrigger,
|
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
|
movement::TextLayoutDetails,
|
|
};
|
|
use gpui::{Context, Window};
|
|
use language::Selection;
|
|
|
|
impl Vim {
|
|
pub fn change_motion(
|
|
&mut self,
|
|
motion: Motion,
|
|
times: Option<usize>,
|
|
forced_motion: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Some motions ignore failure when switching to normal mode
|
|
let mut motion_kind = if matches!(
|
|
motion,
|
|
Motion::Left
|
|
| Motion::Right
|
|
| Motion::EndOfLine { .. }
|
|
| Motion::WrappingLeft
|
|
| Motion::StartOfLine { .. }
|
|
) {
|
|
Some(MotionKind::Exclusive)
|
|
} else {
|
|
None
|
|
};
|
|
self.update_editor(cx, |vim, editor, cx| {
|
|
let text_layout_details = editor.text_layout_details(window, cx);
|
|
editor.transact(window, cx, |editor, window, cx| {
|
|
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
|
editor.set_clip_at_line_ends(false, cx);
|
|
editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.move_with(&mut |map, selection| {
|
|
let kind = match motion {
|
|
Motion::NextWordStart { ignore_punctuation }
|
|
| Motion::NextSubwordStart { ignore_punctuation } => {
|
|
expand_changed_word_selection(
|
|
map,
|
|
selection,
|
|
times,
|
|
ignore_punctuation,
|
|
&text_layout_details,
|
|
motion == Motion::NextSubwordStart { ignore_punctuation },
|
|
!matches!(motion, Motion::NextWordStart { .. }),
|
|
)
|
|
}
|
|
_ => {
|
|
let kind = motion.expand_selection(
|
|
map,
|
|
selection,
|
|
times,
|
|
&text_layout_details,
|
|
forced_motion,
|
|
);
|
|
if matches!(
|
|
motion,
|
|
Motion::CurrentLine | Motion::Down { .. } | Motion::Up { .. }
|
|
) {
|
|
let mut start_offset =
|
|
selection.start.to_offset(map, Bias::Left);
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(selection.start.to_point(map));
|
|
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
|
if ch == '\n' || !classifier.is_whitespace(ch) {
|
|
break;
|
|
}
|
|
start_offset = offset + ch.len_utf8();
|
|
}
|
|
selection.start = start_offset.to_display_point(map);
|
|
}
|
|
kind
|
|
}
|
|
};
|
|
if let Some(kind) = kind {
|
|
motion_kind.get_or_insert(kind);
|
|
}
|
|
});
|
|
});
|
|
if let Some(kind) = motion_kind {
|
|
vim.copy_selections_content(editor, kind, window, cx);
|
|
editor.delete_selections_with_linked_edits(window, cx);
|
|
editor.refresh_edit_prediction(
|
|
true,
|
|
false,
|
|
EditPredictionRequestTrigger::BufferEdit,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
if motion_kind.is_some() {
|
|
self.switch_mode(Mode::Insert, false, window, cx)
|
|
} else {
|
|
self.switch_mode(Mode::Normal, false, window, cx)
|
|
}
|
|
}
|
|
|
|
pub fn change_object(
|
|
&mut self,
|
|
object: Object,
|
|
around: bool,
|
|
times: Option<usize>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let mut objects_found = false;
|
|
self.update_editor(cx, |vim, editor, cx| {
|
|
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
|
editor.set_clip_at_line_ends(false, cx);
|
|
editor.transact(window, cx, |editor, window, cx| {
|
|
editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.move_with(&mut |map, selection| {
|
|
objects_found |= object.expand_selection(map, selection, around, times);
|
|
});
|
|
});
|
|
if objects_found {
|
|
let kind = match object.target_visual_mode(vim.mode, around) {
|
|
Mode::VisualLine => MotionKind::Linewise,
|
|
_ => MotionKind::Exclusive,
|
|
};
|
|
vim.copy_selections_content(editor, kind, window, cx);
|
|
editor.delete_selections_with_linked_edits(window, cx);
|
|
editor.refresh_edit_prediction(
|
|
true,
|
|
false,
|
|
EditPredictionRequestTrigger::BufferEdit,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
if objects_found {
|
|
self.switch_mode(Mode::Insert, false, window, cx);
|
|
} else {
|
|
self.switch_mode(Mode::Normal, false, window, cx);
|
|
}
|
|
}
|
|
}
|
|
|
|
// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
|
|
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
|
|
// on a non-blank. This is because "cw" is interpreted as change-word, and a
|
|
// word does not include the following white space. {Vi: "cw" when on a blank
|
|
// followed by other blanks changes only the first blank; this is probably a
|
|
// bug, because "dw" deletes all the blanks}
|
|
fn expand_changed_word_selection(
|
|
map: &DisplaySnapshot,
|
|
selection: &mut Selection<DisplayPoint>,
|
|
times: Option<usize>,
|
|
ignore_punctuation: bool,
|
|
text_layout_details: &TextLayoutDetails,
|
|
use_subword: bool,
|
|
always_advance: bool,
|
|
) -> Option<MotionKind> {
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(selection.start.to_point(map));
|
|
|
|
let is_in_word = map
|
|
.buffer_chars_at(selection.head().to_offset(map, Bias::Left))
|
|
.next()
|
|
.map(|(c, _)| !classifier.is_whitespace(c))
|
|
.unwrap_or_default();
|
|
|
|
if is_in_word {
|
|
let advance_end = |point, times, always_advance| {
|
|
if use_subword {
|
|
motion::next_subword_end(map, point, ignore_punctuation, times, false)
|
|
} else {
|
|
motion::next_word_end(map, point, ignore_punctuation, times, false, always_advance)
|
|
}
|
|
};
|
|
|
|
let next_char = map
|
|
.buffer_chars_at(
|
|
motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
|
|
)
|
|
.next();
|
|
|
|
if let Some((next, _)) = next_char
|
|
&& next != ' '
|
|
{
|
|
selection.end = advance_end(selection.end, 1, always_advance);
|
|
}
|
|
|
|
if let Some(times) = times
|
|
&& times > 1
|
|
{
|
|
selection.end = advance_end(selection.end, times - 1, true);
|
|
}
|
|
|
|
selection.end = motion::next_char(map, selection.end, false);
|
|
Some(MotionKind::Inclusive)
|
|
} else {
|
|
let motion = if use_subword {
|
|
Motion::NextSubwordStart { ignore_punctuation }
|
|
} else {
|
|
Motion::NextWordStart { ignore_punctuation }
|
|
};
|
|
motion.expand_selection(map, selection, times, text_layout_details, false)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use indoc::indoc;
|
|
|
|
use crate::state::Mode;
|
|
use crate::test::{NeovimBackedTestContext, VimTestContext};
|
|
|
|
#[gpui::test]
|
|
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate("c h", "Teˇst").await.assert_matches();
|
|
cx.simulate("c h", "Tˇest").await.assert_matches();
|
|
cx.simulate("c h", "ˇTest").await.assert_matches();
|
|
cx.simulate(
|
|
"c h",
|
|
indoc! {"
|
|
Test
|
|
ˇtest"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate("c backspace", "Teˇst").await.assert_matches();
|
|
cx.simulate("c backspace", "Tˇest").await.assert_matches();
|
|
cx.simulate("c backspace", "ˇTest").await.assert_matches();
|
|
cx.simulate(
|
|
"c backspace",
|
|
indoc! {"
|
|
Test
|
|
ˇtest"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_l(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate("c l", "Teˇst").await.assert_matches();
|
|
cx.simulate("c l", "Tesˇt").await.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_w(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate("c w", "Teˇst").await.assert_matches();
|
|
cx.simulate("c w", "Tˇest test").await.assert_matches();
|
|
cx.simulate("c w", "Testˇ test").await.assert_matches();
|
|
cx.simulate("c w", "Tesˇt test").await.assert_matches();
|
|
cx.simulate(
|
|
"c w",
|
|
indoc! {"
|
|
Test teˇst
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c w",
|
|
indoc! {"
|
|
Test tesˇt
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c w",
|
|
indoc! {"
|
|
Test test
|
|
ˇ
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
|
|
cx.simulate("c shift-w", "Test teˇst-test test")
|
|
.await
|
|
.assert_matches();
|
|
|
|
// on last character of word, `cw` doesn't eat subsequent punctuation
|
|
// see https://github.com/zed-industries/zed/issues/35269
|
|
cx.simulate("c w", "tesˇt-test").await.assert_matches();
|
|
|
|
cx.simulate("c 2 w", "ˇTest test test")
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate("c 2 w", "Tˇest test test")
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate("c 2 w", "tesˇt-test").await.assert_matches();
|
|
|
|
cx.simulate("c 2 shift-w", "Test teˇst-test test Test")
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_e(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate("c e", "Teˇst Test").await.assert_matches();
|
|
cx.simulate("c e", "Tˇest test").await.assert_matches();
|
|
cx.simulate(
|
|
"c e",
|
|
indoc! {"
|
|
Test teˇst
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c e",
|
|
indoc! {"
|
|
Test tesˇt
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c e",
|
|
indoc! {"
|
|
Test test
|
|
ˇ
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
|
|
cx.simulate("c shift-e", "Test teˇst-test test")
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_b(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate("c b", "Teˇst Test").await.assert_matches();
|
|
cx.simulate("c b", "Test ˇtest").await.assert_matches();
|
|
cx.simulate("c b", "Test1 test2 ˇtest3")
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c b",
|
|
indoc! {"
|
|
Test test
|
|
ˇtest"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c b",
|
|
indoc! {"
|
|
Test test
|
|
ˇ
|
|
test"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
|
|
cx.simulate("c shift-b", "Test test-test ˇtest")
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate(
|
|
"c $",
|
|
indoc! {"
|
|
The qˇuick
|
|
brown fox"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c $",
|
|
indoc! {"
|
|
The quick
|
|
ˇ
|
|
brown fox"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.simulate(
|
|
"c 0",
|
|
indoc! {"
|
|
The qˇuick
|
|
brown fox"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c 0",
|
|
indoc! {"
|
|
The quick
|
|
ˇ
|
|
brown fox"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.simulate(
|
|
"c k",
|
|
indoc! {"
|
|
The quick
|
|
brown ˇfox
|
|
jumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c k",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
jumps ˇover"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c k",
|
|
indoc! {"
|
|
The qˇuick
|
|
brown fox
|
|
jumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c k",
|
|
indoc! {"
|
|
ˇ
|
|
brown fox
|
|
jumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c k",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
ˇjumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate(
|
|
"c j",
|
|
indoc! {"
|
|
The quick
|
|
brown ˇfox
|
|
jumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c j",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
jumps ˇover"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c j",
|
|
indoc! {"
|
|
The qˇuick
|
|
brown fox
|
|
jumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c j",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
ˇ"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c j",
|
|
indoc! {"
|
|
The quick
|
|
ˇbrown fox
|
|
jumps over"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate(
|
|
"c shift-g",
|
|
indoc! {"
|
|
The quick
|
|
brownˇ fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c shift-g",
|
|
indoc! {"
|
|
The quick
|
|
brownˇ fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c shift-g",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
jumps over
|
|
the lˇazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c shift-g",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
jumps over
|
|
ˇ"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_cc(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate(
|
|
"c c",
|
|
indoc! {"
|
|
The quick
|
|
brownˇ fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
|
|
cx.simulate(
|
|
"c c",
|
|
indoc! {"
|
|
ˇThe quick
|
|
brown fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
|
|
cx.simulate(
|
|
"c c",
|
|
indoc! {"
|
|
The quick
|
|
broˇwn fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.simulate(
|
|
"c g g",
|
|
indoc! {"
|
|
The quick
|
|
brownˇ fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c g g",
|
|
indoc! {"
|
|
The quick
|
|
brown fox
|
|
jumps over
|
|
the lˇazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c g g",
|
|
indoc! {"
|
|
The qˇuick
|
|
brown fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate(
|
|
"c g g",
|
|
indoc! {"
|
|
ˇ
|
|
brown fox
|
|
jumps over
|
|
the lazy"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for count in 1..=5 {
|
|
cx.simulate_at_each_offset(
|
|
&format!("c {count} j"),
|
|
indoc! {"
|
|
ˇThe quˇickˇ browˇn
|
|
ˇ
|
|
ˇfox ˇjumpsˇ-ˇoˇver
|
|
ˇthe lazy dog
|
|
"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for count in 1..=5 {
|
|
cx.simulate_at_each_offset(
|
|
&format!("c {count} l"),
|
|
indoc! {"
|
|
ˇThe quˇickˇ browˇn
|
|
ˇ
|
|
ˇfox ˇjumpsˇ-ˇoˇver
|
|
ˇthe lazy dog
|
|
"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for count in 1..=5 {
|
|
cx.simulate_at_each_offset(
|
|
&format!("c {count} b"),
|
|
indoc! {"
|
|
ˇThe quˇickˇ browˇn
|
|
ˇ
|
|
ˇfox ˇjumpsˇ-ˇoˇver
|
|
ˇthe lazy dog
|
|
"},
|
|
)
|
|
.await
|
|
.assert_matches()
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for count in 1..=5 {
|
|
cx.simulate_at_each_offset(
|
|
&format!("c {count} e"),
|
|
indoc! {"
|
|
ˇThe quˇickˇ browˇn
|
|
ˇ
|
|
ˇfox ˇjumpsˇ-ˇoˇver
|
|
ˇthe lazy dog
|
|
"},
|
|
)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_with_selection_spanning_expanded_diff_hunk(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
let diff_base = indoc! {"
|
|
fn main() {
|
|
println!(\"old\");
|
|
}
|
|
"};
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
fn main() {
|
|
ˇprintln!(\"new\");
|
|
}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.set_head_text(diff_base);
|
|
cx.update_editor(|editor, window, cx| {
|
|
editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
|
|
});
|
|
|
|
// Enter visual mode and move up so the selection spans from the
|
|
// insertion (current line) into the deletion (diff base line).
|
|
// Then press `c` which in visual mode dispatches `vim::Substitute`,
|
|
// performing the change operation across the insertion/deletion boundary.
|
|
cx.simulate_keystrokes("v k c");
|
|
}
|
|
}
|