workspace: Allow disabling tab cycles when using keyboard navigation (#51253)
Add a `wrap_around` field, defaulting to `true`, to both `pane::ActivatePreviousItem` and `pane::ActivateNextItem` actions to optionally disable wrapping when cycling past the first or last tab. Release Notes: - Added a `wrap_around` option to both `pane::ActivatePreviousItem` and `pane::ActivateNextItem` actions to optionally disable wrapping when cycling past the first or last tab. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
1 parent
f8e871c7b0
commit
18cf2ce256
6 files changed
+122
-24
No files matched your search
@@ -223,8 +223,8 @@ async fn test_close_selected_item(cx: &mut gpui::TestAppContext) {
|
||||
// 1.txt | [3.txt] | 2.txt | 4.txt
|
||||
//
|
||||
// With 3.txt being the active item in the pane.
|
||||
cx.dispatch_action(ActivatePreviousItem);
|
||||
cx.dispatch_action(ActivatePreviousItem);
|
||||
cx.dispatch_action(ActivatePreviousItem::default());
|
||||
cx.dispatch_action(ActivatePreviousItem::default());
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.simulate_modifiers_change(Modifiers::control());
|
||||
|
||||
@@ -1660,9 +1660,13 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
action.range.replace(range.clone());
|
||||
Some(Box::new(action))
|
||||
}),
|
||||
VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
|
||||
VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
|
||||
VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
|
||||
VimCommand::new(("bn", "ext"), workspace::ActivateNextItem::default()).count(),
|
||||
VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem::default()).count(),
|
||||
VimCommand::new(
|
||||
("bp", "revious"),
|
||||
workspace::ActivatePreviousItem::default(),
|
||||
)
|
||||
.count(),
|
||||
VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
|
||||
VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
|
||||
VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
|
||||
@@ -1670,9 +1674,13 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
|
||||
VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
|
||||
VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
|
||||
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
|
||||
VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
|
||||
VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
|
||||
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem::default()).count(),
|
||||
VimCommand::new(
|
||||
("tabp", "revious"),
|
||||
workspace::ActivatePreviousItem::default(),
|
||||
)
|
||||
.count(),
|
||||
VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem::default()).count(),
|
||||
VimCommand::new(
|
||||
("tabc", "lose"),
|
||||
workspace::CloseActiveItem {
|
||||
|
||||
@@ -449,7 +449,10 @@ pub fn init(cx: &mut App) {
|
||||
);
|
||||
} else {
|
||||
// If no count is provided, go to the next tab.
|
||||
window.dispatch_action(workspace::pane::ActivateNextItem.boxed_clone(), cx);
|
||||
window.dispatch_action(
|
||||
workspace::pane::ActivateNextItem::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -473,7 +476,10 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
} else {
|
||||
// No count provided, go to the previous tab.
|
||||
window.dispatch_action(workspace::pane::ActivatePreviousItem.boxed_clone(), cx);
|
||||
window.dispatch_action(
|
||||
workspace::pane::ActivatePreviousItem::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -48,7 +48,9 @@ use ui::{
|
||||
IconDecorationKind, Indicator, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition,
|
||||
Tooltip, prelude::*, right_click_menu,
|
||||
};
|
||||
use util::{ResultExt, debug_panic, maybe, paths::PathStyle, truncate_and_remove_front};
|
||||
use util::{
|
||||
ResultExt, debug_panic, maybe, paths::PathStyle, serde::default_true, truncate_and_remove_front,
|
||||
};
|
||||
|
||||
/// A selected entry in e.g. project panel.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -229,13 +231,41 @@ split_structs!(
|
||||
SplitVertical => "Splits the pane vertically."
|
||||
);
|
||||
|
||||
/// Activates the previous item in the pane.
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = pane)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct ActivatePreviousItem {
|
||||
/// Whether to wrap from the first item to the last item.
|
||||
#[serde(default = "default_true")]
|
||||
pub wrap_around: bool,
|
||||
}
|
||||
|
||||
impl Default for ActivatePreviousItem {
|
||||
fn default() -> Self {
|
||||
Self { wrap_around: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Activates the next item in the pane.
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = pane)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct ActivateNextItem {
|
||||
/// Whether to wrap from the last item to the first item.
|
||||
#[serde(default = "default_true")]
|
||||
pub wrap_around: bool,
|
||||
}
|
||||
|
||||
impl Default for ActivateNextItem {
|
||||
fn default() -> Self {
|
||||
Self { wrap_around: true }
|
||||
}
|
||||
}
|
||||
|
||||
actions!(
|
||||
pane,
|
||||
[
|
||||
/// Activates the previous item in the pane.
|
||||
ActivatePreviousItem,
|
||||
/// Activates the next item in the pane.
|
||||
ActivateNextItem,
|
||||
/// Activates the last item in the pane.
|
||||
ActivateLastItem,
|
||||
/// Switches to the alternate file.
|
||||
@@ -1477,14 +1507,14 @@ impl Pane {
|
||||
|
||||
pub fn activate_previous_item(
|
||||
&mut self,
|
||||
_: &ActivatePreviousItem,
|
||||
action: &ActivatePreviousItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut index = self.active_item_index;
|
||||
if index > 0 {
|
||||
index -= 1;
|
||||
} else if !self.items.is_empty() {
|
||||
} else if action.wrap_around && !self.items.is_empty() {
|
||||
index = self.items.len() - 1;
|
||||
}
|
||||
self.activate_item(index, true, true, window, cx);
|
||||
@@ -1492,14 +1522,14 @@ impl Pane {
|
||||
|
||||
pub fn activate_next_item(
|
||||
&mut self,
|
||||
_: &ActivateNextItem,
|
||||
action: &ActivateNextItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut index = self.active_item_index;
|
||||
if index + 1 < self.items.len() {
|
||||
index += 1;
|
||||
} else {
|
||||
} else if action.wrap_around {
|
||||
index = 0;
|
||||
}
|
||||
self.activate_item(index, true, true, window, cx);
|
||||
@@ -8585,6 +8615,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_activate_item_with_wrap_around(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
add_labeled_item(&pane, "A", false, cx);
|
||||
add_labeled_item(&pane, "B", false, cx);
|
||||
add_labeled_item(&pane, "C", false, cx);
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_next_item(&ActivateNextItem { wrap_around: false }, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_next_item(&ActivateNextItem::default(), window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A*", "B", "C"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_previous_item(&ActivatePreviousItem { wrap_around: false }, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A*", "B", "C"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_previous_item(&ActivatePreviousItem::default(), window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_previous_item(&ActivatePreviousItem { wrap_around: false }, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A", "B*", "C"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_next_item(&ActivateNextItem { wrap_around: false }, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -7083,7 +7083,7 @@ impl Workspace {
|
||||
))
|
||||
.on_action(cx.listener(Workspace::toggle_centered_layout))
|
||||
.on_action(cx.listener(
|
||||
|workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
|
||||
|workspace: &mut Workspace, action: &pane::ActivateNextItem, window, cx| {
|
||||
if let Some(active_dock) = workspace.active_dock(window, cx) {
|
||||
let dock = active_dock.read(cx);
|
||||
if let Some(active_panel) = dock.active_panel() {
|
||||
@@ -7101,14 +7101,17 @@ impl Workspace {
|
||||
}
|
||||
|
||||
if let Some(pane) = recent_pane {
|
||||
let wrap_around = action.wrap_around;
|
||||
pane.update(cx, |pane, cx| {
|
||||
let current_index = pane.active_item_index();
|
||||
let items_len = pane.items_len();
|
||||
if items_len > 0 {
|
||||
let next_index = if current_index + 1 < items_len {
|
||||
current_index + 1
|
||||
} else {
|
||||
} else if wrap_around {
|
||||
0
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
pane.activate_item(
|
||||
next_index, false, false, window, cx,
|
||||
@@ -7124,7 +7127,7 @@ impl Workspace {
|
||||
},
|
||||
))
|
||||
.on_action(cx.listener(
|
||||
|workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
|
||||
|workspace: &mut Workspace, action: &pane::ActivatePreviousItem, window, cx| {
|
||||
if let Some(active_dock) = workspace.active_dock(window, cx) {
|
||||
let dock = active_dock.read(cx);
|
||||
if let Some(active_panel) = dock.active_panel() {
|
||||
@@ -7142,14 +7145,17 @@ impl Workspace {
|
||||
}
|
||||
|
||||
if let Some(pane) = recent_pane {
|
||||
let wrap_around = action.wrap_around;
|
||||
pane.update(cx, |pane, cx| {
|
||||
let current_index = pane.active_item_index();
|
||||
let items_len = pane.items_len();
|
||||
if items_len > 0 {
|
||||
let prev_index = if current_index > 0 {
|
||||
current_index - 1
|
||||
} else {
|
||||
} else if wrap_around {
|
||||
items_len.saturating_sub(1)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
pane.activate_item(
|
||||
prev_index, false, false, window, cx,
|
||||
|
||||
@@ -4579,7 +4579,10 @@ mod tests {
|
||||
assert_key_bindings_for(
|
||||
window.into(),
|
||||
cx,
|
||||
vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
|
||||
vec![
|
||||
("backspace", &ActionB),
|
||||
("{", &ActivatePreviousItem::default()),
|
||||
],
|
||||
line!(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user