Refactor file_finder send element open code (#7210)

Follow-up of https://github.com/zed-industries/zed/pull/6947 (cc
@alygin) that fixes a few style nits and refactors the code around:
* use already stored `currently_opened_path` to decide what to do with
the history item sorting
* use the same method to set history items, encapsulate the bubbling up
logic there
* ensure history elements are properly sorted before populating

The main reason to change all that is the new comparator in the previous
version:
https://github.com/zed-industries/zed/pull/6947/files#diff-eac7c8c99856f77cee39117708cd1467fd5bbc8805da2564f851951638020842R234
that almost violated `util::extend_sorted` contract, requiring both
collections to be sorted the same way as the comparator would be: it did
work, because we bubbled currently open item up in the history items
list manually, and that we have only one such item.

Release Notes:
- N/A
This commit is contained in:
Kirill Bulatov 2024-02-01 14:35:42 +02:00 committed by GitHub
parent 0edffd9248
commit 0102ffbfca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 94 deletions

1
Cargo.lock generated
View File

@ -2661,6 +2661,7 @@ dependencies = [
"env_logger",
"fuzzy",
"gpui",
"itertools 0.11.0",
"language",
"menu",
"picker",

View File

@ -15,6 +15,7 @@ collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
itertools = "0.11"
menu = { path = "../menu" }
picker = { path = "../picker" }
postage.workspace = true

View File

@ -8,6 +8,7 @@ use gpui::{
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
@ -61,36 +62,18 @@ impl FileFinder {
let abs_path = project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
FoundPath::new_opened_file(project_path, abs_path)
FoundPath::new(project_path, abs_path)
});
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
let history_items = workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.chain(
workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.filter(|(history_path, _)| {
Some(history_path)
!= currently_opened_path
.as_ref()
.map(|found_path| &found_path.project)
})
.filter(|(_, history_abs_path)| {
history_abs_path.as_ref()
!= currently_opened_path
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path))
.collect::<Vec<_>>();
let project = workspace.project().clone();
let weak_workspace = cx.view().downgrade();
@ -209,39 +192,21 @@ impl Matches {
fn push_new_matches(
&mut self,
history_items: &Vec<FoundPath>,
currently_opened: Option<&FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
extend_old_matches: bool,
) {
let matching_history_paths = matching_history_item_paths(history_items, query);
let matching_history_paths =
matching_history_item_paths(history_items, currently_opened, query);
let new_search_matches = new_search_matches
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
let history_items_to_show = history_items.iter().filter_map(|history_item| {
Some((
history_item.clone(),
Some(
matching_history_paths
.get(&history_item.project.path)?
.clone(),
),
))
});
self.history.clear();
util::extend_sorted(
&mut self.history,
history_items_to_show,
100,
|(ap, a), (bp, b)| {
if ap.opened_file {
return cmp::Ordering::Less;
}
if bp.opened_file {
return cmp::Ordering::Greater;
}
b.cmp(a)
},
);
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));
@ -250,14 +215,59 @@ impl Matches {
}
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 history_items_to_show = history_items
.into_iter()
.chain(currently_opened)
.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)),
})
.sorted_by(|(path_a, match_a), (path_b, match_b)| {
match (
Some(path_a) == currently_opened,
Some(path_b) == currently_opened,
) {
(true, false) => cmp::Ordering::Less,
(false, true) => cmp::Ordering::Greater,
_ => match_b.cmp(match_a),
}
});
self.history.clear();
util::extend_sorted(
&mut self.history,
history_items_to_show.collect::<Vec<_>>(),
100,
|(path_a, match_a), (path_b, match_b)| match (
Some(path_a) == currently_opened,
Some(path_b) == currently_opened,
) {
(true, false) => cmp::Ordering::Less,
(false, true) => cmp::Ordering::Greater,
_ => match_b.cmp(match_a).then(path_b.cmp(path_a)),
},
);
}
}
fn matching_history_item_paths(
history_items: &Vec<FoundPath>,
currently_opened: Option<&FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
let history_items_by_worktrees = history_items
.iter()
.chain(currently_opened)
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
path: &found_path.project.path,
@ -309,28 +319,15 @@ fn matching_history_item_paths(
matching_history_paths
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct FoundPath {
project: ProjectPath,
absolute: Option<PathBuf>,
opened_file: bool,
}
impl FoundPath {
fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
Self {
project,
absolute,
opened_file: false,
}
}
fn new_opened_file(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
Self {
project,
absolute,
opened_file: true,
}
Self { project, absolute }
}
}
@ -474,6 +471,7 @@ impl FileFinderDelegate {
.map(|query| query.path_like.path_query());
self.matches.push_new_matches(
&self.history_items,
self.currently_opened_path.as_ref(),
&query,
matches.into_iter(),
extend_old_matches,
@ -652,18 +650,17 @@ impl FileFinderDelegate {
})
}
/// Calculates selection index after the user performed search.
/// Prefers to return 1 if the top visible item corresponds to the currently opened file, otherwise returns 0.
/// Skips first history match (that is displayed topmost) if it's currently opened.
fn calculate_selected_index(&self) -> usize {
let first = self.matches.history.get(0);
if let Some(first) = first {
if !first.0.opened_file {
return 0;
if let Some(Match::History(path, _)) = self.matches.get(0) {
if Some(path) == self.currently_opened_path.as_ref() {
let elements_after_first = self.matches.len() - 1;
if elements_after_first > 0 {
return 1;
}
}
} else {
return 0;
}
(self.matches.len() - 1).min(1)
0
}
}
@ -707,20 +704,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: 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())
})
.cloned()
.map(|p| (p, None))
.collect(),
history: Vec::new(),
search: Vec::new(),
};
self.matches.set_new_history(
self.currently_opened_path.as_ref(),
None,
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.selected_index = self.calculate_selected_index();
cx.notify();
Task::ready(())

View File

@ -1126,6 +1126,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, "lib.rs");
assert_match_at_position(finder, 2, "bar.rs");
});
// all files match, main.rs is still on top
@ -1446,15 +1447,21 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
}
}
#[track_caller]
fn assert_match_selection(
finder: &Picker<FileFinderDelegate>,
expected_selection_index: usize,
expected_file_name: &str,
) {
assert_eq!(finder.delegate.selected_index(), expected_selection_index);
assert_eq!(
finder.delegate.selected_index(),
expected_selection_index,
"Match is not selected"
);
assert_match_at_position(finder, expected_selection_index, expected_file_name);
}
#[track_caller]
fn assert_match_at_position(
finder: &Picker<FileFinderDelegate>,
match_index: usize,
@ -1464,11 +1471,12 @@ fn assert_match_at_position(
.delegate
.matches
.get(match_index)
.expect("Finder should have a match item with the given index");
.unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
let match_file_name = match match_item {
Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
Match::Search(path_match) => path_match.0.path.file_name(),
};
let match_file_name = match_file_name.unwrap().to_string_lossy().to_string();
assert_eq!(match_file_name.as_str(), expected_file_name);
}
.unwrap()
.to_string_lossy();
assert_eq!(match_file_name, expected_file_name);
}