Cargo.lock ๐
@@ -3973,6 +3973,7 @@ dependencies = [
"menu",
"picker",
"project",
+ "serde",
"serde_json",
"settings",
"text",
Kirill Bulatov created
Cargo.lock | 1
crates/file_finder/Cargo.toml | 1
crates/file_finder/src/file_finder.rs | 236 ++++++++++-------
crates/file_finder/src/file_finder_tests.rs | 298 ++++++++++++++--------
crates/search/src/buffer_search.rs | 6
crates/zed/src/zed/app_menus.rs | 2
6 files changed, 339 insertions(+), 205 deletions(-)
@@ -3973,6 +3973,7 @@ dependencies = [
"menu",
"picker",
"project",
+ "serde",
"serde_json",
"settings",
"text",
@@ -24,6 +24,7 @@ menu.workspace = true
picker.workspace = true
project.workspace = true
settings.workspace = true
+serde.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true
@@ -3,13 +3,13 @@ mod file_finder_tests;
mod new_path_prompt;
-use collections::{HashMap, HashSet};
+use collections::{BTreeSet, HashMap};
use editor::{scroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
- actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
- Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View,
- ViewContext, VisualContext, WeakView,
+ actions, impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter,
+ FocusHandle, FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render,
+ Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use itertools::Itertools;
use new_path_prompt::NewPathPrompt;
@@ -29,7 +29,14 @@ use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::{item::PreviewTabsSettings, ModalView, Workspace};
-actions!(file_finder, [Toggle, SelectPrev]);
+actions!(file_finder, [SelectPrev]);
+impl_actions!(file_finder, [Toggle]);
+
+#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)]
+pub struct Toggle {
+ #[serde(default)]
+ pub separate_history: bool,
+}
impl ModalView for FileFinder {}
@@ -45,9 +52,9 @@ pub fn init(cx: &mut AppContext) {
impl FileFinder {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
- workspace.register_action(|workspace, _: &Toggle, cx| {
+ workspace.register_action(|workspace, action: &Toggle, cx| {
let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
- Self::open(workspace, cx);
+ Self::open(workspace, action.separate_history, cx);
return;
};
@@ -60,7 +67,7 @@ impl FileFinder {
});
}
- fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+ fn open(workspace: &mut Workspace, separate_history: bool, cx: &mut ViewContext<Workspace>) {
let project = workspace.project().read(cx);
let currently_opened_path = workspace
@@ -92,6 +99,7 @@ impl FileFinder {
project,
currently_opened_path,
history_items,
+ separate_history,
cx,
);
@@ -161,6 +169,7 @@ pub struct FileFinderDelegate {
has_changed_selected_index: bool,
cancel_flag: Arc<AtomicBool>,
history_items: Vec<FoundPath>,
+ separate_history: bool,
}
/// Use a custom ordering for file finder: the regular one
@@ -198,104 +207,117 @@ impl PartialOrd for ProjectPanelOrdMatch {
#[derive(Debug, Default)]
struct Matches {
- history: Vec<(FoundPath, Option<ProjectPanelOrdMatch>)>,
- search: Vec<ProjectPanelOrdMatch>,
+ separate_history: bool,
+ matches: Vec<Match>,
}
-#[derive(Debug)]
-enum Match<'a> {
- History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>),
- Search(&'a ProjectPanelOrdMatch),
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+enum Match {
+ History(FoundPath, Option<ProjectPanelOrdMatch>),
+ Search(ProjectPanelOrdMatch),
}
impl Matches {
fn len(&self) -> usize {
- self.history.len() + self.search.len()
+ self.matches.len()
}
- fn get(&self, index: usize) -> Option<Match<'_>> {
- if index < self.history.len() {
- self.history
- .get(index)
- .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
- } else {
- self.search
- .get(index - self.history.len())
- .map(Match::Search)
- }
+ fn get(&self, index: usize) -> Option<&Match> {
+ self.matches.get(index)
}
- fn push_new_matches(
- &mut self,
- history_items: &Vec<FoundPath>,
- currently_opened: Option<&FoundPath>,
- query: &PathLikeWithPosition<FileSearchQuery>,
+ fn push_new_matches<'a>(
+ &'a mut self,
+ history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
+ currently_opened: Option<&'a FoundPath>,
+ query: Option<&PathLikeWithPosition<FileSearchQuery>>,
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
extend_old_matches: bool,
) {
+ let no_history_score = 0;
let matching_history_paths =
- matching_history_item_paths(history_items, currently_opened, query);
+ matching_history_item_paths(history_items.clone(), currently_opened, query);
let new_search_matches = new_search_matches
- .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
-
- self.set_new_history(
- currently_opened,
- Some(&matching_history_paths),
- history_items,
- );
- if extend_old_matches {
- self.search
- .retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
- } else {
- self.search.clear();
- }
- util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
- }
-
- fn set_new_history<'a>(
- &mut self,
- currently_opened: Option<&'a FoundPath>,
- query_matches: Option<&'a HashMap<Arc<Path>, ProjectPanelOrdMatch>>,
- history_items: impl IntoIterator<Item = &'a FoundPath> + 'a,
- ) {
- let mut processed_paths = HashSet::default();
- self.history = history_items
+ .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path))
+ .map(Match::Search)
+ .map(|m| (no_history_score, m));
+ let old_search_matches = self
+ .matches
+ .drain(..)
+ .filter(|_| extend_old_matches)
+ .filter(|m| matches!(m, Match::Search(_)))
+ .map(|m| (no_history_score, m));
+ let history_matches = history_items
.into_iter()
.chain(currently_opened)
- .filter(|&path| processed_paths.insert(path))
- .filter_map(|history_item| match &query_matches {
- Some(query_matches) => Some((
- history_item.clone(),
- Some(query_matches.get(&history_item.project.path)?.clone()),
- )),
- None => Some((history_item.clone(), None)),
- })
.enumerate()
- .sorted_by(
- |(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match (
- Some(path_a) == currently_opened,
- Some(path_b) == currently_opened,
- ) {
+ .filter_map(|(i, history_item)| {
+ let query_match = matching_history_paths
+ .get(&history_item.project.path)
+ .cloned();
+ let query_match = if query.is_some() {
+ query_match?
+ } else {
+ query_match.flatten()
+ };
+ Some((i + 1, Match::History(history_item.clone(), query_match)))
+ });
+
+ let mut unique_matches = BTreeSet::new();
+ self.matches = old_search_matches
+ .chain(history_matches)
+ .chain(new_search_matches)
+ .filter(|(_, m)| unique_matches.insert(m.clone()))
+ .sorted_by(|(history_score_a, a), (history_score_b, b)| {
+ match (a, b) {
// bubble currently opened files to the top
- (true, false) => cmp::Ordering::Less,
- (false, true) => cmp::Ordering::Greater,
- // arrange the files by their score (best score on top) and by their occurrence in the history
- // (history items visited later are on the top)
- _ => match_b.cmp(match_a).then(index_a.cmp(index_b)),
- },
- )
- .map(|(_, paths)| paths)
+ (Match::History(path, _), _) if Some(path) == currently_opened => {
+ cmp::Ordering::Less
+ }
+ (_, Match::History(path, _)) if Some(path) == currently_opened => {
+ cmp::Ordering::Greater
+ }
+
+ (Match::History(_, _), Match::Search(_)) if self.separate_history => {
+ cmp::Ordering::Less
+ }
+ (Match::Search(_), Match::History(_, _)) if self.separate_history => {
+ cmp::Ordering::Greater
+ }
+
+ (Match::History(_, match_a), Match::History(_, match_b)) => match_b
+ .cmp(match_a)
+ .then(history_score_a.cmp(history_score_b)),
+ (Match::History(_, match_a), Match::Search(match_b)) => {
+ Some(match_b).cmp(&match_a.as_ref())
+ }
+ (Match::Search(match_a), Match::History(_, match_b)) => {
+ match_b.as_ref().cmp(&Some(match_a))
+ }
+ (Match::Search(match_a), Match::Search(match_b)) => match_b.cmp(match_a),
+ }
+ })
+ .take(100)
+ .map(|(_, m)| m)
.collect();
}
}
-fn matching_history_item_paths(
- history_items: &Vec<FoundPath>,
- currently_opened: Option<&FoundPath>,
- query: &PathLikeWithPosition<FileSearchQuery>,
-) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
+fn matching_history_item_paths<'a>(
+ history_items: impl IntoIterator<Item = &'a FoundPath>,
+ currently_opened: Option<&'a FoundPath>,
+ query: Option<&PathLikeWithPosition<FileSearchQuery>>,
+) -> HashMap<Arc<Path>, Option<ProjectPanelOrdMatch>> {
+ let Some(query) = query else {
+ return history_items
+ .into_iter()
+ .chain(currently_opened)
+ .map(|found_path| (Arc::clone(&found_path.project.path), None))
+ .collect();
+ };
+
let history_items_by_worktrees = history_items
- .iter()
+ .into_iter()
.chain(currently_opened)
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
@@ -340,7 +362,7 @@ fn matching_history_item_paths(
.map(|path_match| {
(
Arc::clone(&path_match.path),
- ProjectPanelOrdMatch(path_match),
+ Some(ProjectPanelOrdMatch(path_match)),
)
}),
);
@@ -399,6 +421,7 @@ impl FileFinderDelegate {
project: Model<Project>,
currently_opened_path: Option<FoundPath>,
history_items: Vec<FoundPath>,
+ separate_history: bool,
cx: &mut ViewContext<FileFinder>,
) -> Self {
Self::subscribe_to_updates(&project, cx);
@@ -416,6 +439,7 @@ impl FileFinderDelegate {
selected_index: 0,
cancel_flag: Arc::new(AtomicBool::new(false)),
history_items,
+ separate_history,
}
}
@@ -510,7 +534,7 @@ impl FileFinderDelegate {
self.matches.push_new_matches(
&self.history_items,
self.currently_opened_path.as_ref(),
- &query,
+ Some(&query),
matches.into_iter(),
extend_old_matches,
);
@@ -523,7 +547,7 @@ impl FileFinderDelegate {
fn labels_for_match(
&self,
- path_match: Match,
+ path_match: &Match,
cx: &AppContext,
ix: usize,
) -> (String, Vec<usize>, String, Vec<usize>) {
@@ -727,12 +751,21 @@ impl PickerDelegate for FileFinderDelegate {
}
fn separators_after_indices(&self) -> Vec<usize> {
- let history_items = self.matches.history.len();
- if history_items == 0 || self.matches.search.is_empty() {
- Vec::new()
- } else {
- vec![history_items - 1]
+ if self.separate_history {
+ let first_non_history_index = self
+ .matches
+ .matches
+ .iter()
+ .enumerate()
+ .find(|(_, m)| !matches!(m, Match::History(_, _)))
+ .map(|(i, _)| i);
+ if let Some(first_non_history_index) = first_non_history_index {
+ if first_non_history_index > 0 {
+ return vec![first_non_history_index - 1];
+ }
+ }
}
+ Vec::new()
}
fn update_matches(
@@ -746,18 +779,20 @@ impl PickerDelegate for FileFinderDelegate {
let project = self.project.read(cx);
self.latest_search_id = post_inc(&mut self.search_count);
self.matches = Matches {
- history: Vec::new(),
- search: Vec::new(),
+ separate_history: self.separate_history,
+ ..Matches::default()
};
- self.matches.set_new_history(
- self.currently_opened_path.as_ref(),
- None,
+ self.matches.push_new_matches(
self.history_items.iter().filter(|history_item| {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local() && history_item.absolute.is_some())
}),
+ self.currently_opened_path.as_ref(),
+ None,
+ None.into_iter(),
+ false,
);
self.selected_index = 0;
@@ -919,12 +954,23 @@ impl PickerDelegate for FileFinderDelegate {
.get(ix)
.expect("Invalid matches state: no element for index {ix}");
+ let icon = match &path_match {
+ Match::History(_, _) => Icon::new(IconName::HistoryRerun)
+ .color(Color::Muted)
+ .size(IconSize::Small)
+ .into_any_element(),
+ Match::Search(_) => v_flex()
+ .flex_none()
+ .size(IconSize::Small.rems())
+ .into_any_element(),
+ };
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match, cx, ix);
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
+ .end_slot::<AnyElement>(Some(icon))
.inset(true)
.selected(selected)
.child(
@@ -116,7 +116,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
.await;
picker.update(cx, |picker, _| {
assert_eq!(
- collect_search_matches(picker).search_only(),
+ collect_search_matches(picker).search_paths_only(),
vec![PathBuf::from("a/b/file2.txt")],
"Matching abs path should be the only match"
)
@@ -138,7 +138,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
.await;
picker.update(cx, |picker, _| {
assert_eq!(
- collect_search_matches(picker).search_only(),
+ collect_search_matches(picker).search_paths_only(),
Vec::<PathBuf>::new(),
"Mismatching abs path should produce no matches"
)
@@ -171,7 +171,7 @@ async fn test_complex_path(cx: &mut TestAppContext) {
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 1);
assert_eq!(
- collect_search_matches(picker).search_only(),
+ collect_search_matches(picker).search_paths_only(),
vec![PathBuf::from("ๅ
ถไป/Sๆฐๆฎ่กจๆ ผ/task.xlsx")],
)
});
@@ -369,12 +369,8 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
});
picker.update(cx, |picker, cx| {
+ let matches = collect_search_matches(picker).search_matches_only();
let delegate = &mut picker.delegate;
- assert!(
- delegate.matches.history.is_empty(),
- "Search matches expected"
- );
- let matches = delegate.matches.search.clone();
// Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found.
@@ -383,7 +379,10 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
delegate.latest_search_id,
true, // did-cancel
query.clone(),
- vec![matches[1].clone(), matches[3].clone()],
+ vec![
+ ProjectPanelOrdMatch(matches[1].clone()),
+ ProjectPanelOrdMatch(matches[3].clone()),
+ ],
cx,
);
@@ -393,15 +392,20 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
delegate.latest_search_id,
true, // did-cancel
query.clone(),
- vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
+ vec![
+ ProjectPanelOrdMatch(matches[0].clone()),
+ ProjectPanelOrdMatch(matches[2].clone()),
+ ProjectPanelOrdMatch(matches[3].clone()),
+ ],
cx,
);
- assert!(
- delegate.matches.history.is_empty(),
- "Search matches expected"
+ assert_eq!(
+ collect_search_matches(picker)
+ .search_matches_only()
+ .as_slice(),
+ &matches[0..4]
);
- assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
});
}
@@ -480,15 +484,11 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
cx.read(|cx| {
let picker = picker.read(cx);
let delegate = &picker.delegate;
- assert!(
- delegate.matches.history.is_empty(),
- "Search matches expected"
- );
- let matches = delegate.matches.search.clone();
+ let matches = collect_search_matches(picker).search_matches_only();
assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) =
- delegate.labels_for_path_match(&matches[0].0);
+ delegate.labels_for_path_match(&matches[0]);
assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "");
@@ -552,15 +552,10 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
})
.await;
- finder.update(cx, |f, _| {
- let delegate = &f.delegate;
- assert!(
- delegate.matches.history.is_empty(),
- "Search matches expected"
- );
- let matches = &delegate.matches.search;
- assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt"));
- assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt"));
+ finder.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_paths_only();
+ assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
+ assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
});
}
@@ -877,7 +872,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
for expected_selected_index in 0..current_history.len() {
- cx.dispatch_action(Toggle);
+ cx.dispatch_action(Toggle::default());
let picker = active_file_picker(&workspace, cx);
let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
assert_eq!(
@@ -886,7 +881,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
);
}
- cx.dispatch_action(Toggle);
+ cx.dispatch_action(Toggle::default());
let selected_index = workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<FileFinder>(cx)
@@ -945,20 +940,19 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
finder.delegate.update_matches(first_query.to_string(), cx)
})
.await;
- finder.update(cx, |finder, _| {
- let delegate = &finder.delegate;
- assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
- let history_match = delegate.matches.history.first().unwrap();
- assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
- assert_eq!(history_match.0, FoundPath::new(
+ finder.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker);
+ assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
+ let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
+ assert_eq!(history_match, &FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
));
- assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
- assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
+ assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
+ assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
});
let second_query = "fsdasdsa";
@@ -968,14 +962,11 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
finder.delegate.update_matches(second_query.to_string(), cx)
})
.await;
- finder.update(cx, |finder, _| {
- let delegate = &finder.delegate;
+ finder.update(cx, |picker, _| {
assert!(
- delegate.matches.history.is_empty(),
- "No history entries should match {second_query}"
- );
- assert!(
- delegate.matches.search.is_empty(),
+ collect_search_matches(picker)
+ .search_paths_only()
+ .is_empty(),
"No search entries should match {second_query}"
);
});
@@ -990,20 +981,19 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
.update_matches(first_query_again.to_string(), cx)
})
.await;
- finder.update(cx, |finder, _| {
- let delegate = &finder.delegate;
- assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
- let history_match = delegate.matches.history.first().unwrap();
- assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
- assert_eq!(history_match.0, FoundPath::new(
+ finder.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker);
+ assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
+ let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
+ assert_eq!(history_match, &FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
));
- assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
- assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
+ assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
+ assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
});
}
@@ -1139,6 +1129,9 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
assert_eq!(finder.delegate.matches.len(), 5);
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");
});
// main.rs is not among matches, select top item
@@ -1150,6 +1143,7 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_match_at_position(finder, 0, "bar.rs");
+ assert_match_at_position(finder, 1, "lib.rs");
});
// main.rs is back, put it on top and select next item
@@ -1162,6 +1156,7 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "moo.rs");
+ assert_match_at_position(finder, 2, "maaa.rs");
});
// get back to the initial state
@@ -1174,6 +1169,99 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_selection(finder, 0, "main.rs");
assert_match_at_position(finder, 1, "lib.rs");
+ assert_match_at_position(finder, 2, "bar.rs");
+ });
+}
+
+#[gpui::test]
+async fn test_non_separate_history_items(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "test": {
+ "bar.rs": "// Bar file",
+ "lib.rs": "// Lib file",
+ "maaa.rs": "// Maaaaaaa",
+ "main.rs": "// Main file",
+ "moo.rs": "// Moooooo",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+
+ open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
+ open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
+ open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
+
+ cx.dispatch_action(Toggle::default());
+ let picker = active_file_picker(&workspace, cx);
+ // main.rs is on top, previously used is selected
+ picker.update(cx, |finder, _| {
+ assert_eq!(finder.delegate.matches.len(), 3);
+ assert_match_selection(finder, 0, "main.rs");
+ assert_match_at_position(finder, 1, "lib.rs");
+ assert_match_at_position(finder, 2, "bar.rs");
+ });
+
+ // all files match, main.rs is still on top, but the second item is selected
+ picker
+ .update(cx, |finder, cx| {
+ finder.delegate.update_matches(".rs".to_string(), cx)
+ })
+ .await;
+ picker.update(cx, |finder, _| {
+ assert_eq!(finder.delegate.matches.len(), 5);
+ 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");
+ });
+
+ // main.rs is not among matches, select top item
+ picker
+ .update(cx, |finder, cx| {
+ finder.delegate.update_matches("b".to_string(), cx)
+ })
+ .await;
+ picker.update(cx, |finder, _| {
+ assert_eq!(finder.delegate.matches.len(), 2);
+ assert_match_at_position(finder, 0, "bar.rs");
+ assert_match_at_position(finder, 1, "lib.rs");
+ });
+
+ // main.rs is back, put it on top and select next item
+ picker
+ .update(cx, |finder, cx| {
+ finder.delegate.update_matches("m".to_string(), cx)
+ })
+ .await;
+ picker.update(cx, |finder, _| {
+ assert_eq!(finder.delegate.matches.len(), 3);
+ assert_match_at_position(finder, 0, "main.rs");
+ assert_match_selection(finder, 1, "moo.rs");
+ assert_match_at_position(finder, 2, "maaa.rs");
+ });
+
+ // get back to the initial state
+ picker
+ .update(cx, |finder, cx| {
+ finder.delegate.update_matches("".to_string(), cx)
+ })
+ .await;
+ picker.update(cx, |finder, _| {
+ assert_eq!(finder.delegate.matches.len(), 3);
+ assert_match_selection(finder, 0, "main.rs");
+ assert_match_at_position(finder, 1, "lib.rs");
+ assert_match_at_position(finder, 2, "bar.rs");
});
}
@@ -1266,19 +1354,8 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
let finder = open_file_picker(&workspace, cx);
let query = "collab_ui";
cx.simulate_input(query);
- finder.update(cx, |finder, _| {
- let delegate = &finder.delegate;
- assert!(
- delegate.matches.history.is_empty(),
- "History items should not math query {query}, they should be matched by name only"
- );
-
- let search_entries = delegate
- .matches
- .search
- .iter()
- .map(|path_match| path_match.0.path.to_path_buf())
- .collect::<Vec<_>>();
+ finder.update(cx, |picker, _| {
+ let search_entries = collect_search_matches(picker).search_paths_only();
assert_eq!(
search_entries,
vec![
@@ -1321,15 +1398,9 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
let picker = open_file_picker(&workspace, cx);
cx.simulate_input("rs");
- picker.update(cx, |finder, _| {
- let history_entries = finder.delegate
- .matches
- .history
- .iter()
- .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
- .collect::<Vec<_>>();
+ picker.update(cx, |picker, _| {
assert_eq!(
- history_entries,
+ collect_search_matches(picker).history,
vec![
PathBuf::from("test/first.rs"),
PathBuf::from("test/third.rs"),
@@ -1582,7 +1653,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav(
// Back to navigation with initial shortcut
// Open file on modifiers release
cx.simulate_modifiers_change(Modifiers::secondary_key());
- cx.dispatch_action(Toggle);
+ cx.dispatch_action(Toggle::default());
cx.simulate_modifiers_change(Modifiers::none());
cx.read(|cx| {
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
@@ -1776,7 +1847,9 @@ fn open_file_picker(
workspace: &View<Workspace>,
cx: &mut VisualTestContext,
) -> View<Picker<FileFinderDelegate>> {
- cx.dispatch_action(Toggle);
+ cx.dispatch_action(Toggle {
+ separate_history: true,
+ });
active_file_picker(workspace, cx)
}
@@ -1795,15 +1868,17 @@ fn active_file_picker(
})
}
-#[derive(Debug)]
+#[derive(Debug, Default)]
struct SearchEntries {
history: Vec<PathBuf>,
+ history_found_paths: Vec<FoundPath>,
search: Vec<PathBuf>,
+ search_matches: Vec<PathMatch>,
}
impl SearchEntries {
#[track_caller]
- fn search_only(self) -> Vec<PathBuf> {
+ fn search_paths_only(self) -> Vec<PathBuf> {
assert!(
self.history.is_empty(),
"Should have no history matches, but got: {:?}",
@@ -1811,35 +1886,50 @@ impl SearchEntries {
);
self.search
}
+
+ #[track_caller]
+ fn search_matches_only(self) -> Vec<PathMatch> {
+ assert!(
+ self.history.is_empty(),
+ "Should have no history matches, but got: {:?}",
+ self.history
+ );
+ self.search_matches
+ }
}
fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
- let matches = &picker.delegate.matches;
- SearchEntries {
- history: matches
- .history
- .iter()
- .map(|(history_path, path_match)| {
- path_match
- .as_ref()
- .map(|path_match| {
- Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
- })
- .unwrap_or_else(|| {
- history_path
- .absolute
- .as_deref()
- .unwrap_or_else(|| &history_path.project.path)
- .to_path_buf()
- })
- })
- .collect(),
- search: matches
- .search
- .iter()
- .map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
- .collect(),
+ let mut search_entries = SearchEntries::default();
+ for m in &picker.delegate.matches.matches {
+ match m {
+ Match::History(history_path, path_match) => {
+ search_entries.history.push(
+ path_match
+ .as_ref()
+ .map(|path_match| {
+ Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
+ })
+ .unwrap_or_else(|| {
+ history_path
+ .absolute
+ .as_deref()
+ .unwrap_or_else(|| &history_path.project.path)
+ .to_path_buf()
+ }),
+ );
+ search_entries
+ .history_found_paths
+ .push(history_path.clone());
+ }
+ Match::Search(path_match) => {
+ search_entries
+ .search
+ .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
+ search_entries.search_matches.push(path_match.0.clone());
+ }
+ }
}
+ search_entries
}
#[track_caller]
@@ -42,13 +42,9 @@ const MIN_INPUT_WIDTH_REMS: f32 = 10.;
const MAX_INPUT_WIDTH_REMS: f32 = 30.;
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
-const fn true_value() -> bool {
- true
-}
-
#[derive(PartialEq, Clone, Deserialize)]
pub struct Deploy {
- #[serde(default = "true_value")]
+ #[serde(default = "util::serde::default_true")]
pub focus: bool,
#[serde(default)]
pub replace_enabled: bool,
@@ -138,7 +138,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::separator(),
MenuItem::action("Command Palette...", command_palette::Toggle),
MenuItem::separator(),
- MenuItem::action("Go to File...", file_finder::Toggle),
+ MenuItem::action("Go to File...", file_finder::Toggle::default()),
// MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
MenuItem::action("Go to Symbol in Editor...", outline::Toggle),
MenuItem::action("Go to Line/Column...", go_to_line::Toggle),