Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions crates/base/src/input/editor/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ impl SearchSession {
}

pub(crate) fn update_query(&mut self, query: impl Into<String>, case_insensitive: bool) {
self.query = query.into();
let query = query.into();
if self.query == query && self.case_insensitive == case_insensitive {
return;
}

self.query = query;
self.case_insensitive = case_insensitive;
self.matcher.update_query(&self.query, case_insensitive);
}
Expand All @@ -76,19 +81,25 @@ impl<M: InputModeKind> InputBaseState<M> {
self.search_session
.open(replace_mode, self.is_replaceable());
let selected = self.selected_text().to_string();
if !selected.is_empty() {
self.search_session.query = selected;
}
self.search_session.anchor_offset = self
.last_layout
.as_ref()
.map(|layout| layout.visible_range_offset.start);
self.search_session.matcher.update_query(
&self.search_session.query,
self.search_session.case_insensitive,
);
let query = if selected.is_empty() {
self.search_session.query.clone()
} else {
selected
};
let query_changed = query != self.search_session.query;
// A retained query resumes its previous occurrence. Only a new query
// is anchored to the current viewport.
self.search_session.anchor_offset = if query_changed {
self.last_layout
.as_ref()
.map(|layout| layout.visible_range_offset.start)
} else {
None
};
let case_insensitive = self.search_session.case_insensitive;
self.search_session.update_query(query, case_insensitive);
self.search_session.matcher.update(&self.text);
if let Some(anchor) = self.search_session.anchor_offset {
if query_changed && let Some(anchor) = self.search_session.anchor_offset {
self.search_session.matcher.update_cursor_by_offset(anchor);
}
cx.notify();
Expand Down Expand Up @@ -404,6 +415,23 @@ mod tests {
assert_eq!(matcher.next(), Some(5..10));
}

#[test]
fn identical_query_keeps_the_current_match() {
let mut session = SearchSession::default();
session.update_query("foo", true);
session.matcher.update(&Rope::from("foo bar foo baz foo"));
session.matcher.update_cursor_by_offset(12);
assert_eq!(session.matcher.current_match_index(), 2);

// Reopening Find and the styled search panel's initial query echo both
// update the session with the same query. Neither should reset the
// previously active occurrence.
session.update_query("foo", true);

assert_eq!(session.matcher.current_match_index(), 2);
assert_eq!(session.matcher.label(), "3/3");
}

#[test]
fn replacement_keeps_current_match_index_on_next_match() {
let mut matcher = SearchMatcher::new();
Expand Down
39 changes: 39 additions & 0 deletions crates/component/src/input/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,45 @@ mod tests {
});
}

/// Reopening search must preserve navigation already owned by Base.
///
/// Closing and reopening Find keeps the previous occurrence in browsers.
/// Neither Base reopening the session nor the styled panel echoing its
/// retained query may reset the current match to zero.
#[gpui::test]
fn reopening_search_panel_preserves_the_previous_match(cx: &mut gpui::TestAppContext) {
cx.update(crate::init);
let (probe, cx) = cx.add_window_view(|window, cx| OverlayProbe {
state: cx.new(|cx| crate::input::EditorState::new(window, cx).searchable(true)),
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());

cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.set_value("foo bar foo baz foo", window, cx);
state.open_search(false, cx);
state.set_search_query("foo", true, cx);
assert_eq!(state.next_search_match(cx), Some(8..11));
assert_eq!(state.search_session().matcher.current_match_index(), 1);
state.close_search(cx);
state.open_search(false, cx);
assert_eq!(state.search_session().matcher.current_match_index(), 1);
});

let mut host = InputOverlayHost::new(state.clone(), window, cx);
host.sync(&state, window, cx);

state.read_with(cx, |state, _| {
assert_eq!(state.search_session().matcher.current_match_index(), 1);
assert_eq!(state.search_session().matcher.label(), "2/3");
});

state.update(cx, |state, cx| {
assert_eq!(state.next_search_match(cx), Some(16..19));
});
});
}

/// A frame that changed nothing must not rebuild the popovers.
///
/// Sync runs every frame, so the change check has to be cheap and stable.
Expand Down
Loading