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, 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, 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, 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(target_os = "windows")]
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, 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]
255async fn test_new_path_prompt(cx: &mut TestAppContext) {
256 let app_state = init_test(cx);
257 app_state
258 .fs
259 .as_fake()
260 .insert_tree(
261 path!("/root"),
262 json!({
263 "a1": "A1",
264 "a2": "A2",
265 "a3": "A3",
266 "dir1": {},
267 "dir2": {
268 "c": "C",
269 "d1": "D1",
270 "d2": "D2",
271 "d3": "D3",
272 "dir3": {},
273 "dir4": {}
274 }
275 }),
276 )
277 .await;
278
279 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
280
281 let (picker, cx) = build_open_path_prompt(project, true, cx);
282
283 insert_query(path!("/root"), &picker, cx).await;
284 assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
285
286 insert_query(path!("/root/d"), &picker, cx).await;
287 assert_eq!(
288 collect_match_candidates(&picker, cx),
289 vec!["d", "dir1", "dir2"]
290 );
291
292 insert_query(path!("/root/dir1"), &picker, cx).await;
293 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
294
295 insert_query(path!("/root/dir12"), &picker, cx).await;
296 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir12"]);
297
298 insert_query(path!("/root/dir1"), &picker, cx).await;
299 assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1"]);
300}
301
302fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
303 cx.update(|cx| {
304 let state = AppState::test(cx);
305 theme::init(theme::LoadThemes::JustBase, cx);
306 language::init(cx);
307 super::init(cx);
308 editor::init(cx);
309 workspace::init_settings(cx);
310 Project::init_settings(cx);
311 state
312 })
313}
314
315fn build_open_path_prompt(
316 project: Entity<Project>,
317 creating_path: bool,
318 cx: &mut TestAppContext,
319) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
320 let (tx, _) = futures::channel::oneshot::channel();
321 let lister = project::DirectoryLister::Project(project.clone());
322 let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
323
324 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
325 (
326 workspace.update_in(cx, |_, window, cx| {
327 cx.new(|cx| {
328 let picker = Picker::uniform_list(delegate, window, cx)
329 .width(rems(34.))
330 .modal(false);
331 let query = lister.default_query(cx);
332 picker.set_query(query, window, cx);
333 picker
334 })
335 }),
336 cx,
337 )
338}
339
340async fn insert_query(
341 query: &str,
342 picker: &Entity<Picker<OpenPathDelegate>>,
343 cx: &mut VisualTestContext,
344) {
345 picker
346 .update_in(cx, |f, window, cx| {
347 f.delegate.update_matches(query.to_string(), window, cx)
348 })
349 .await;
350}
351
352fn confirm_completion(
353 query: &str,
354 select: usize,
355 picker: &Entity<Picker<OpenPathDelegate>>,
356 cx: &mut VisualTestContext,
357) -> String {
358 picker
359 .update_in(cx, |f, window, cx| {
360 if f.delegate.selected_index() != select {
361 f.delegate.set_selected_index(select, window, cx);
362 }
363 f.delegate.confirm_completion(query.to_string(), window, cx)
364 })
365 .unwrap()
366}
367
368fn collect_match_candidates(
369 picker: &Entity<Picker<OpenPathDelegate>>,
370 cx: &mut VisualTestContext,
371) -> Vec<String> {
372 picker.update(cx, |f, _| f.delegate.collect_match_candidates())
373}