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