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