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, 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, 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, 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
254fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
255 cx.update(|cx| {
256 let state = AppState::test(cx);
257 theme::init(theme::LoadThemes::JustBase, cx);
258 language::init(cx);
259 super::init(cx);
260 editor::init(cx);
261 workspace::init_settings(cx);
262 Project::init_settings(cx);
263 state
264 })
265}
266
267fn build_open_path_prompt(
268 project: Entity<Project>,
269 cx: &mut TestAppContext,
270) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
271 let (tx, _) = futures::channel::oneshot::channel();
272 let lister = project::DirectoryLister::Project(project.clone());
273 let delegate = OpenPathDelegate::new(tx, lister.clone());
274
275 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
276 (
277 workspace.update_in(cx, |_, window, cx| {
278 cx.new(|cx| {
279 let picker = Picker::uniform_list(delegate, window, cx)
280 .width(rems(34.))
281 .modal(false);
282 let query = lister.default_query(cx);
283 picker.set_query(query, window, cx);
284 picker
285 })
286 }),
287 cx,
288 )
289}
290
291async fn insert_query(
292 query: &str,
293 picker: &Entity<Picker<OpenPathDelegate>>,
294 cx: &mut VisualTestContext,
295) {
296 picker
297 .update_in(cx, |f, window, cx| {
298 f.delegate.update_matches(query.to_string(), window, cx)
299 })
300 .await;
301}
302
303fn confirm_completion(
304 query: &str,
305 select: usize,
306 picker: &Entity<Picker<OpenPathDelegate>>,
307 cx: &mut VisualTestContext,
308) -> String {
309 picker
310 .update_in(cx, |f, window, cx| {
311 if f.delegate.selected_index() != select {
312 f.delegate.set_selected_index(select, window, cx);
313 }
314 f.delegate.confirm_completion(query.to_string(), window, cx)
315 })
316 .unwrap()
317}
318
319fn collect_match_candidates(
320 picker: &Entity<Picker<OpenPathDelegate>>,
321 cx: &mut VisualTestContext,
322) -> Vec<String> {
323 picker.update(cx, |f, _| f.delegate.collect_match_candidates())
324}