file_finder: Add option to create new file (#31567)

https://github.com/user-attachments/assets/7c8a05a1-8d59-4371-a1d6-a8cb82aa13b9

While implementing this, I noticed that currently when the search panel
displays only one result, the box oscillates a bit up and down like so:


https://github.com/user-attachments/assets/dd1520e2-fa0b-4307-b27a-984e69b0a644

Not sure how to fix this at the moment, maybe that could be another PR?

Release Notes:

- Add option to create new file in project search panel.
This commit is contained in:
Daniel Zhu 2025-06-03 10:44:57 -07:00 committed by GitHub
parent 1bc052d76b
commit de225fd242
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 117 additions and 33 deletions

View file

@ -196,7 +196,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
cx.simulate_input("bna");
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 2);
assert_eq!(picker.delegate.matches.len(), 3);
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
@ -229,7 +229,12 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
picker.update(cx, |picker, _| {
assert_eq!(
picker.delegate.matches.len(),
1,
// existence of CreateNew option depends on whether path already exists
if bandana_query == util::separator!("a/bandana") {
1
} else {
2
},
"Wrong number of matches for bandana query '{bandana_query}'. Matches: {:?}",
picker.delegate.matches
);
@ -269,9 +274,9 @@ async fn test_unicode_paths(cx: &mut TestAppContext) {
cx.simulate_input("g");
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 1);
assert_eq!(picker.delegate.matches.len(), 2);
assert_match_at_position(picker, 1, "g");
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
cx.read(|cx| {
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
@ -365,13 +370,12 @@ async fn test_complex_path(cx: &mut TestAppContext) {
cx.simulate_input("t");
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 1);
assert_eq!(picker.delegate.matches.len(), 2);
assert_eq!(
collect_search_matches(picker).search_paths_only(),
vec![PathBuf::from("其他/S数据表格/task.xlsx")],
)
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
cx.read(|cx| {
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
@ -416,8 +420,9 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
})
.await;
picker.update(cx, |finder, _| {
assert_match_at_position(finder, 1, &query_inside_file.to_string());
let finder = &finder.delegate;
assert_eq!(finder.matches.len(), 1);
assert_eq!(finder.matches.len(), 2);
let latest_search_query = finder
.latest_search_query
.as_ref()
@ -431,7 +436,6 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
);
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
@ -491,8 +495,9 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
})
.await;
picker.update(cx, |finder, _| {
assert_match_at_position(finder, 1, &query_outside_file.to_string());
let delegate = &finder.delegate;
assert_eq!(delegate.matches.len(), 1);
assert_eq!(delegate.matches.len(), 2);
let latest_search_query = delegate
.latest_search_query
.as_ref()
@ -506,7 +511,6 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
);
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
@ -561,7 +565,8 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
.await;
picker.update(cx, |picker, _cx| {
assert_eq!(picker.delegate.matches.len(), 5)
// CreateNew option not shown in this case since file already exists
assert_eq!(picker.delegate.matches.len(), 5);
});
picker.update_in(cx, |picker, window, cx| {
@ -959,7 +964,8 @@ async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
.await;
cx.read(|cx| {
let finder = picker.read(cx);
assert_eq!(finder.delegate.matches.len(), 0);
assert_eq!(finder.delegate.matches.len(), 1);
assert_match_at_position(finder, 0, "dir");
});
}
@ -1518,12 +1524,13 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 5);
assert_eq!(finder.delegate.matches.len(), 6);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "bar.rs");
assert_match_at_position(finder, 2, "lib.rs");
assert_match_at_position(finder, 3, "moo.rs");
assert_match_at_position(finder, 4, "maaa.rs");
assert_match_at_position(finder, 5, ".rs");
});
// main.rs is not among matches, select top item
@ -1533,9 +1540,10 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "bar.rs");
assert_match_at_position(finder, 1, "lib.rs");
assert_match_at_position(finder, 2, "b");
});
// main.rs is back, put it on top and select next item
@ -1545,10 +1553,11 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_eq!(finder.delegate.matches.len(), 4);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "moo.rs");
assert_match_at_position(finder, 2, "maaa.rs");
assert_match_at_position(finder, 3, "m");
});
// get back to the initial state
@ -1623,12 +1632,13 @@ async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppC
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 5);
assert_eq!(finder.delegate.matches.len(), 6);
assert_match_selection(finder, 0, "main.rs");
assert_match_at_position(finder, 1, "bar.rs");
assert_match_at_position(finder, 2, "lib.rs");
assert_match_at_position(finder, 3, "moo.rs");
assert_match_at_position(finder, 4, "maaa.rs");
assert_match_at_position(finder, 5, ".rs");
});
}
@ -1679,12 +1689,13 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) {
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 5);
assert_eq!(finder.delegate.matches.len(), 6);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "moo.rs");
assert_match_at_position(finder, 2, "bar.rs");
assert_match_at_position(finder, 3, "lib.rs");
assert_match_at_position(finder, 4, "maaa.rs");
assert_match_at_position(finder, 5, ".rs");
});
// main.rs is not among matches, select top item
@ -1694,9 +1705,10 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) {
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "bar.rs");
assert_match_at_position(finder, 1, "lib.rs");
assert_match_at_position(finder, 2, "b");
});
// main.rs is back, put it on top and select next item
@ -1706,10 +1718,11 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) {
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_eq!(finder.delegate.matches.len(), 4);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "moo.rs");
assert_match_at_position(finder, 2, "maaa.rs");
assert_match_at_position(finder, 3, "m");
});
// get back to the initial state
@ -1965,9 +1978,10 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp
let picker = open_file_picker(&workspace, cx);
cx.simulate_input("rs");
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "lib.rs");
assert_match_at_position(finder, 1, "main.rs");
assert_match_at_position(finder, 2, "rs");
});
// Delete main.rs
@ -1980,8 +1994,9 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp
// main.rs is in not among search results anymore
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 1);
assert_eq!(finder.delegate.matches.len(), 2);
assert_match_at_position(finder, 0, "lib.rs");
assert_match_at_position(finder, 1, "rs");
});
// Create util.rs
@ -1994,9 +2009,10 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp
// util.rs is among search results
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "lib.rs");
assert_match_at_position(finder, 1, "util.rs");
assert_match_at_position(finder, 2, "rs");
});
}
@ -2036,9 +2052,10 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
let picker = open_file_picker(&workspace, cx);
cx.simulate_input("rs");
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "bar.rs");
assert_match_at_position(finder, 1, "lib.rs");
assert_match_at_position(finder, 2, "rs");
});
// Add new worktree
@ -2054,10 +2071,11 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
// main.rs is among search results
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_eq!(finder.delegate.matches.len(), 4);
assert_match_at_position(finder, 0, "bar.rs");
assert_match_at_position(finder, 1, "lib.rs");
assert_match_at_position(finder, 2, "main.rs");
assert_match_at_position(finder, 3, "rs");
});
// Remove the first worktree
@ -2068,8 +2086,9 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
// Files from the first worktree are not in the search results anymore
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 1);
assert_eq!(finder.delegate.matches.len(), 2);
assert_match_at_position(finder, 0, "main.rs");
assert_match_at_position(finder, 1, "rs");
});
}
@ -2414,7 +2433,7 @@ async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
cx.run_until_parked();
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 6);
assert_eq!(picker.delegate.matches.len(), 7);
assert_eq!(picker.delegate.selected_index, 0);
});
@ -2426,7 +2445,7 @@ async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
cx.run_until_parked();
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 6);
assert_eq!(picker.delegate.matches.len(), 7);
assert_eq!(picker.delegate.selected_index, 3);
});
}
@ -2468,7 +2487,7 @@ async fn open_queried_buffer(
let history_items = picker.update(cx, |finder, _| {
assert_eq!(
finder.delegate.matches.len(),
expected_matches,
expected_matches + 1, // +1 from CreateNew option
"Unexpected number of matches found for query `{input}`, matches: {:?}",
finder.delegate.matches
);
@ -2617,6 +2636,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
.push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
search_entries.search_matches.push(path_match.0.clone());
}
Match::CreateNew(_) => {}
}
}
search_entries
@ -2650,6 +2670,7 @@ fn assert_match_at_position(
let match_file_name = match &match_item {
Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
Match::Search(path_match) => path_match.0.path.file_name(),
Match::CreateNew(project_path) => project_path.path.file_name(),
}
.unwrap()
.to_string_lossy();