Correct other end visual block functionality (#27678)
Closes #27385 Builds on #27604 so that `vim::OtherEnd` works in visual block mode. This is accomplished by reversing the order of active selections in the buffer when the user hit `o`, so that the cursor moves diagonally across the selection. The current behavior is preserved for `shift-o`, which is how the cursors behave in vim. We'll close #27604 since this encapsulates that change, but if you'd prefer to take only the visual block motion component, we'll keep the branch for #27604 open. Test case: growing a box down and to the right, other ending, followed by growing and shrinking the box: https://github.com/user-attachments/assets/1df544e1-efce-4354-b354-bbfec007a7df Test case: growing a box up and to the left, other ending, followed by growing and shrinking the box: https://github.com/user-attachments/assets/2f6d7729-c63a-4486-960b-23474c2e507a Release Notes: - Improved visual block mode when cursor is at beginning of selection - Improved visual block mode so that `o` and `shift-o` reach parity with vim --------- Co-authored-by: KyleBarton <kjbarton4@gmail.com>
This commit is contained in:
parent
4a5c492188
commit
5c0adde7bb
8 changed files with 165 additions and 6 deletions
|
@ -258,7 +258,7 @@
|
||||||
"u": "vim::ConvertToLowerCase",
|
"u": "vim::ConvertToLowerCase",
|
||||||
"shift-u": "vim::ConvertToUpperCase",
|
"shift-u": "vim::ConvertToUpperCase",
|
||||||
"shift-o": "vim::OtherEnd",
|
"shift-o": "vim::OtherEnd",
|
||||||
"o": "vim::OtherEnd",
|
"o": "vim::OtherEndRowAware",
|
||||||
"d": "vim::VisualDelete",
|
"d": "vim::VisualDelete",
|
||||||
"x": "vim::VisualDelete",
|
"x": "vim::VisualDelete",
|
||||||
"shift-d": "vim::VisualDeleteLine",
|
"shift-d": "vim::VisualDeleteLine",
|
||||||
|
|
|
@ -655,6 +655,25 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||||
.collect();
|
.collect();
|
||||||
self.select(selections);
|
self.select(selections);
|
||||||
}
|
}
|
||||||
|
pub fn reverse_selections(&mut self) {
|
||||||
|
let map = &self.display_map();
|
||||||
|
let mut new_selections: Vec<Selection<Point>> = Vec::new();
|
||||||
|
let disjoint = self.disjoint.clone();
|
||||||
|
for selection in disjoint
|
||||||
|
.iter()
|
||||||
|
.sorted_by(|first, second| Ord::cmp(&second.id, &first.id))
|
||||||
|
.collect::<Vec<&Selection<Anchor>>>()
|
||||||
|
{
|
||||||
|
new_selections.push(Selection {
|
||||||
|
id: self.new_selection_id(),
|
||||||
|
start: selection.start.to_display_point(map).to_point(map),
|
||||||
|
end: selection.end.to_display_point(map).to_point(map),
|
||||||
|
reversed: selection.reversed,
|
||||||
|
goal: selection.goal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.select(new_selections);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_with(
|
pub fn move_with(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
@ -28,7 +28,7 @@ but while developing this test you'll need to run it with the neovim flag enable
|
||||||
cargo test -p vim --features neovim test_visual_star_hash
|
cargo test -p vim --features neovim test_visual_star_hash
|
||||||
```
|
```
|
||||||
|
|
||||||
This will run your keystrokes against a headless neovim and cache the results in the test_data directory.
|
This will run your keystrokes against a headless neovim and cache the results in the test_data directory. Note that neovim must be installed and reachable on your $PATH in order to run the feature.
|
||||||
|
|
||||||
|
|
||||||
## Testing zed-only behavior
|
## Testing zed-only behavior
|
||||||
|
|
|
@ -32,6 +32,7 @@ actions!(
|
||||||
VisualYank,
|
VisualYank,
|
||||||
VisualYankLine,
|
VisualYankLine,
|
||||||
OtherEnd,
|
OtherEnd,
|
||||||
|
OtherEndRowAware,
|
||||||
SelectNext,
|
SelectNext,
|
||||||
SelectPrevious,
|
SelectPrevious,
|
||||||
SelectNextMatch,
|
SelectNextMatch,
|
||||||
|
@ -55,6 +56,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||||
vim.toggle_mode(Mode::VisualBlock, window, cx)
|
vim.toggle_mode(Mode::VisualBlock, window, cx)
|
||||||
});
|
});
|
||||||
Vim::action(editor, cx, Vim::other_end);
|
Vim::action(editor, cx, Vim::other_end);
|
||||||
|
Vim::action(editor, cx, Vim::other_end_row_aware);
|
||||||
Vim::action(editor, cx, Vim::visual_insert_end_of_line);
|
Vim::action(editor, cx, Vim::visual_insert_end_of_line);
|
||||||
Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
|
Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
|
||||||
Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| {
|
Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| {
|
||||||
|
@ -265,7 +267,16 @@ impl Vim {
|
||||||
head = movement::saturating_left(map, head);
|
head = movement::saturating_left(map, head);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((new_head, _)) = move_selection(map, head, goal) else {
|
let reverse_aware_goal = if was_reversed {
|
||||||
|
SelectionGoal::HorizontalRange {
|
||||||
|
start: end,
|
||||||
|
end: start,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
goal
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
head = new_head;
|
head = new_head;
|
||||||
|
@ -321,7 +332,9 @@ impl Vim {
|
||||||
id: s.new_selection_id(),
|
id: s.new_selection_id(),
|
||||||
start: start.to_point(map),
|
start: start.to_point(map),
|
||||||
end: end.to_point(map),
|
end: end.to_point(map),
|
||||||
reversed: is_reversed,
|
reversed: is_reversed &&
|
||||||
|
// For neovim parity: cursor is not reversed when column is a single character
|
||||||
|
end.column() - start.column() > 1,
|
||||||
goal,
|
goal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -336,7 +349,6 @@ impl Vim {
|
||||||
row.0 += 1
|
row.0 += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.select(selections);
|
s.select(selections);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -462,7 +474,26 @@ impl Vim {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||||
s.move_with(|_, selection| {
|
s.move_with(|_, selection| {
|
||||||
selection.reversed = !selection.reversed;
|
selection.reversed = !selection.reversed;
|
||||||
})
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn other_end_row_aware(
|
||||||
|
&mut self,
|
||||||
|
_: &OtherEndRowAware,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let mode = self.mode;
|
||||||
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
||||||
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||||
|
s.move_with(|_, selection| {
|
||||||
|
selection.reversed = !selection.reversed;
|
||||||
|
});
|
||||||
|
if mode == Mode::VisualBlock {
|
||||||
|
s.reverse_selections();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1214,6 +1245,75 @@ mod test {
|
||||||
"
|
"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
cx.set_shared_state(indoc! {"
|
||||||
|
The ˇquick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
|
||||||
|
cx.shared_state().await.assert_eq(indoc! {"
|
||||||
|
The «quick ˇ»brown
|
||||||
|
fox «jumps ˇ»over
|
||||||
|
the lazy dog"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
cx.set_shared_state(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jumpsˇ over
|
||||||
|
the lazy dog"})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
|
||||||
|
cx.shared_state().await.assert_eq(indoc! {"
|
||||||
|
The «ˇquick »brown
|
||||||
|
fox «ˇjumps »over
|
||||||
|
the lazy dog"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
cx.set_shared_state(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jˇumps over
|
||||||
|
the lazy dog"})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
|
||||||
|
cx.shared_state().await.assert_eq(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox j«umps ˇ»over
|
||||||
|
the l«azy dˇ»og"});
|
||||||
|
cx.simulate_shared_keystrokes("o k").await;
|
||||||
|
cx.shared_state().await.assert_eq(indoc! {"
|
||||||
|
The q«ˇuick »brown
|
||||||
|
fox j«ˇumps »over
|
||||||
|
the l«ˇazy d»og"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
cx.set_shared_state(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox jˇumps over
|
||||||
|
the lazy dog"})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
|
||||||
|
cx.shared_state().await.assert_eq(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox j«umps ˇ»over
|
||||||
|
the l«azy dˇ»og"});
|
||||||
|
cx.simulate_shared_keystrokes("shift-o k").await;
|
||||||
|
cx.shared_state().await.assert_eq(indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox j«ˇumps »over
|
||||||
|
the lazy dog"});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"ctrl-v"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Get":{"state":"The «quick ˇ»brown\nfox «jumps ˇ»over\nthe lazy dog","mode":"VisualBlock"}}
|
11
crates/vim/test_data/test_visual_block_mode_other_end.json
Normal file
11
crates/vim/test_data/test_visual_block_mode_other_end.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"ctrl-v"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"k"}
|
||||||
|
{"Get":{"state":"The q«ˇuick »brown\nfox j«ˇumps »over\nthe l«ˇazy d»og","mode":"VisualBlock"}}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"ctrl-v"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}}
|
||||||
|
{"Key":"shift-o"}
|
||||||
|
{"Key":"k"}
|
||||||
|
{"Get":{"state":"The quick brown\nfox j«ˇumps »over\nthe lazy dog","mode":"VisualBlock"}}
|
9
crates/vim/test_data/test_visual_block_mode_up_left.json
Normal file
9
crates/vim/test_data/test_visual_block_mode_up_left.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{"Put":{"state":"The quick brown\nfox jumpsˇ over\nthe lazy dog"}}
|
||||||
|
{"Key":"ctrl-v"}
|
||||||
|
{"Key":"h"}
|
||||||
|
{"Key":"h"}
|
||||||
|
{"Key":"h"}
|
||||||
|
{"Key":"h"}
|
||||||
|
{"Key":"h"}
|
||||||
|
{"Key":"k"}
|
||||||
|
{"Get":{"state":"The «ˇquick »brown\nfox «ˇjumps »over\nthe lazy dog","mode":"VisualBlock"}}
|
Loading…
Add table
Add a link
Reference in a new issue