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