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