open_path_prompt_tests.rs

  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}