Detailed changes
@@ -391,6 +391,14 @@
"ctrl-enter": "search::ReplaceAll",
},
},
+ {
+ "context": "BufferSearchBar && !in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ "shift-enter": "search::SelectPreviousMatch",
+ },
+ },
{
"context": "BufferSearchBar && !in_replace > Editor",
"bindings": {
@@ -424,6 +432,12 @@
"ctrl-alt-enter": "search::ReplaceAll",
},
},
+ {
+ "context": "ProjectSearchBar && !in_replace > Editor",
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ },
+ },
{
"context": "ProjectSearchView",
"bindings": {
@@ -446,6 +446,13 @@
{
"context": "BufferSearchBar && !in_replace > Editor",
"use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ "shift-enter": "search::SelectPreviousMatch",
+ },
+ },
+ {
+ "context": "BufferSearchBar && !in_replace > Editor",
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery",
@@ -473,7 +480,6 @@
},
{
"context": "ProjectSearchBar > Editor",
- "use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery",
@@ -487,6 +493,12 @@
"cmd-enter": "search::ReplaceAll",
},
},
+ {
+ "context": "ProjectSearchBar && !in_replace > Editor",
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ },
+ },
{
"context": "ProjectSearchView",
"use_key_equivalents": true,
@@ -398,6 +398,13 @@
{
"context": "BufferSearchBar && !in_replace > Editor",
"use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ "shift-enter": "search::SelectPreviousMatch",
+ },
+ },
+ {
+ "context": "BufferSearchBar && !in_replace > Editor",
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery",
@@ -415,7 +422,6 @@
},
{
"context": "ProjectSearchBar > Editor",
- "use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery",
@@ -429,6 +435,12 @@
"ctrl-alt-enter": "search::ReplaceAll",
},
},
+ {
+ "context": "ProjectSearchBar && !in_replace > Editor",
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ },
+ },
{
"context": "ProjectSearchView",
"use_key_equivalents": true,
@@ -303,7 +303,8 @@ impl Console {
}
fn previous_query(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
- let prev = self.history.previous(&mut self.cursor);
+ let current_query = self.query_bar.read(cx).text(cx);
+ let prev = self.history.previous(&mut self.cursor, ¤t_query);
if let Some(prev) = prev {
self.query_bar.update(cx, |editor, cx| {
editor.set_text(prev, window, cx);
@@ -2891,14 +2891,23 @@ impl Editor {
}
let disjoint = self.selections.disjoint_anchors();
- let snapshot = self.snapshot(window, cx);
- let snapshot = snapshot.buffer_snapshot();
- if self.mode == EditorMode::SingleLine
- && let [selection] = disjoint
+ if matches!(
+ &self.mode,
+ EditorMode::SingleLine | EditorMode::AutoHeight { .. }
+ ) && let [selection] = disjoint
&& selection.start == selection.end
- && selection.end.to_offset(snapshot) == snapshot.len()
{
- key_context.add("end_of_input");
+ let snapshot = self.snapshot(window, cx);
+ let snapshot = snapshot.buffer_snapshot();
+ let caret_offset = selection.end.to_offset(snapshot);
+
+ if caret_offset == MultiBufferOffset(0) {
+ key_context.add("start_of_input");
+ }
+
+ if caret_offset == snapshot.len() {
+ key_context.add("end_of_input");
+ }
}
if self.has_any_expanded_diff_hunks(cx) {
@@ -30815,14 +30815,47 @@ async fn test_end_of_editor_context(cx: &mut TestAppContext) {
cx.set_state("line1\nline2Λ");
cx.update_editor(|e, window, cx| {
e.set_mode(EditorMode::SingleLine);
+ assert!(!e.key_context(window, cx).contains("start_of_input"));
assert!(e.key_context(window, cx).contains("end_of_input"));
});
cx.set_state("Λline1\nline2");
cx.update_editor(|e, window, cx| {
+ e.set_mode(EditorMode::SingleLine);
+ assert!(e.key_context(window, cx).contains("start_of_input"));
+ assert!(!e.key_context(window, cx).contains("end_of_input"));
+ });
+ cx.set_state("line1Λ\nline2");
+ cx.update_editor(|e, window, cx| {
+ e.set_mode(EditorMode::SingleLine);
+ assert!(!e.key_context(window, cx).contains("start_of_input"));
+ assert!(!e.key_context(window, cx).contains("end_of_input"));
+ });
+
+ cx.set_state("line1\nline2Λ");
+ cx.update_editor(|e, window, cx| {
+ e.set_mode(EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: Some(4),
+ });
+ assert!(!e.key_context(window, cx).contains("start_of_input"));
+ assert!(e.key_context(window, cx).contains("end_of_input"));
+ });
+ cx.set_state("Λline1\nline2");
+ cx.update_editor(|e, window, cx| {
+ e.set_mode(EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: Some(4),
+ });
+ assert!(e.key_context(window, cx).contains("start_of_input"));
assert!(!e.key_context(window, cx).contains("end_of_input"));
});
cx.set_state("line1Λ\nline2");
cx.update_editor(|e, window, cx| {
+ e.set_mode(EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: Some(4),
+ });
+ assert!(!e.key_context(window, cx).contains("start_of_input"));
assert!(!e.key_context(window, cx).contains("end_of_input"));
});
}
@@ -1645,14 +1645,9 @@ impl SearchableItem for Editor {
match setting {
SeedQuerySetting::Never => String::new(),
SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
- let text: String = buffer_snapshot
+ buffer_snapshot
.text_for_range(selection.start..selection.end)
- .collect();
- if text.contains('\n') {
- String::new()
- } else {
- text
- }
+ .collect()
}
SeedQuerySetting::Selection => String::new(),
SeedQuerySetting::Always => {
@@ -19,12 +19,19 @@ pub enum QueryInsertionBehavior {
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct SearchHistoryCursor {
selection: Option<usize>,
+ draft: Option<String>,
}
impl SearchHistoryCursor {
- /// Resets the selection to `None`.
+ /// Resets the selection to `None` and clears the draft.
pub fn reset(&mut self) {
self.selection = None;
+ self.draft = None;
+ }
+
+ /// Takes the stored draft query, if any.
+ pub fn take_draft(&mut self) -> Option<String> {
+ self.draft.take()
}
}
@@ -45,6 +52,8 @@ impl SearchHistory {
}
pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) {
+ cursor.draft = None;
+
if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains
&& let Some(previously_searched) = self.history.back_mut()
&& search_string.contains(previously_searched.as_str())
@@ -81,7 +90,23 @@ impl SearchHistory {
/// Get the previous history entry using the given `SearchHistoryCursor`.
/// Uses the last element in the history when there is no cursor.
- pub fn previous(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
+ ///
+ /// `current_query` is the current text in the search editor. If it differs
+ /// from the history entry at the cursor position (or if the cursor has no
+ /// selection), it is saved as a draft so it can be restored later.
+ pub fn previous(
+ &mut self,
+ cursor: &mut SearchHistoryCursor,
+ current_query: &str,
+ ) -> Option<&str> {
+ let matches_history = cursor
+ .selection
+ .and_then(|i| self.history.get(i))
+ .is_some_and(|entry| entry == current_query);
+ if !matches_history {
+ cursor.draft = Some(current_query.to_string());
+ }
+
let prev_index = match cursor.selection {
Some(index) => index.checked_sub(1)?,
None => self.history.len().checked_sub(1)?,
@@ -38,7 +38,7 @@ fn test_add() {
// add item when it equals to current item if it's not the last one
search_history.add(&mut cursor, "php".to_string());
- search_history.previous(&mut cursor);
+ search_history.previous(&mut cursor, "");
assert_eq!(search_history.current(&cursor), Some("rustlang"));
search_history.add(&mut cursor, "rustlang".to_string());
assert_eq!(search_history.len(), 3, "Should add item");
@@ -71,13 +71,13 @@ fn test_next_and_previous() {
assert_eq!(search_history.current(&cursor), Some("TypeScript"));
- assert_eq!(search_history.previous(&mut cursor), Some("JavaScript"));
+ assert_eq!(search_history.previous(&mut cursor, ""), Some("JavaScript"));
assert_eq!(search_history.current(&cursor), Some("JavaScript"));
- assert_eq!(search_history.previous(&mut cursor), Some("Rust"));
+ assert_eq!(search_history.previous(&mut cursor, ""), Some("Rust"));
assert_eq!(search_history.current(&cursor), Some("Rust"));
- assert_eq!(search_history.previous(&mut cursor), None);
+ assert_eq!(search_history.previous(&mut cursor, ""), None);
assert_eq!(search_history.current(&cursor), Some("Rust"));
assert_eq!(search_history.next(&mut cursor), Some("JavaScript"));
@@ -103,14 +103,14 @@ fn test_reset_selection() {
cursor.reset();
assert_eq!(search_history.current(&cursor), None);
assert_eq!(
- search_history.previous(&mut cursor),
+ search_history.previous(&mut cursor, ""),
Some("TypeScript"),
"Should start from the end after reset on previous item query"
);
- search_history.previous(&mut cursor);
+ search_history.previous(&mut cursor, "");
assert_eq!(search_history.current(&cursor), Some("JavaScript"));
- search_history.previous(&mut cursor);
+ search_history.previous(&mut cursor, "");
assert_eq!(search_history.current(&cursor), Some("Rust"));
cursor.reset();
@@ -134,8 +134,11 @@ fn test_multiple_cursors() {
assert_eq!(search_history.current(&cursor1), Some("TypeScript"));
assert_eq!(search_history.current(&cursor2), Some("C++"));
- assert_eq!(search_history.previous(&mut cursor1), Some("JavaScript"));
- assert_eq!(search_history.previous(&mut cursor2), Some("Java"));
+ assert_eq!(
+ search_history.previous(&mut cursor1, ""),
+ Some("JavaScript")
+ );
+ assert_eq!(search_history.previous(&mut cursor2, ""), Some("Java"));
assert_eq!(search_history.next(&mut cursor1), Some("TypeScript"));
assert_eq!(search_history.next(&mut cursor1), Some("Python"));
@@ -6,8 +6,9 @@ use crate::{
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
buffer_search::registrar::WithResultsOrExternalQuery,
search_bar::{
- ActionButtonState, alignment_element, filter_search_results_input, input_base_styles,
- render_action_button, render_text_input,
+ ActionButtonState, HistoryNavigationDirection, alignment_element,
+ filter_search_results_input, input_base_styles, render_action_button, render_text_input,
+ should_navigate_history,
},
};
use any_vec::AnyVec;
@@ -15,6 +16,7 @@ use collections::HashMap;
use editor::{
Editor, EditorSettings, MultiBufferOffset, SplittableEditor, ToggleSplitDiff,
actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll},
+ scroll::Autoscroll,
};
use futures::channel::oneshot;
use gpui::{
@@ -337,13 +339,11 @@ impl Render for BufferSearchBar {
};
let query_column = input_style
- .child(
- div()
- .flex_1()
- .min_w(px(0.))
- .overflow_hidden()
- .child(render_text_input(&self.query_editor, color_override, cx)),
- )
+ .child(div().flex_1().min_w_0().py_1().child(render_text_input(
+ &self.query_editor,
+ color_override,
+ cx,
+ )))
.child(
h_flex()
.flex_none()
@@ -484,39 +484,42 @@ impl Render for BufferSearchBar {
.child(query_column)
.child(mode_column);
- let replace_line =
- should_show_replace_input.then(|| {
- let replace_column = input_base_styles(replacement_border)
- .child(render_text_input(&self.replacement_editor, None, cx));
- let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
-
- let replace_actions = h_flex()
- .min_w_64()
- .gap_1()
- .child(render_action_button(
- "buffer-search-replace-button",
- IconName::ReplaceNext,
- Default::default(),
- "Replace Next Match",
- &ReplaceNext,
- focus_handle.clone(),
- ))
- .child(render_action_button(
- "buffer-search-replace-button",
- IconName::ReplaceAll,
- Default::default(),
- "Replace All Matches",
- &ReplaceAll,
- focus_handle,
- ));
+ let replace_line = should_show_replace_input.then(|| {
+ let replace_column = input_base_styles(replacement_border).child(
+ div()
+ .flex_1()
+ .py_1()
+ .child(render_text_input(&self.replacement_editor, None, cx)),
+ );
+ let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
+
+ let replace_actions = h_flex()
+ .min_w_64()
+ .gap_1()
+ .child(render_action_button(
+ "buffer-search-replace-button",
+ IconName::ReplaceNext,
+ Default::default(),
+ "Replace Next Match",
+ &ReplaceNext,
+ focus_handle.clone(),
+ ))
+ .child(render_action_button(
+ "buffer-search-replace-button",
+ IconName::ReplaceAll,
+ Default::default(),
+ "Replace All Matches",
+ &ReplaceAll,
+ focus_handle,
+ ));
- h_flex()
- .w_full()
- .gap_2()
- .when(has_collapse_button, |this| this.child(alignment_element()))
- .child(replace_column)
- .child(replace_actions)
- });
+ h_flex()
+ .w_full()
+ .gap_2()
+ .when(has_collapse_button, |this| this.child(alignment_element()))
+ .child(replace_column)
+ .child(replace_actions)
+ });
let mut key_context = KeyContext::new_with_defaults();
key_context.add("BufferSearchBar");
@@ -831,13 +834,13 @@ impl BufferSearchBar {
cx: &mut Context<Self>,
) -> Self {
let query_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
+ let mut editor = Editor::auto_height(1, 4, window, cx);
editor.set_use_autoclose(false);
editor
});
cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
.detach();
- let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
+ let replacement_editor = cx.new(|cx| Editor::auto_height(1, 4, window, cx));
cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
.detach();
@@ -1186,6 +1189,7 @@ impl BufferSearchBar {
let len = query_buffer.len(cx);
query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
});
+ query_editor.request_autoscroll(Autoscroll::fit(), cx);
});
self.set_search_options(options, cx);
self.clear_matches(window, cx);
@@ -1704,15 +1708,19 @@ impl BufferSearchBar {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Next, cx) {
+ cx.propagate();
+ return;
+ }
+
if let Some(new_query) = self
.search_history
.next(&mut self.search_history_cursor)
.map(str::to_string)
{
drop(self.search(&new_query, Some(self.search_options), false, window, cx));
- } else {
- self.search_history_cursor.reset();
- drop(self.search("", Some(self.search_options), false, window, cx));
+ } else if let Some(draft) = self.search_history_cursor.take_draft() {
+ drop(self.search(&draft, Some(self.search_options), false, window, cx));
}
}
@@ -1722,6 +1730,11 @@ impl BufferSearchBar {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ if !should_navigate_history(&self.query_editor, HistoryNavigationDirection::Previous, cx) {
+ cx.propagate();
+ return;
+ }
+
if self.query(cx).is_empty()
&& let Some(new_query) = self
.search_history
@@ -1732,9 +1745,10 @@ impl BufferSearchBar {
return;
}
+ let current_query = self.query(cx);
if let Some(new_query) = self
.search_history
- .previous(&mut self.search_history_cursor)
+ .previous(&mut self.search_history_cursor, ¤t_query)
.map(str::to_string)
{
drop(self.search(&new_query, Some(self.search_options), false, window, cx));
@@ -2716,13 +2730,13 @@ mod tests {
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
- // Next history query after the latest should set the query to the empty string.
+ // Next history query after the latest should preserve the current query.
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.next_history_query(&NextHistoryQuery, window, cx);
});
cx.background_executor.run_until_parked();
search_bar.update(cx, |search_bar, cx| {
- assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.query(cx), "c");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update_in(cx, |search_bar, window, cx| {
@@ -2730,17 +2744,17 @@ mod tests {
});
cx.background_executor.run_until_parked();
search_bar.update(cx, |search_bar, cx| {
- assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.query(cx), "c");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
- // First previous query for empty current query should set the query to the latest.
+ // Previous query should navigate backwards through history.
search_bar.update_in(cx, |search_bar, window, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
});
cx.background_executor.run_until_parked();
search_bar.update(cx, |search_bar, cx| {
- assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.query(cx), "b");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
@@ -2750,7 +2764,7 @@ mod tests {
});
cx.background_executor.run_until_parked();
search_bar.update(cx, |search_bar, cx| {
- assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.query(cx), "a");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
@@ -2831,11 +2845,71 @@ mod tests {
});
cx.background_executor.run_until_parked();
search_bar.update(cx, |search_bar, cx| {
- assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.query(cx), "ba");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
}
+ #[perf]
+ #[gpui::test]
+ async fn test_search_query_history_autoscroll(cx: &mut TestAppContext) {
+ let (_editor, search_bar, cx) = init_test(cx);
+
+ // Add a long multi-line query that exceeds the editor's max
+ // visible height (4 lines), then a short query.
+ let long_query = "line1\nline2\nline3\nline4\nline5\nline6";
+ search_bar
+ .update_in(cx, |search_bar, window, cx| {
+ search_bar.search(long_query, None, true, window, cx)
+ })
+ .await
+ .unwrap();
+ search_bar
+ .update_in(cx, |search_bar, window, cx| {
+ search_bar.search("short", None, true, window, cx)
+ })
+ .await
+ .unwrap();
+
+ // Navigate back to the long entry. Since "short" is single-line,
+ // the history navigation is allowed.
+ search_bar.update_in(cx, |search_bar, window, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
+ });
+ cx.background_executor.run_until_parked();
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), long_query);
+ });
+
+ // The cursor should be scrolled into view despite the content
+ // exceeding the editor's max visible height.
+ search_bar.update_in(cx, |search_bar, window, cx| {
+ let snapshot = search_bar
+ .query_editor
+ .update(cx, |editor, cx| editor.snapshot(window, cx));
+ let cursor_row = search_bar
+ .query_editor
+ .read(cx)
+ .selections
+ .newest_display(&snapshot)
+ .head()
+ .row();
+ let scroll_top = search_bar
+ .query_editor
+ .update(cx, |editor, cx| editor.scroll_position(cx).y);
+ let visible_lines = search_bar
+ .query_editor
+ .read(cx)
+ .visible_line_count()
+ .unwrap_or(0.0);
+ let scroll_bottom = scroll_top + visible_lines;
+ assert!(
+ (cursor_row.0 as f64) < scroll_bottom,
+ "cursor row {cursor_row:?} should be visible (scroll range {scroll_top}..{scroll_bottom})"
+ );
+ });
+ }
+
#[perf]
#[gpui::test]
async fn test_replace_simple(cx: &mut TestAppContext) {
@@ -4,8 +4,8 @@ use crate::{
ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord,
buffer_search::Deploy,
search_bar::{
- ActionButtonState, alignment_element, input_base_styles, render_action_button,
- render_text_input,
+ ActionButtonState, HistoryNavigationDirection, alignment_element, input_base_styles,
+ render_action_button, render_text_input, should_navigate_history,
},
};
use anyhow::Context as _;
@@ -934,7 +934,7 @@ impl ProjectSearchView {
}));
let query_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
+ let mut editor = Editor::auto_height(1, 4, window, cx);
editor.set_placeholder_text("Search all filesβ¦", window, cx);
editor.set_text(query_text, window, cx);
editor
@@ -957,7 +957,7 @@ impl ProjectSearchView {
}),
);
let replacement_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
+ let mut editor = Editor::auto_height(1, 4, window, cx);
editor.set_placeholder_text("Replace in projectβ¦", window, cx);
if let Some(text) = replacement_text {
editor.set_text(text, window, cx);
@@ -1551,8 +1551,9 @@ impl ProjectSearchView {
SearchInputKind::Exclude => &self.excluded_files_editor,
};
- editor.update(cx, |included_editor, cx| {
- included_editor.set_text(text, window, cx)
+ editor.update(cx, |editor, cx| {
+ editor.set_text(text, window, cx);
+ editor.request_autoscroll(Autoscroll::fit(), cx);
});
}
@@ -1997,6 +1998,11 @@ impl ProjectSearchBar {
),
] {
if editor.focus_handle(cx).is_focused(window) {
+ if !should_navigate_history(&editor, HistoryNavigationDirection::Next, cx) {
+ cx.propagate();
+ return;
+ }
+
let new_query = search_view.entity.update(cx, |model, cx| {
let project = model.project.clone();
@@ -2006,13 +2012,14 @@ impl ProjectSearchBar {
.next(model.cursor_mut(kind))
.map(str::to_string)
}) {
- new_query
+ Some(new_query)
} else {
- model.cursor_mut(kind).reset();
- String::new()
+ model.cursor_mut(kind).take_draft()
}
});
- search_view.set_search_editor(kind, &new_query, window, cx);
+ if let Some(new_query) = new_query {
+ search_view.set_search_editor(kind, &new_query, window, cx);
+ }
}
}
});
@@ -2039,6 +2046,15 @@ impl ProjectSearchBar {
),
] {
if editor.focus_handle(cx).is_focused(window) {
+ if !should_navigate_history(
+ &editor,
+ HistoryNavigationDirection::Previous,
+ cx,
+ ) {
+ cx.propagate();
+ return;
+ }
+
if editor.read(cx).text(cx).is_empty()
&& let Some(new_query) = search_view
.entity
@@ -2053,12 +2069,13 @@ impl ProjectSearchBar {
return;
}
+ let current_query = editor.read(cx).text(cx);
if let Some(new_query) = search_view.entity.update(cx, |model, cx| {
let project = model.project.clone();
project.update(cx, |project, _| {
project
.search_history_mut(kind)
- .previous(model.cursor_mut(kind))
+ .previous(model.cursor_mut(kind), ¤t_query)
.map(str::to_string)
})
}) {
@@ -2157,7 +2174,11 @@ impl Render for ProjectSearchBar {
.on_action(
cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)),
)
- .child(render_text_input(&search.query_editor, color_override, cx))
+ .child(div().flex_1().py_1().child(render_text_input(
+ &search.query_editor,
+ color_override,
+ cx,
+ )))
.child(
h_flex()
.gap_1()
@@ -2315,8 +2336,13 @@ impl Render for ProjectSearchBar {
.child(mode_column);
let replace_line = search.replace_enabled.then(|| {
- let replace_column = input_base_styles(InputPanel::Replacement)
- .child(render_text_input(&search.replacement_editor, None, cx));
+ let replace_column = input_base_styles(InputPanel::Replacement).child(
+ div().flex_1().py_1().child(render_text_input(
+ &search.replacement_editor,
+ None,
+ cx,
+ )),
+ );
let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
let replace_actions = h_flex()
@@ -3915,7 +3941,7 @@ pub mod tests {
})
.unwrap();
- // Next history query after the latest should set the query to the empty string.
+ // Next history query after the latest should preserve the current query.
window
.update(cx, |_, window, cx| {
search_bar.update(cx, |search_bar, cx| {
@@ -3927,7 +3953,10 @@ pub mod tests {
window
.update(cx, |_, _, cx| {
search_view.update(cx, |search_view, cx| {
- assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(
+ search_view.query_editor.read(cx).text(cx),
+ "JUST_TEXT_INPUT"
+ );
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
})
@@ -3943,13 +3972,16 @@ pub mod tests {
window
.update(cx, |_, _, cx| {
search_view.update(cx, |search_view, cx| {
- assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(
+ search_view.query_editor.read(cx).text(cx),
+ "JUST_TEXT_INPUT"
+ );
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
})
.unwrap();
- // First previous query for empty current query should set the query to the latest submitted one.
+ // Previous query should navigate backwards through history.
window
.update(cx, |_, window, cx| {
search_bar.update(cx, |search_bar, cx| {
@@ -3961,7 +3993,7 @@ pub mod tests {
window
.update(cx, |_, _, cx| {
search_view.update(cx, |search_view, cx| {
- assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
})
@@ -3979,7 +4011,7 @@ pub mod tests {
window
.update(cx, |_, _, cx| {
search_view.update(cx, |search_view, cx| {
- assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
})
@@ -4133,11 +4165,75 @@ pub mod tests {
window
.update(cx, |_, _, cx| {
search_view.update(cx, |search_view, cx| {
- assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
})
.unwrap();
+
+ // Typing text without running a search, then navigating history, should allow
+ // restoring the draft when pressing next past the end.
+ window
+ .update(cx, |_, window, cx| {
+ search_view.update(cx, |search_view, cx| {
+ search_view.query_editor.update(cx, |query_editor, cx| {
+ query_editor.set_text("unsaved draft", window, cx)
+ });
+ })
+ })
+ .unwrap();
+ cx.background_executor.run_until_parked();
+
+ // Navigate up into history β the draft should be stashed.
+ window
+ .update(cx, |_, window, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.focus_search(window, cx);
+ search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
+ });
+ })
+ .unwrap();
+ window
+ .update(cx, |_, _, cx| {
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ });
+ })
+ .unwrap();
+
+ // Navigate forward through history.
+ window
+ .update(cx, |_, window, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.focus_search(window, cx);
+ search_bar.next_history_query(&NextHistoryQuery, window, cx);
+ });
+ })
+ .unwrap();
+ window
+ .update(cx, |_, _, cx| {
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+ });
+ })
+ .unwrap();
+
+ // Navigate past the end β the draft should be restored.
+ window
+ .update(cx, |_, window, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.focus_search(window, cx);
+ search_bar.next_history_query(&NextHistoryQuery, window, cx);
+ });
+ })
+ .unwrap();
+ window
+ .update(cx, |_, _, cx| {
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "unsaved draft");
+ });
+ })
+ .unwrap();
}
#[perf]
@@ -4323,9 +4419,6 @@ pub mod tests {
cx.background_executor.run_until_parked();
select_next_history_item(&search_bar_2, cx);
- assert_eq!(active_query(&search_view_2, cx), "");
-
- select_prev_history_item(&search_bar_2, cx);
assert_eq!(active_query(&search_view_2, cx), "THREE");
select_prev_history_item(&search_bar_2, cx);
@@ -4337,6 +4430,9 @@ pub mod tests {
select_prev_history_item(&search_bar_2, cx);
assert_eq!(active_query(&search_view_2, cx), "ONE");
+ select_prev_history_item(&search_bar_2, cx);
+ assert_eq!(active_query(&search_view_2, cx), "ONE");
+
// Search view 1 should now see the query from search view 2.
assert_eq!(active_query(&search_view_1, cx), "ONE");
@@ -4348,7 +4444,7 @@ pub mod tests {
assert_eq!(active_query(&search_view_2, cx), "THREE");
select_next_history_item(&search_bar_2, cx);
- assert_eq!(active_query(&search_view_2, cx), "");
+ assert_eq!(active_query(&search_view_2, cx), "THREE");
select_next_history_item(&search_bar_1, cx);
assert_eq!(active_query(&search_view_1, cx), "TWO");
@@ -4357,7 +4453,7 @@ pub mod tests {
assert_eq!(active_query(&search_view_1, cx), "THREE");
select_next_history_item(&search_bar_1, cx);
- assert_eq!(active_query(&search_view_1, cx), "");
+ assert_eq!(active_query(&search_view_1, cx), "THREE");
}
#[perf]
@@ -1,10 +1,37 @@
-use editor::{Editor, EditorElement, EditorStyle};
-use gpui::{Action, Entity, FocusHandle, Hsla, IntoElement, TextStyle};
+use editor::{Editor, EditorElement, EditorStyle, MultiBufferOffset, ToOffset};
+use gpui::{Action, App, Entity, FocusHandle, Hsla, IntoElement, TextStyle};
use settings::Settings;
use theme::ThemeSettings;
use ui::{IconButton, IconButtonShape};
use ui::{Tooltip, prelude::*};
+pub(super) enum HistoryNavigationDirection {
+ Previous,
+ Next,
+}
+
+pub(super) fn should_navigate_history(
+ editor: &Entity<Editor>,
+ direction: HistoryNavigationDirection,
+ cx: &App,
+) -> bool {
+ let editor_ref = editor.read(cx);
+ let snapshot = editor_ref.buffer().read(cx).snapshot(cx);
+ if snapshot.max_point().row == 0 {
+ return true;
+ }
+ let selections = editor_ref.selections.disjoint_anchors();
+ if let [selection] = selections {
+ let offset = selection.end.to_offset(&snapshot);
+ match direction {
+ HistoryNavigationDirection::Previous => offset == MultiBufferOffset(0),
+ HistoryNavigationDirection::Next => offset == snapshot.len(),
+ }
+ } else {
+ true
+ }
+}
+
pub(super) enum ActionButtonState {
Disabled,
Toggled,
@@ -43,7 +70,7 @@ pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div
h_flex()
.map(map)
.min_w_32()
- .h_8()
+ .min_h_8()
.pl_2()
.pr_1()
.border_1()