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}