Files
zed/crates/tab_switcher/src/tab_switcher_tests.rs
Andrew Lygin 894b39a918 Add tab switcher (#7987)
The Tab Switcher implementation (#7653):
- `ctrl-tab` opens the Tab Switcher and moves selection to the
previously selcted tab. It also cycles selection forward.
- `ctrl-shift-tab` opens the Tab Switcher and moves selection to the
last tab in the list. It also cycles selection backward.
- Tab is selected and the Tab Switcher is closed on the shortcut
modifier key (`ctrl` by default) release.
- List items are in reverse activation history order.
- The list reacts to the item changes in background (new tab, tab
closed, tab title changed etc.)

Intentionally not in scope of this PR:
- File icons
- Close buttons

I will come back to these features. I think they need to be implemented
in separate PRs, and be synchronized with changes in how tabs are
rendered, to reuse the code as it's done in the current implementation.
The Tab Switcher looks usable even without them.

Known Issues:

Tab Switcher doesn't react to mouse click on a list item. It's not a tab
switcher specific problem, it looks like ctrl-clicks are not handled the
same way in Zed as cmd-clicks. For instance, menu items can be activated
with cmd-click, but don't react to ctrl-click. Since the Tab Switcher's
default keybinding is `ctrl-tab`, the user can only click an item with
`ctrl` pushed down, thus preventing `on_click()` from firing.

fixes #7653, #7321

Release Notes:

- Added Tab Switcher which is accessible via `ctrl-tab` and
`ctrl-shift-tab` (#7653) (#7321)

Related issues:

- Unblocks #7356, I hope 😄

How it looks and works (it's only `ctrl-tab`'s and `ctrl-shift-tab`'s,
no `enter`'s or mouse clicks):


https://github.com/zed-industries/zed/assets/2101250/4ad4ec6a-5314-481b-8b35-7ac85e43eb92

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-03-27 11:15:08 -07:00

283 lines
9.1 KiB
Rust

use super::*;
use editor::Editor;
use gpui::{TestAppContext, VisualTestContext};
use menu::SelectPrev;
use project::{Project, ProjectPath};
use serde_json::json;
use std::path::Path;
use workspace::{AppState, Workspace};
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
#[gpui::test]
async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action(
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"1.txt": "First file",
"2.txt": "Second file",
"3.txt": "Third file",
"4.txt": "Fourth file",
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_2 = open_buffer("2.txt", &workspace, cx).await;
let tab_3 = open_buffer("3.txt", &workspace, cx).await;
let tab_4 = open_buffer("4.txt", &workspace, cx).await;
// Starts with the previously opened item selected
let tab_switcher = open_tab_switcher(false, &workspace, cx);
tab_switcher.update(cx, |tab_switcher, _| {
assert_eq!(tab_switcher.delegate.matches.len(), 4);
assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone());
assert_match_selection(tab_switcher, 1, tab_3.boxed_clone());
assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone());
assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone());
});
cx.dispatch_action(Toggle { select_last: false });
cx.dispatch_action(Toggle { select_last: false });
tab_switcher.update(cx, |tab_switcher, _| {
assert_eq!(tab_switcher.delegate.matches.len(), 4);
assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone());
assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone());
assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone());
assert_match_selection(tab_switcher, 3, tab_1.boxed_clone());
});
cx.dispatch_action(SelectPrev);
tab_switcher.update(cx, |tab_switcher, _| {
assert_eq!(tab_switcher.delegate.matches.len(), 4);
assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone());
assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone());
assert_match_selection(tab_switcher, 2, tab_2.boxed_clone());
assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone());
});
}
#[gpui::test]
async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"1.txt": "First file",
"2.txt": "Second file",
"3.txt": "Third file",
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_2 = open_buffer("2.txt", &workspace, cx).await;
let tab_3 = open_buffer("3.txt", &workspace, cx).await;
// Starts with the last item selected
let tab_switcher = open_tab_switcher(true, &workspace, cx);
tab_switcher.update(cx, |tab_switcher, _| {
assert_eq!(tab_switcher.delegate.matches.len(), 3);
assert_match_at_position(tab_switcher, 0, tab_3);
assert_match_at_position(tab_switcher, 1, tab_2);
assert_match_selection(tab_switcher, 2, tab_1);
});
}
#[gpui::test]
async fn test_open_item_on_modifiers_release(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"1.txt": "First file",
"2.txt": "Second file",
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_2 = open_buffer("2.txt", &workspace, cx).await;
cx.simulate_modifiers_change(Modifiers::control());
let tab_switcher = open_tab_switcher(false, &workspace, cx);
tab_switcher.update(cx, |tab_switcher, _| {
assert_eq!(tab_switcher.delegate.matches.len(), 2);
assert_match_at_position(tab_switcher, 0, tab_2.boxed_clone());
assert_match_selection(tab_switcher, 1, tab_1.boxed_clone());
});
cx.simulate_modifiers_change(Modifiers::none());
cx.read(|cx| {
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
assert_eq!(active_editor.read(cx).title(cx), "1.txt");
});
assert_tab_switcher_is_closed(workspace, cx);
}
#[gpui::test]
async fn test_open_on_empty_pane(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state.fs.as_fake().insert_tree("/root", json!({})).await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
cx.simulate_modifiers_change(Modifiers::control());
let tab_switcher = open_tab_switcher(false, &workspace, cx);
tab_switcher.update(cx, |tab_switcher, _| {
assert!(tab_switcher.delegate.matches.is_empty());
});
cx.simulate_modifiers_change(Modifiers::none());
assert_tab_switcher_is_closed(workspace, cx);
}
#[gpui::test]
async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree("/root", json!({"1.txt": "Single file"}))
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let tab = open_buffer("1.txt", &workspace, cx).await;
let tab_switcher = open_tab_switcher(false, &workspace, cx);
tab_switcher.update(cx, |tab_switcher, _| {
assert_eq!(tab_switcher.delegate.matches.len(), 1);
assert_match_selection(tab_switcher, 0, tab);
});
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
super::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
})
}
#[track_caller]
fn open_tab_switcher(
select_last: bool,
workspace: &View<Workspace>,
cx: &mut VisualTestContext,
) -> View<Picker<TabSwitcherDelegate>> {
cx.dispatch_action(Toggle { select_last });
get_active_tab_switcher(workspace, cx)
}
#[track_caller]
fn get_active_tab_switcher(
workspace: &View<Workspace>,
cx: &mut VisualTestContext,
) -> View<Picker<TabSwitcherDelegate>> {
workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<TabSwitcher>(cx)
.expect("tab switcher is not open")
.read(cx)
.picker
.clone()
})
}
async fn open_buffer(
file_path: &str,
workspace: &View<Workspace>,
cx: &mut gpui::VisualTestContext,
) -> Box<dyn ItemHandle> {
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().last().expect("worktree not found");
worktree.read(cx).id()
});
let project_path = ProjectPath {
worktree_id,
path: Arc::from(Path::new(file_path)),
};
workspace
.update(cx, move |workspace, cx| {
workspace.open_path(project_path, None, true, cx)
})
.await
.unwrap()
}
#[track_caller]
fn assert_match_selection(
tab_switcher: &Picker<TabSwitcherDelegate>,
expected_selection_index: usize,
expected_item: Box<dyn ItemHandle>,
) {
assert_eq!(
tab_switcher.delegate.selected_index(),
expected_selection_index,
"item is not selected"
);
assert_match_at_position(tab_switcher, expected_selection_index, expected_item);
}
#[track_caller]
fn assert_match_at_position(
tab_switcher: &Picker<TabSwitcherDelegate>,
match_index: usize,
expected_item: Box<dyn ItemHandle>,
) {
let match_item = tab_switcher
.delegate
.matches
.get(match_index)
.unwrap_or_else(|| panic!("Tab Switcher has no match for index {match_index}"));
assert_eq!(match_item.item.item_id(), expected_item.item_id());
}
#[track_caller]
fn assert_tab_switcher_is_closed(workspace: View<Workspace>, cx: &mut VisualTestContext) {
workspace.update(cx, |workspace, cx| {
assert!(
workspace.active_modal::<TabSwitcher>(cx).is_none(),
"tab switcher is still open"
);
});
}