tab_switcher_tests.rs

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