1use std::sync::Arc;
2
3use gpui::{AppContext, Entity, TestAppContext, VisualTestContext};
4use picker::{Picker, PickerDelegate};
5use project::Project;
6use serde_json::json;
7use ui::rems;
8use util::{path, paths::PathStyle};
9use workspace::{AppState, Workspace};
10
11use crate::OpenPathDelegate;
12
13#[gpui::test]
14async fn test_open_path_prompt(cx: &mut TestAppContext) {
15 let app_state = init_test(cx);
16 app_state
17 .fs
18 .as_fake()
19 .insert_tree(
20 path!("/root"),
21 json!({
22 "a1": "A1",
23 "a2": "A2",
24 "a3": "A3",
25 "dir1": {},
26 "dir2": {
27 "c": "C",
28 "d1": "D1",
29 "d2": "D2",
30 "d3": "D3",
31 "dir3": {},
32 "dir4": {}
33 }
34 }),
35 )
36 .await;
37
38 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
39
40 let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
41
42 insert_query(path!("sadjaoislkdjasldj"), &picker, cx).await;
43 assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
44
45 let query = path!("/root");
46 insert_query(query, &picker, cx).await;
47 assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
48
49 #[cfg(not(windows))]
50 let expected_separator = "./";
51 #[cfg(windows)]
52 let expected_separator = ".\\";
53
54 // If the query ends with a slash, the picker should show the contents of the directory.
55 let query = path!("/root/");
56 insert_query(query, &picker, cx).await;
57 assert_eq!(
58 collect_match_candidates(&picker, cx),
59 vec![expected_separator, "a1", "a2", "a3", "dir1", "dir2"]
60 );
61
62 // Show candidates for the query "a".
63 let query = path!("/root/a");
64 insert_query(query, &picker, cx).await;
65 assert_eq!(
66 collect_match_candidates(&picker, cx),
67 vec!["a1", "a2", "a3"]
68 );
69
70 // Show candidates for the query "d".
71 let query = path!("/root/d");
72 insert_query(query, &picker, cx).await;
73 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
74
75 let query = path!("/root/dir2");
76 insert_query(query, &picker, cx).await;
77 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir2"]);
78
79 let query = path!("/root/dir2/");
80 insert_query(query, &picker, cx).await;
81 assert_eq!(
82 collect_match_candidates(&picker, cx),
83 vec![expected_separator, "c", "d1", "d2", "d3", "dir3", "dir4"]
84 );
85
86 // Show candidates for the query "d".
87 let query = path!("/root/dir2/d");
88 insert_query(query, &picker, cx).await;
89 assert_eq!(
90 collect_match_candidates(&picker, cx),
91 vec!["d1", "d2", "d3", "dir3", "dir4"]
92 );
93
94 let query = path!("/root/dir2/di");
95 insert_query(query, &picker, cx).await;
96 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir3", "dir4"]);
97}
98
99#[gpui::test]
100async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
101 let app_state = init_test(cx);
102 app_state
103 .fs
104 .as_fake()
105 .insert_tree(
106 path!("/root"),
107 json!({
108 "a": "A",
109 "dir1": {},
110 "dir2": {
111 "c": "C",
112 "d": "D",
113 "dir3": {},
114 "dir4": {}
115 }
116 }),
117 )
118 .await;
119
120 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
121
122 let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
123
124 // Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
125 let query = path!("/root");
126 insert_query(query, &picker, cx).await;
127 assert_eq!(
128 confirm_completion(query, 0, &picker, cx).unwrap(),
129 path!("/root/")
130 );
131
132 // Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash.
133 let query = path!("/root/");
134 insert_query(query, &picker, cx).await;
135 assert_eq!(
136 confirm_completion(query, 0, &picker, cx),
137 None,
138 "First entry is `./` and when we confirm completion, it is tabbed below"
139 );
140 assert_eq!(
141 confirm_completion(query, 1, &picker, cx).unwrap(),
142 path!("/root/a"),
143 "Second entry is the first entry of a directory that we want to be completed"
144 );
145
146 // Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash.
147 let query = path!("/root/");
148 insert_query(query, &picker, cx).await;
149 assert_eq!(
150 confirm_completion(query, 2, &picker, cx).unwrap(),
151 path!("/root/dir1/")
152 );
153
154 let query = path!("/root/a");
155 insert_query(query, &picker, cx).await;
156 assert_eq!(
157 confirm_completion(query, 0, &picker, cx).unwrap(),
158 path!("/root/a")
159 );
160
161 let query = path!("/root/d");
162 insert_query(query, &picker, cx).await;
163 assert_eq!(
164 confirm_completion(query, 1, &picker, cx).unwrap(),
165 path!("/root/dir2/")
166 );
167
168 let query = path!("/root/dir2");
169 insert_query(query, &picker, cx).await;
170 assert_eq!(
171 confirm_completion(query, 0, &picker, cx).unwrap(),
172 path!("/root/dir2/")
173 );
174
175 let query = path!("/root/dir2/");
176 insert_query(query, &picker, cx).await;
177 assert_eq!(
178 confirm_completion(query, 1, &picker, cx).unwrap(),
179 path!("/root/dir2/c")
180 );
181
182 let query = path!("/root/dir2/");
183 insert_query(query, &picker, cx).await;
184 assert_eq!(
185 confirm_completion(query, 3, &picker, cx).unwrap(),
186 path!("/root/dir2/dir3/")
187 );
188
189 let query = path!("/root/dir2/d");
190 insert_query(query, &picker, cx).await;
191 assert_eq!(
192 confirm_completion(query, 0, &picker, cx).unwrap(),
193 path!("/root/dir2/d")
194 );
195
196 let query = path!("/root/dir2/d");
197 insert_query(query, &picker, cx).await;
198 assert_eq!(
199 confirm_completion(query, 1, &picker, cx).unwrap(),
200 path!("/root/dir2/dir3/")
201 );
202
203 let query = path!("/root/dir2/di");
204 insert_query(query, &picker, cx).await;
205 assert_eq!(
206 confirm_completion(query, 1, &picker, cx).unwrap(),
207 path!("/root/dir2/dir4/")
208 );
209}
210
211#[gpui::test]
212#[cfg_attr(not(target_os = "windows"), ignore)]
213async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
214 let app_state = init_test(cx);
215 app_state
216 .fs
217 .as_fake()
218 .insert_tree(
219 path!("/root"),
220 json!({
221 "a": "A",
222 "dir1": {},
223 "dir2": {}
224 }),
225 )
226 .await;
227
228 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
229
230 let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx);
231
232 // Support both forward and backward slashes.
233 let query = "C:/root/";
234 insert_query(query, &picker, cx).await;
235 assert_eq!(
236 collect_match_candidates(&picker, cx),
237 vec![".\\", "a", "dir1", "dir2"]
238 );
239 assert_eq!(
240 confirm_completion(query, 0, &picker, cx),
241 None,
242 "First entry is `.\\` and when we confirm completion, it is tabbed below"
243 );
244 assert_eq!(
245 confirm_completion(query, 1, &picker, cx).unwrap(),
246 "C:/root/a",
247 "Second entry is the first entry of a directory that we want to be completed"
248 );
249
250 let query = "C:\\root/";
251 insert_query(query, &picker, cx).await;
252 assert_eq!(
253 collect_match_candidates(&picker, cx),
254 vec![".\\", "a", "dir1", "dir2"]
255 );
256 assert_eq!(
257 confirm_completion(query, 1, &picker, cx).unwrap(),
258 "C:\\root/a"
259 );
260
261 let query = "C:\\root\\";
262 insert_query(query, &picker, cx).await;
263 assert_eq!(
264 collect_match_candidates(&picker, cx),
265 vec![".\\", "a", "dir1", "dir2"]
266 );
267 assert_eq!(
268 confirm_completion(query, 1, &picker, cx).unwrap(),
269 "C:\\root\\a"
270 );
271
272 // Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
273 let query = "C:/root/d";
274 insert_query(query, &picker, cx).await;
275 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
276 assert_eq!(
277 confirm_completion(query, 1, &picker, cx).unwrap(),
278 "C:/root/dir2\\"
279 );
280
281 let query = "C:\\root/d";
282 insert_query(query, &picker, cx).await;
283 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
284 assert_eq!(
285 confirm_completion(query, 0, &picker, cx).unwrap(),
286 "C:\\root/dir1\\"
287 );
288
289 let query = "C:\\root\\d";
290 insert_query(query, &picker, cx).await;
291 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
292 assert_eq!(
293 confirm_completion(query, 0, &picker, cx).unwrap(),
294 "C:\\root\\dir1\\"
295 );
296}
297
298#[gpui::test]
299#[cfg_attr(not(target_os = "windows"), ignore)]
300async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
301 let app_state = init_test(cx);
302 app_state
303 .fs
304 .as_fake()
305 .insert_tree(
306 "/root",
307 json!({
308 "a": "A",
309 "dir1": {},
310 "dir2": {}
311 }),
312 )
313 .await;
314
315 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
316
317 let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx);
318
319 let query = "/root/";
320 insert_query(query, &picker, cx).await;
321 assert_eq!(
322 collect_match_candidates(&picker, cx),
323 vec!["./", "a", "dir1", "dir2"]
324 );
325 assert_eq!(
326 confirm_completion(query, 1, &picker, cx).unwrap(),
327 "/root/a"
328 );
329
330 // Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
331 let query = "/root/d";
332 insert_query(query, &picker, cx).await;
333 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
334 assert_eq!(
335 confirm_completion(query, 1, &picker, cx).unwrap(),
336 "/root/dir2/"
337 );
338
339 let query = "/root/d";
340 insert_query(query, &picker, cx).await;
341 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
342 assert_eq!(
343 confirm_completion(query, 0, &picker, cx).unwrap(),
344 "/root/dir1/"
345 );
346}
347
348#[gpui::test]
349async fn test_new_path_prompt(cx: &mut TestAppContext) {
350 let app_state = init_test(cx);
351 app_state
352 .fs
353 .as_fake()
354 .insert_tree(
355 path!("/root"),
356 json!({
357 "a1": "A1",
358 "a2": "A2",
359 "a3": "A3",
360 "dir1": {},
361 "dir2": {
362 "c": "C",
363 "d1": "D1",
364 "d2": "D2",
365 "d3": "D3",
366 "dir3": {},
367 "dir4": {}
368 }
369 }),
370 )
371 .await;
372
373 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
374
375 let (picker, cx) = build_open_path_prompt(project, true, PathStyle::local(), cx);
376
377 insert_query(path!("/root"), &picker, cx).await;
378 assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
379
380 insert_query(path!("/root/d"), &picker, cx).await;
381 assert_eq!(
382 collect_match_candidates(&picker, cx),
383 vec!["d", "dir1", "dir2"]
384 );
385
386 insert_query(path!("/root/dir1"), &picker, cx).await;
387 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
388
389 insert_query(path!("/root/dir12"), &picker, cx).await;
390 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir12"]);
391
392 insert_query(path!("/root/dir1"), &picker, cx).await;
393 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
394}
395
396fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
397 cx.update(|cx| {
398 let state = AppState::test(cx);
399 theme::init(theme::LoadThemes::JustBase, cx);
400 language::init(cx);
401 super::init(cx);
402 editor::init(cx);
403 workspace::init_settings(cx);
404 Project::init_settings(cx);
405 state
406 })
407}
408
409fn build_open_path_prompt(
410 project: Entity<Project>,
411 creating_path: bool,
412 path_style: PathStyle,
413 cx: &mut TestAppContext,
414) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
415 let (tx, _) = futures::channel::oneshot::channel();
416 let lister = project::DirectoryLister::Project(project.clone());
417 let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style);
418
419 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
420 (
421 workspace.update_in(cx, |_, window, cx| {
422 cx.new(|cx| {
423 let picker = Picker::uniform_list(delegate, window, cx)
424 .width(rems(34.))
425 .modal(false);
426 let query = lister.default_query(cx);
427 picker.set_query(query, window, cx);
428 picker
429 })
430 }),
431 cx,
432 )
433}
434
435async fn insert_query(
436 query: &str,
437 picker: &Entity<Picker<OpenPathDelegate>>,
438 cx: &mut VisualTestContext,
439) {
440 picker
441 .update_in(cx, |f, window, cx| {
442 f.delegate.update_matches(query.to_string(), window, cx)
443 })
444 .await;
445}
446
447fn confirm_completion(
448 query: &str,
449 select: usize,
450 picker: &Entity<Picker<OpenPathDelegate>>,
451 cx: &mut VisualTestContext,
452) -> Option<String> {
453 picker.update_in(cx, |f, window, cx| {
454 if f.delegate.selected_index() != select {
455 f.delegate.set_selected_index(select, window, cx);
456 }
457 f.delegate.confirm_completion(query.to_string(), window, cx)
458 })
459}
460
461fn collect_match_candidates(
462 picker: &Entity<Picker<OpenPathDelegate>>,
463 cx: &mut VisualTestContext,
464) -> Vec<String> {
465 picker.update(cx, |f, _| f.delegate.collect_match_candidates())
466}