Cargo.lock 🔗
@@ -14920,6 +14920,7 @@ dependencies = [
"multi_buffer",
"nvim-rs",
"parking_lot",
+ "project",
"project_panel",
"regex",
"release_channel",
Conrad Irwin and João Marcos created
Closes #24243
Release Notes:
- vim: Fix j/k on folded multibuffer headers
---------
Co-authored-by: João Marcos <marcospb19@hotmail.com>
Cargo.lock | 1
crates/editor/src/display_map.rs | 5
crates/editor/src/display_map/block_map.rs | 9
crates/editor/src/test/editor_test_context.rs | 91 +++++++++
crates/vim/Cargo.toml | 1
crates/vim/src/motion.rs | 17 +
crates/vim/src/test.rs | 205 ++++++++++++++++++++
crates/vim/src/test/vim_test_context.rs | 36 ++-
8 files changed, 348 insertions(+), 17 deletions(-)
@@ -14920,6 +14920,7 @@ dependencies = [
"multi_buffer",
"nvim-rs",
"parking_lot",
+ "project",
"project_panel",
"regex",
"release_channel",
@@ -1124,6 +1124,11 @@ impl DisplaySnapshot {
self.block_snapshot.is_block_line(BlockRow(display_row.0))
}
+ pub fn is_folded_buffer_header(&self, display_row: DisplayRow) -> bool {
+ self.block_snapshot
+ .is_folded_buffer_header(BlockRow(display_row.0))
+ }
+
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
let wrap_row = self
.block_snapshot
@@ -1618,6 +1618,15 @@ impl BlockSnapshot {
cursor.item().map_or(false, |t| t.block.is_some())
}
+ pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
+ cursor.seek(&row, Bias::Right, &());
+ let Some(transform) = cursor.item() else {
+ return false;
+ };
+ matches!(transform.block, Some(Block::FoldedBuffer { .. }))
+ }
+
pub(super) fn is_line_replaced(&self, row: MultiBufferRow) -> bool {
let wrap_point = self
.wrap_snapshot
@@ -89,6 +89,16 @@ impl EditorTestContext {
Path::new("/root")
}
+ pub async fn for_editor_in(editor: Entity<Editor>, cx: &mut gpui::VisualTestContext) -> Self {
+ cx.focus(&editor);
+ Self {
+ window: cx.windows()[0],
+ cx: cx.clone(),
+ editor,
+ assertion_cx: AssertionContextManager::new(),
+ }
+ }
+
pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
let editor_view = editor.root(cx).unwrap();
Self {
@@ -381,6 +391,76 @@ impl EditorTestContext {
assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
}
+ #[track_caller]
+ pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
+ let expected_excerpts = marked_text
+ .strip_prefix("[EXCERPT]\n")
+ .unwrap()
+ .split("[EXCERPT]\n")
+ .collect::<Vec<_>>();
+
+ let (selections, excerpts) = self.update_editor(|editor, _, cx| {
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+
+ let selections = editor.selections.disjoint_anchors();
+ let excerpts = multibuffer_snapshot
+ .excerpts()
+ .map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range))
+ .collect::<Vec<_>>();
+
+ (selections, excerpts)
+ });
+
+ assert_eq!(excerpts.len(), expected_excerpts.len());
+
+ for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
+ let is_folded = self
+ .update_editor(|editor, _, cx| editor.is_buffer_folded(snapshot.remote_id(), cx));
+ let (expected_text, expected_selections) =
+ marked_text_ranges(expected_excerpts[ix], true);
+ if expected_text == "[FOLDED]\n" {
+ assert!(is_folded, "excerpt {} should be folded", ix);
+ let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
+ if expected_selections.len() > 0 {
+ assert!(
+ is_selected,
+ "excerpt {} should be selected. Got {:?}",
+ ix,
+ self.editor_state()
+ );
+ } else {
+ assert!(!is_selected, "excerpt {} should not be selected", ix);
+ }
+ continue;
+ }
+ assert!(!is_folded, "excerpt {} should not be folded", ix);
+ assert_eq!(
+ snapshot
+ .text_for_range(range.context.clone())
+ .collect::<String>(),
+ expected_text
+ );
+
+ let selections = selections
+ .iter()
+ .filter(|s| s.head().excerpt_id == excerpt_id)
+ .map(|s| {
+ let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
+ - text::ToOffset::to_offset(&range.context.start, &snapshot);
+ let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
+ - text::ToOffset::to_offset(&range.context.start, &snapshot);
+ tail..head
+ })
+ .collect::<Vec<_>>();
+ // todo: selections that cross excerpt boundaries..
+ assert_eq!(
+ selections, expected_selections,
+ "excerpt {} has incorrect selections",
+ ix,
+ );
+ }
+ }
+
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
@@ -392,6 +472,17 @@ impl EditorTestContext {
self.assert_selections(expected_selections, marked_text.to_string())
}
+ /// Make an assertion about the editor's text and the ranges and directions
+ /// of its selections using a string containing embedded range markers.
+ ///
+ /// See the `util::test::marked_text_ranges` function for more information.
+ #[track_caller]
+ pub fn assert_display_state(&mut self, marked_text: &str) {
+ let (expected_text, expected_selections) = marked_text_ranges(marked_text, true);
+ pretty_assertions::assert_eq!(self.display_text(), expected_text, "unexpected buffer text");
+ self.assert_selections(expected_selections, marked_text.to_string())
+ }
+
pub fn editor_state(&mut self) -> String {
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
}
@@ -55,6 +55,7 @@ git_ui.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true
project_panel.workspace = true
@@ -1329,12 +1329,25 @@ pub(crate) fn start_of_relative_buffer_row(
fn up_down_buffer_rows(
map: &DisplaySnapshot,
- point: DisplayPoint,
+ mut point: DisplayPoint,
mut goal: SelectionGoal,
- times: isize,
+ mut times: isize,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let bias = if times < 0 { Bias::Left } else { Bias::Right };
+
+ while map.is_folded_buffer_header(point.row()) {
+ if times < 0 {
+ (point, _) = movement::up(map, point, goal, true, text_layout_details);
+ times += 1;
+ } else if times > 0 {
+ (point, _) = movement::down(map, point, goal, true, text_layout_details);
+ times -= 1;
+ } else {
+ break;
+ }
+ }
+
let start = map.display_point_to_fold_point(point, Bias::Left);
let begin_folded_line = map.fold_point_to_display_point(
map.fold_snapshot
@@ -6,9 +6,13 @@ use std::time::Duration;
use collections::HashMap;
use command_palette::CommandPalette;
-use editor::{actions::DeleteLine, display_map::DisplayRow, DisplayPoint};
+use editor::{
+ actions::DeleteLine, display_map::DisplayRow, test::editor_test_context::EditorTestContext,
+ DisplayPoint, Editor, EditorMode, MultiBuffer,
+};
use futures::StreamExt;
use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext};
+use language::Point;
pub use neovim_backed_test_context::*;
use settings::SettingsStore;
pub use vim_test_context::*;
@@ -1707,3 +1711,202 @@ async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) {
cx.simulate_shared_keystrokes("l l escape .").await;
cx.shared_state().await.assert_eq("hellˇllo world.");
}
+
+#[gpui::test]
+async fn test_folded_multibuffer_excerpts(cx: &mut gpui::TestAppContext) {
+ VimTestContext::init(cx);
+ cx.update(|cx| {
+ VimTestContext::init_keybindings(true, cx);
+ });
+ let (editor, cx) = cx.add_window_view(|window, cx| {
+ let multi_buffer = MultiBuffer::build_multi(
+ [
+ ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
+ ("aaa\nbbb\nccc\nddd\n", vec![Point::row_range(0..2)]),
+ ("AAA\nBBB\nCCC\nDDD\n", vec![Point::row_range(0..2)]),
+ ("one\ntwo\nthr\nfou\n", vec![Point::row_range(0..2)]),
+ ],
+ cx,
+ );
+ let mut editor = Editor::new(
+ EditorMode::Full,
+ multi_buffer.clone(),
+ None,
+ true,
+ window,
+ cx,
+ );
+
+ let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
+ // fold all but the second buffer, so that we test navigating between two
+ // adjacent folded buffers, as well as folded buffers at the start and
+ // end the multibuffer
+ editor.fold_buffer(buffer_ids[0], cx);
+ editor.fold_buffer(buffer_ids[2], cx);
+ editor.fold_buffer(buffer_ids[3], cx);
+
+ editor
+ });
+ let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
+
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("j");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ ˇaaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("j");
+ cx.simulate_keystroke("j");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ ˇ[EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("j");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("j");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ ˇ[FOLDED]
+ "
+ });
+ cx.simulate_keystroke("k");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("k");
+ cx.simulate_keystroke("k");
+ cx.simulate_keystroke("k");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ ˇaaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("k");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystroke("shift-g");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ ˇ[FOLDED]
+ "
+ });
+ cx.simulate_keystrokes("g g");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ aaa
+ bbb
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.update_editor(|editor, _, cx| {
+ let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
+ editor.fold_buffer(buffer_ids[1], cx);
+ });
+
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+ cx.simulate_keystrokes("2 j");
+ cx.assert_excerpts_with_selections(indoc! {"
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ [EXCERPT]
+ ˇ[FOLDED]
+ [EXCERPT]
+ [FOLDED]
+ "
+ });
+}
@@ -24,6 +24,10 @@ impl VimTestContext {
git_ui::init(cx);
crate::init(cx);
search::init(cx);
+ language::init(cx);
+ editor::init_settings(cx);
+ project::Project::init_settings(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
});
}
@@ -56,22 +60,26 @@ impl VimTestContext {
)
}
+ pub fn init_keybindings(enabled: bool, cx: &mut App) {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
+ });
+ let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
+ "keymaps/default-macos.json",
+ cx,
+ )
+ .unwrap();
+ cx.bind_keys(default_key_bindings);
+ if enabled {
+ let vim_key_bindings =
+ settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
+ cx.bind_keys(vim_key_bindings);
+ }
+ }
+
pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
cx.update(|_, cx| {
- SettingsStore::update_global(cx, |store, cx| {
- store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
- });
- let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
- "keymaps/default-macos.json",
- cx,
- )
- .unwrap();
- cx.bind_keys(default_key_bindings);
- if enabled {
- let vim_key_bindings =
- settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
- cx.bind_keys(vim_key_bindings);
- }
+ Self::init_keybindings(enabled, cx);
});
// Setup search toolbars and keypress hook