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