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}