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