tab_switcher_tests.rs

  1use super::*;
  2use editor::Editor;
  3use gpui::{TestAppContext, VisualTestContext};
  4use menu::SelectPrevious;
  5use project::{Project, ProjectPath};
  6use serde_json::json;
  7use util::{path, rel_path::rel_path};
  8use workspace::{AppState, Workspace};
  9
 10#[ctor::ctor]
 11fn init_logger() {
 12    zlog::init_test();
 13}
 14
 15#[gpui::test]
 16async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action(
 17    cx: &mut gpui::TestAppContext,
 18) {
 19    let app_state = init_test(cx);
 20
 21    app_state
 22        .fs
 23        .as_fake()
 24        .insert_tree(
 25            path!("/root"),
 26            json!({
 27                "1.txt": "First file",
 28                "2.txt": "Second file",
 29                "3.txt": "Third file",
 30                "4.txt": "Fourth file",
 31            }),
 32        )
 33        .await;
 34
 35    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 36    let (workspace, cx) =
 37        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 38
 39    let tab_1 = open_buffer("1.txt", &workspace, cx).await;
 40    let tab_2 = open_buffer("2.txt", &workspace, cx).await;
 41    let tab_3 = open_buffer("3.txt", &workspace, cx).await;
 42    let tab_4 = open_buffer("4.txt", &workspace, cx).await;
 43
 44    // Starts with the previously opened item selected
 45    let tab_switcher = open_tab_switcher(false, &workspace, cx);
 46    tab_switcher.update(cx, |tab_switcher, _| {
 47        assert_eq!(tab_switcher.delegate.matches.len(), 4);
 48        assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone());
 49        assert_match_selection(tab_switcher, 1, tab_3.boxed_clone());
 50        assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone());
 51        assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone());
 52    });
 53
 54    cx.dispatch_action(Toggle { select_last: false });
 55    cx.dispatch_action(Toggle { select_last: false });
 56    tab_switcher.update(cx, |tab_switcher, _| {
 57        assert_eq!(tab_switcher.delegate.matches.len(), 4);
 58        assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone());
 59        assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone());
 60        assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone());
 61        assert_match_selection(tab_switcher, 3, tab_1.boxed_clone());
 62    });
 63
 64    cx.dispatch_action(SelectPrevious);
 65    tab_switcher.update(cx, |tab_switcher, _| {
 66        assert_eq!(tab_switcher.delegate.matches.len(), 4);
 67        assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone());
 68        assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone());
 69        assert_match_selection(tab_switcher, 2, tab_2.boxed_clone());
 70        assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone());
 71    });
 72}
 73
 74#[gpui::test]
 75async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) {
 76    let app_state = init_test(cx);
 77
 78    app_state
 79        .fs
 80        .as_fake()
 81        .insert_tree(
 82            path!("/root"),
 83            json!({
 84                "1.txt": "First file",
 85                "2.txt": "Second file",
 86                "3.txt": "Third file",
 87            }),
 88        )
 89        .await;
 90
 91    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
 92    let (workspace, cx) =
 93        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 94
 95    let tab_1 = open_buffer("1.txt", &workspace, cx).await;
 96    let tab_2 = open_buffer("2.txt", &workspace, cx).await;
 97    let tab_3 = open_buffer("3.txt", &workspace, cx).await;
 98
 99    // Starts with the last item selected
100    let tab_switcher = open_tab_switcher(true, &workspace, cx);
101    tab_switcher.update(cx, |tab_switcher, _| {
102        assert_eq!(tab_switcher.delegate.matches.len(), 3);
103        assert_match_at_position(tab_switcher, 0, tab_3);
104        assert_match_at_position(tab_switcher, 1, tab_2);
105        assert_match_selection(tab_switcher, 2, tab_1);
106    });
107}
108
109#[gpui::test]
110async fn test_open_item_on_modifiers_release(cx: &mut gpui::TestAppContext) {
111    let app_state = init_test(cx);
112
113    app_state
114        .fs
115        .as_fake()
116        .insert_tree(
117            path!("/root"),
118            json!({
119                "1.txt": "First file",
120                "2.txt": "Second file",
121            }),
122        )
123        .await;
124
125    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
126    let (workspace, cx) =
127        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
128
129    let tab_1 = open_buffer("1.txt", &workspace, cx).await;
130    let tab_2 = open_buffer("2.txt", &workspace, cx).await;
131
132    cx.simulate_modifiers_change(Modifiers::control());
133    let tab_switcher = open_tab_switcher(false, &workspace, cx);
134    tab_switcher.update(cx, |tab_switcher, _| {
135        assert_eq!(tab_switcher.delegate.matches.len(), 2);
136        assert_match_at_position(tab_switcher, 0, tab_2.boxed_clone());
137        assert_match_selection(tab_switcher, 1, tab_1.boxed_clone());
138    });
139
140    cx.simulate_modifiers_change(Modifiers::none());
141    cx.read(|cx| {
142        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
143        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
144    });
145    assert_tab_switcher_is_closed(workspace, cx);
146}
147
148#[gpui::test]
149async fn test_open_on_empty_pane(cx: &mut gpui::TestAppContext) {
150    let app_state = init_test(cx);
151    app_state.fs.as_fake().insert_tree("/root", json!({})).await;
152
153    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
154    let (workspace, cx) =
155        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
156
157    cx.simulate_modifiers_change(Modifiers::control());
158    let tab_switcher = open_tab_switcher(false, &workspace, cx);
159    tab_switcher.update(cx, |tab_switcher, _| {
160        assert!(tab_switcher.delegate.matches.is_empty());
161    });
162
163    cx.simulate_modifiers_change(Modifiers::none());
164    assert_tab_switcher_is_closed(workspace, cx);
165}
166
167#[gpui::test]
168async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) {
169    let app_state = init_test(cx);
170    app_state
171        .fs
172        .as_fake()
173        .insert_tree(path!("/root"), json!({"1.txt": "Single file"}))
174        .await;
175
176    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
177    let (workspace, cx) =
178        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
179
180    let tab = open_buffer("1.txt", &workspace, cx).await;
181
182    let tab_switcher = open_tab_switcher(false, &workspace, cx);
183    tab_switcher.update(cx, |tab_switcher, _| {
184        assert_eq!(tab_switcher.delegate.matches.len(), 1);
185        assert_match_selection(tab_switcher, 0, tab);
186    });
187}
188
189#[gpui::test]
190async fn test_close_selected_item(cx: &mut gpui::TestAppContext) {
191    let app_state = init_test(cx);
192    app_state
193        .fs
194        .as_fake()
195        .insert_tree(
196            path!("/root"),
197            json!({
198                "1.txt": "First file",
199                "2.txt": "Second file",
200            }),
201        )
202        .await;
203
204    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
205    let (workspace, cx) =
206        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
207
208    let tab_1 = open_buffer("1.txt", &workspace, cx).await;
209    let tab_2 = open_buffer("2.txt", &workspace, cx).await;
210
211    cx.simulate_modifiers_change(Modifiers::control());
212    let tab_switcher = open_tab_switcher(false, &workspace, cx);
213    tab_switcher.update(cx, |tab_switcher, _| {
214        assert_eq!(tab_switcher.delegate.matches.len(), 2);
215        assert_match_at_position(tab_switcher, 0, tab_2.boxed_clone());
216        assert_match_selection(tab_switcher, 1, tab_1.boxed_clone());
217    });
218
219    cx.simulate_modifiers_change(Modifiers::control());
220    cx.dispatch_action(CloseSelectedItem);
221    tab_switcher.update(cx, |tab_switcher, _| {
222        assert_eq!(tab_switcher.delegate.matches.len(), 1);
223        assert_match_selection(tab_switcher, 0, tab_2);
224    });
225
226    // Still switches tab on modifiers release
227    cx.simulate_modifiers_change(Modifiers::none());
228    cx.read(|cx| {
229        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
230        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
231    });
232    assert_tab_switcher_is_closed(workspace, cx);
233}
234
235#[gpui::test]
236async fn test_close_preserves_selected_position(cx: &mut gpui::TestAppContext) {
237    let app_state = init_test(cx);
238    app_state
239        .fs
240        .as_fake()
241        .insert_tree(
242            path!("/root"),
243            json!({
244                "1.txt": "First file",
245                "2.txt": "Second file",
246                "3.txt": "Third file",
247            }),
248        )
249        .await;
250
251    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
252    let (workspace, cx) =
253        cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
254
255    let tab_1 = open_buffer("1.txt", &workspace, cx).await;
256    let tab_2 = open_buffer("2.txt", &workspace, cx).await;
257    let tab_3 = open_buffer("3.txt", &workspace, cx).await;
258
259    let tab_switcher = open_tab_switcher(false, &workspace, cx);
260    tab_switcher.update(cx, |tab_switcher, _| {
261        assert_eq!(tab_switcher.delegate.matches.len(), 3);
262        assert_match_at_position(tab_switcher, 0, tab_3.boxed_clone());
263        assert_match_selection(tab_switcher, 1, tab_2.boxed_clone());
264        assert_match_at_position(tab_switcher, 2, tab_1.boxed_clone());
265    });
266
267    // Verify that if the selected tab was closed, tab at the same position is selected.
268    cx.dispatch_action(CloseSelectedItem);
269    tab_switcher.update(cx, |tab_switcher, _| {
270        assert_eq!(tab_switcher.delegate.matches.len(), 2);
271        assert_match_at_position(tab_switcher, 0, tab_3.boxed_clone());
272        assert_match_selection(tab_switcher, 1, tab_1.boxed_clone());
273    });
274
275    // But if the position is no longer valid, fall back to the position above.
276    cx.dispatch_action(CloseSelectedItem);
277    tab_switcher.update(cx, |tab_switcher, _| {
278        assert_eq!(tab_switcher.delegate.matches.len(), 1);
279        assert_match_selection(tab_switcher, 0, tab_3.boxed_clone());
280    });
281}
282
283fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
284    cx.update(|cx| {
285        let state = AppState::test(cx);
286        theme::init(theme::LoadThemes::JustBase, cx);
287        language::init(cx);
288        super::init(cx);
289        editor::init(cx);
290        workspace::init_settings(cx);
291        Project::init_settings(cx);
292        state
293    })
294}
295
296#[track_caller]
297fn open_tab_switcher(
298    select_last: bool,
299    workspace: &Entity<Workspace>,
300    cx: &mut VisualTestContext,
301) -> Entity<Picker<TabSwitcherDelegate>> {
302    cx.dispatch_action(Toggle { select_last });
303    get_active_tab_switcher(workspace, cx)
304}
305
306#[track_caller]
307fn get_active_tab_switcher(
308    workspace: &Entity<Workspace>,
309    cx: &mut VisualTestContext,
310) -> Entity<Picker<TabSwitcherDelegate>> {
311    workspace.update(cx, |workspace, cx| {
312        workspace
313            .active_modal::<TabSwitcher>(cx)
314            .expect("tab switcher is not open")
315            .read(cx)
316            .picker
317            .clone()
318    })
319}
320
321async fn open_buffer(
322    file_path: &str,
323    workspace: &Entity<Workspace>,
324    cx: &mut gpui::VisualTestContext,
325) -> Box<dyn ItemHandle> {
326    let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
327    let worktree_id = project.update(cx, |project, cx| {
328        let worktree = project.worktrees(cx).last().expect("worktree not found");
329        worktree.read(cx).id()
330    });
331    let project_path = ProjectPath {
332        worktree_id,
333        path: rel_path(file_path).into(),
334    };
335    workspace
336        .update_in(cx, move |workspace, window, cx| {
337            workspace.open_path(project_path, None, true, window, cx)
338        })
339        .await
340        .unwrap()
341}
342
343#[track_caller]
344fn assert_match_selection(
345    tab_switcher: &Picker<TabSwitcherDelegate>,
346    expected_selection_index: usize,
347    expected_item: Box<dyn ItemHandle>,
348) {
349    assert_eq!(
350        tab_switcher.delegate.selected_index(),
351        expected_selection_index,
352        "item is not selected"
353    );
354    assert_match_at_position(tab_switcher, expected_selection_index, expected_item);
355}
356
357#[track_caller]
358fn assert_match_at_position(
359    tab_switcher: &Picker<TabSwitcherDelegate>,
360    match_index: usize,
361    expected_item: Box<dyn ItemHandle>,
362) {
363    let match_item = tab_switcher
364        .delegate
365        .matches
366        .get(match_index)
367        .unwrap_or_else(|| panic!("Tab Switcher has no match for index {match_index}"));
368    assert_eq!(match_item.item.item_id(), expected_item.item_id());
369}
370
371#[track_caller]
372fn assert_tab_switcher_is_closed(workspace: Entity<Workspace>, cx: &mut VisualTestContext) {
373    workspace.update(cx, |workspace, cx| {
374        assert!(
375            workspace.active_modal::<TabSwitcher>(cx).is_none(),
376            "tab switcher is still open"
377        );
378    });
379}