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