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