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