diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 590e84cf7fc10f7af5dd317bc114b75390414e4f..8f5f99e96f708dcc08cc1a9c1fcfc799d6ba43e7 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -95,8 +95,8 @@ "g g": "vim::StartOfDocument", "g h": "editor::Hover", "g B": "editor::BlameHover", - "g t": "pane::ActivateNextItem", - "g shift-t": "pane::ActivatePreviousItem", + "g t": "vim::GoToTab", + "g shift-t": "vim::GoToPreviousTab", "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToDeclaration", "g y": "editor::GoToTypeDefinition", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 7dfdb973c7603e9ef28bf757a9a716e729b72170..5d227ffd8bb6d92acc0546fcb9b9767962f6b417 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -28,7 +28,7 @@ use editor::Editor; use editor::{Anchor, SelectionEffects}; use editor::{Bias, ToPoint}; use editor::{display_map::ToDisplayPoint, movement}; -use gpui::{Context, Window, actions}; +use gpui::{Action, Context, Window, actions}; use language::{Point, SelectionGoal}; use log::error; use multi_buffer::MultiBufferRow; @@ -94,6 +94,10 @@ actions!( Redo, /// Undoes all changes to the most recently changed line. UndoLastLine, + /// Go to tab page (with count support). + GoToTab, + /// Go to previous tab page (with count support). + GoToPreviousTab, ] ); @@ -116,6 +120,8 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::toggle_comments); Vim::action(editor, cx, Vim::paste); Vim::action(editor, cx, Vim::show_location); + Vim::action(editor, cx, Vim::go_to_tab); + Vim::action(editor, cx, Vim::go_to_previous_tab); Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| { vim.record_current_action(cx); @@ -984,6 +990,54 @@ impl Vim { self.switch_mode(Mode::Insert, true, window, cx); } } + + fn go_to_tab(&mut self, _: &GoToTab, window: &mut Window, cx: &mut Context) { + let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); + + if let Some(tab_index) = count { + // gt goes to tab (1-based). + let zero_based_index = tab_index.saturating_sub(1); + window.dispatch_action( + workspace::pane::ActivateItem(zero_based_index).boxed_clone(), + cx, + ); + } else { + // If no count is provided, go to the next tab. + window.dispatch_action(workspace::pane::ActivateNextItem.boxed_clone(), cx); + } + } + + fn go_to_previous_tab( + &mut self, + _: &GoToPreviousTab, + window: &mut Window, + cx: &mut Context, + ) { + let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); + + if let Some(count) = count { + // gT with count goes back that many tabs with wraparound (not the same as gt!). + if let Some(workspace) = self.workspace(window) { + let pane = workspace.read(cx).active_pane().read(cx); + let item_count = pane.items().count(); + if item_count > 0 { + let current_index = pane.active_item_index(); + let target_index = (current_index as isize - count as isize) + .rem_euclid(item_count as isize) + as usize; + window.dispatch_action( + workspace::pane::ActivateItem(target_index).boxed_clone(), + cx, + ); + } + } + } else { + // No count provided, go to the previous tab. + window.dispatch_action(workspace::pane::ActivatePreviousItem.boxed_clone(), cx); + } + } } #[cfg(test)] mod test { @@ -2119,4 +2173,80 @@ mod test { Mode::Normal, ); } + + #[gpui::test] + async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Open 4 tabs. + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.items(cx).count(), 4); + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); + }); + + cx.simulate_keystrokes("1 g t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0); + }); + + cx.simulate_keystrokes("3 g t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2); + }); + + cx.simulate_keystrokes("4 g t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); + }); + + cx.simulate_keystrokes("1 g t"); + cx.simulate_keystrokes("g t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); + }); + } + + #[gpui::test] + async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Open 4 tabs. + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.items(cx).count(), 4); + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); + }); + + cx.simulate_keystrokes("2 g shift-t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); + }); + + cx.simulate_keystrokes("g shift-t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0); + }); + + // Wraparound: gT from first tab should go to last. + cx.simulate_keystrokes("g shift-t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); + }); + + cx.simulate_keystrokes("6 g shift-t"); + cx.workspace(|workspace, _, cx| { + assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); + }); + } }