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