diff --git a/assets/icons/search_selection.svg b/assets/icons/search_selection.svg
new file mode 100644
index 0000000000..b970db1430
--- /dev/null
+++ b/assets/icons/search_selection.svg
@@ -0,0 +1 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 8bb8753d97..c29beb4ffe 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -232,7 +232,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"ctrl-f": "search::FocusSearch",
- "ctrl-h": "search::ToggleReplace"
+ "ctrl-h": "search::ToggleReplace",
+ "ctrl-l": "search::ToggleSelection"
}
},
{
@@ -296,6 +297,7 @@
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"ctrl-alt-shift-h": "search::ToggleReplace",
+ "ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index da035261ab..9ad328608f 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -176,6 +176,12 @@
"replace_enabled": true
}
],
+ "cmd-alt-l": [
+ "buffer_search::Deploy",
+ {
+ "selection_search_enabled": true
+ }
+ ],
"cmd-e": [
"buffer_search::Deploy",
{
@@ -250,7 +256,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"cmd-f": "search::FocusSearch",
- "cmd-alt-f": "search::ToggleReplace"
+ "cmd-alt-f": "search::ToggleReplace",
+ "cmd-alt-l": "search::ToggleSelection"
}
},
{
@@ -316,6 +323,7 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
+ "cmd-alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 5ab3293735..e5bfbf5392 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -522,6 +522,7 @@ pub struct Editor {
expect_bounds_change: Option>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option>,
+ previous_search_ranges: Option]>>,
}
#[derive(Clone)]
@@ -1824,6 +1825,7 @@ impl Editor {
}),
],
tasks_update_task: None,
+ previous_search_ranges: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
this._subscriptions.extend(project_subscriptions);
@@ -10264,6 +10266,27 @@ impl Editor {
self.background_highlights_in_range(start..end, &snapshot, theme)
}
+ #[cfg(feature = "test-support")]
+ pub fn search_background_highlights(
+ &mut self,
+ cx: &mut ViewContext,
+ ) -> Vec> {
+ let snapshot = self.buffer().read(cx).snapshot(cx);
+
+ let highlights = self
+ .background_highlights
+ .get(&TypeId::of::());
+
+ if let Some((_color, ranges)) = highlights {
+ ranges
+ .iter()
+ .map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot))
+ .collect_vec()
+ } else {
+ vec![]
+ }
+ }
+
fn document_highlights_for_position<'a>(
&'a self,
position: Anchor,
diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs
index 2d5ea9be7f..5f637a7337 100644
--- a/crates/editor/src/items.rs
+++ b/crates/editor/src/items.rs
@@ -13,8 +13,7 @@ use gpui::{
VisualContext, WeakView, WindowContext,
};
use language::{
- proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
- Point, SelectionGoal,
+ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
};
use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
@@ -1008,6 +1007,25 @@ impl SearchableItem for Editor {
self.has_background_highlights::()
}
+ fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut ViewContext) {
+ if self.has_filtered_search_ranges() {
+ self.previous_search_ranges = self
+ .clear_background_highlights::(cx)
+ .map(|(_, ranges)| ranges)
+ }
+
+ if !enabled {
+ return;
+ }
+
+ let ranges = self.selections.disjoint_anchor_ranges();
+ if ranges.iter().any(|range| range.start != range.end) {
+ self.set_search_within_ranges(&ranges, cx);
+ } else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
+ self.set_search_within_ranges(&previous_search_ranges, cx)
+ }
+ }
+
fn query_suggestion(&mut self, cx: &mut ViewContext) -> String {
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
let snapshot = &self.snapshot(cx).buffer_snapshot;
@@ -1016,9 +1034,14 @@ impl SearchableItem for Editor {
match setting {
SeedQuerySetting::Never => String::new(),
SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
- snapshot
+ let text: String = snapshot
.text_for_range(selection.start..selection.end)
- .collect()
+ .collect();
+ if text.contains('\n') {
+ String::new()
+ } else {
+ text
+ }
}
SeedQuerySetting::Selection => String::new(),
SeedQuerySetting::Always => {
@@ -1135,58 +1158,64 @@ impl SearchableItem for Editor {
let search_within_ranges = self
.background_highlights
.get(&TypeId::of::())
- .map(|(_color, ranges)| {
- ranges
- .iter()
- .map(|range| range.to_offset(&buffer))
- .collect::>()
+ .map_or(vec![], |(_color, ranges)| {
+ ranges.iter().map(|range| range.clone()).collect::>()
});
+
cx.background_executor().spawn(async move {
let mut ranges = Vec::new();
+
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
- if let Some(search_within_ranges) = search_within_ranges {
- for range in search_within_ranges {
- let offset = range.start;
- ranges.extend(
- query
- .search(excerpt_buffer, Some(range))
- .await
- .into_iter()
- .map(|range| {
- buffer.anchor_after(range.start + offset)
- ..buffer.anchor_before(range.end + offset)
- }),
- );
- }
+ let search_within_ranges = if search_within_ranges.is_empty() {
+ vec![None]
} else {
- ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
- |range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
- ));
+ search_within_ranges
+ .into_iter()
+ .map(|range| Some(range.to_offset(&buffer)))
+ .collect::>()
+ };
+
+ for range in search_within_ranges {
+ let buffer = &buffer;
+ ranges.extend(
+ query
+ .search(excerpt_buffer, range.clone())
+ .await
+ .into_iter()
+ .map(|matched_range| {
+ let offset = range.clone().map(|r| r.start).unwrap_or(0);
+ buffer.anchor_after(matched_range.start + offset)
+ ..buffer.anchor_before(matched_range.end + offset)
+ }),
+ );
}
} else {
- for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
- if let Some(next_excerpt) = excerpt.next {
- let excerpt_range =
- next_excerpt.range.context.to_offset(&next_excerpt.buffer);
- ranges.extend(
- query
- .search(&next_excerpt.buffer, Some(excerpt_range.clone()))
- .await
- .into_iter()
- .map(|range| {
- let start = next_excerpt
- .buffer
- .anchor_after(excerpt_range.start + range.start);
- let end = next_excerpt
- .buffer
- .anchor_before(excerpt_range.start + range.end);
- buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap()
- ..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap()
- }),
- );
- }
+ let search_within_ranges = if search_within_ranges.is_empty() {
+ vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
+ } else {
+ search_within_ranges
+ };
+
+ for (excerpt_id, search_buffer, search_range) in
+ buffer.excerpts_in_ranges(search_within_ranges)
+ {
+ ranges.extend(
+ query
+ .search(&search_buffer, Some(search_range.clone()))
+ .await
+ .into_iter()
+ .map(|match_range| {
+ let start = search_buffer
+ .anchor_after(search_range.start + match_range.start);
+ let end = search_buffer
+ .anchor_before(search_range.start + match_range.end);
+ buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
+ ..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
+ }),
+ );
}
- }
+ };
+
ranges
})
}
diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs
index 03859ddf2a..70af8b9489 100644
--- a/crates/editor/src/selections_collection.rs
+++ b/crates/editor/src/selections_collection.rs
@@ -273,6 +273,13 @@ impl SelectionsCollection {
self.all(cx).last().unwrap().clone()
}
+ pub fn disjoint_anchor_ranges(&self) -> Vec> {
+ self.disjoint_anchors()
+ .iter()
+ .map(|s| s.start..s.end)
+ .collect()
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn ranges + std::fmt::Debug>(
&self,
diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs
index 44141f89ce..5950b4b629 100644
--- a/crates/language_tools/src/lsp_log.rs
+++ b/crates/language_tools/src/lsp_log.rs
@@ -765,6 +765,7 @@ impl SearchableItem for LspLogView {
regex: true,
// LSP log is read-only.
replacement: false,
+ selection: false,
}
}
fn active_match_index(
diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs
index 4eb630e78e..cdba2fe6cf 100644
--- a/crates/multi_buffer/src/multi_buffer.rs
+++ b/crates/multi_buffer/src/multi_buffer.rs
@@ -3740,6 +3740,62 @@ impl MultiBufferSnapshot {
}
}
+ /// Returns excerpts overlapping the given ranges. If range spans multiple excerpts returns one range for each excerpt
+ pub fn excerpts_in_ranges(
+ &self,
+ ranges: impl IntoIterator- >,
+ ) -> impl Iterator
- )> {
+ let mut ranges = ranges.into_iter().map(|range| range.to_offset(self));
+
+ let mut cursor = self.excerpts.cursor::();
+ let mut next_range = move |cursor: &mut Cursor| {
+ let range = ranges.next();
+ if let Some(range) = range.as_ref() {
+ cursor.seek_forward(&range.start, Bias::Right, &());
+ }
+
+ range
+ };
+ let mut range = next_range(&mut cursor);
+
+ iter::from_fn(move || {
+ if range.is_none() {
+ return None;
+ }
+
+ if range.as_ref().unwrap().is_empty() || *cursor.start() >= range.as_ref().unwrap().end
+ {
+ range = next_range(&mut cursor);
+ if range.is_none() {
+ return None;
+ }
+ }
+
+ cursor.item().map(|excerpt| {
+ let multibuffer_excerpt = MultiBufferExcerpt::new(&excerpt, *cursor.start());
+
+ let multibuffer_excerpt_range = multibuffer_excerpt
+ .map_range_from_buffer(excerpt.range.context.to_offset(&excerpt.buffer));
+
+ let overlap_range = cmp::max(
+ range.as_ref().unwrap().start,
+ multibuffer_excerpt_range.start,
+ )
+ ..cmp::min(range.as_ref().unwrap().end, multibuffer_excerpt_range.end);
+
+ let overlap_range = multibuffer_excerpt.map_range_to_buffer(overlap_range);
+
+ if multibuffer_excerpt_range.end <= range.as_ref().unwrap().end {
+ cursor.next(&());
+ } else {
+ range = next_range(&mut cursor);
+ }
+
+ (excerpt.id, &excerpt.buffer, overlap_range)
+ })
+ })
+ }
+
pub fn remote_selections_in_range<'a>(
&'a self,
range: &'a Range,
@@ -6076,4 +6132,415 @@ mod tests {
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
});
}
+
+ #[gpui::test]
+ fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) {
+ let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
+ let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+ multibuffer.update(cx, |multibuffer, cx| {
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: 0..buffer_1.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: 0..buffer_2.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ );
+ });
+
+ let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
+
+ let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None));
+
+ assert!(excerpts.next().is_none());
+ }
+
+ fn validate_excerpts(
+ actual: &Vec<(ExcerptId, BufferId, Range)>,
+ expected: &Vec<(ExcerptId, BufferId, Range)>,
+ ) {
+ assert_eq!(actual.len(), expected.len());
+
+ actual
+ .into_iter()
+ .zip(expected)
+ .map(|(actual, expected)| {
+ assert_eq!(actual.0, expected.0);
+ assert_eq!(actual.1, expected.1);
+ assert_eq!(actual.2.start, expected.2.start);
+ assert_eq!(actual.2.end, expected.2.end);
+ })
+ .collect_vec();
+ }
+
+ fn map_range_from_excerpt(
+ snapshot: &MultiBufferSnapshot,
+ excerpt_id: ExcerptId,
+ excerpt_buffer: &BufferSnapshot,
+ range: Range,
+ ) -> Range {
+ snapshot
+ .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_before(range.start))
+ .unwrap()
+ ..snapshot
+ .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_after(range.end))
+ .unwrap()
+ }
+
+ fn make_expected_excerpt_info(
+ snapshot: &MultiBufferSnapshot,
+ cx: &mut AppContext,
+ excerpt_id: ExcerptId,
+ buffer: &Model,
+ range: Range,
+ ) -> (ExcerptId, BufferId, Range) {
+ (
+ excerpt_id,
+ buffer.read(cx).remote_id(),
+ map_range_from_excerpt(&snapshot, excerpt_id, &buffer.read(cx).snapshot(), range),
+ )
+ }
+
+ #[gpui::test]
+ fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) {
+ let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
+ let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
+ let buffer_len = buffer_1.read(cx).len();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+ let mut expected_excerpt_id = ExcerptId(0);
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ expected_excerpt_id = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: 0..buffer_1.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: 0..buffer_2.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ );
+ });
+
+ let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
+
+ let range = snapshot
+ .anchor_in_excerpt(expected_excerpt_id, buffer_1.read(cx).anchor_before(1))
+ .unwrap()
+ ..snapshot
+ .anchor_in_excerpt(
+ expected_excerpt_id,
+ buffer_1.read(cx).anchor_after(buffer_len / 2),
+ )
+ .unwrap();
+
+ let expected_excerpts = vec![make_expected_excerpt_info(
+ &snapshot,
+ cx,
+ expected_excerpt_id,
+ &buffer_1,
+ 1..(buffer_len / 2),
+ )];
+
+ let excerpts = snapshot
+ .excerpts_in_ranges(vec![range.clone()].into_iter())
+ .map(|(excerpt_id, buffer, actual_range)| {
+ (
+ excerpt_id,
+ buffer.remote_id(),
+ map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
+ )
+ })
+ .collect_vec();
+
+ validate_excerpts(&excerpts, &expected_excerpts);
+ }
+
+ #[gpui::test]
+ fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext) {
+ let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
+ let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
+ let buffer_len = buffer_1.read(cx).len();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+ let mut excerpt_1_id = ExcerptId(0);
+ let mut excerpt_2_id = ExcerptId(0);
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ excerpt_1_id = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: 0..buffer_1.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ excerpt_2_id = multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: 0..buffer_2.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ });
+
+ let snapshot = multibuffer.read(cx).snapshot(cx);
+
+ let expected_range = snapshot
+ .anchor_in_excerpt(
+ excerpt_1_id,
+ buffer_1.read(cx).anchor_before(buffer_len / 2),
+ )
+ .unwrap()
+ ..snapshot
+ .anchor_in_excerpt(excerpt_2_id, buffer_2.read(cx).anchor_after(buffer_len / 2))
+ .unwrap();
+
+ let expected_excerpts = vec![
+ make_expected_excerpt_info(
+ &snapshot,
+ cx,
+ excerpt_1_id,
+ &buffer_1,
+ (buffer_len / 2)..buffer_len,
+ ),
+ make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len / 2),
+ ];
+
+ let excerpts = snapshot
+ .excerpts_in_ranges(vec![expected_range.clone()].into_iter())
+ .map(|(excerpt_id, buffer, actual_range)| {
+ (
+ excerpt_id,
+ buffer.remote_id(),
+ map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
+ )
+ })
+ .collect_vec();
+
+ validate_excerpts(&excerpts, &expected_excerpts);
+ }
+
+ #[gpui::test]
+ fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) {
+ let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
+ let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
+ let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'r'), cx));
+ let buffer_len = buffer_1.read(cx).len();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+ let mut excerpt_1_id = ExcerptId(0);
+ let mut excerpt_2_id = ExcerptId(0);
+ let mut excerpt_3_id = ExcerptId(0);
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ excerpt_1_id = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: 0..buffer_1.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ excerpt_2_id = multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: 0..buffer_2.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ excerpt_3_id = multibuffer.push_excerpts(
+ buffer_3.clone(),
+ [ExcerptRange {
+ context: 0..buffer_3.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ });
+
+ let snapshot = multibuffer.read(cx).snapshot(cx);
+
+ let expected_range = snapshot
+ .anchor_in_excerpt(
+ excerpt_1_id,
+ buffer_1.read(cx).anchor_before(buffer_len / 2),
+ )
+ .unwrap()
+ ..snapshot
+ .anchor_in_excerpt(excerpt_3_id, buffer_3.read(cx).anchor_after(buffer_len / 2))
+ .unwrap();
+
+ let expected_excerpts = vec![
+ make_expected_excerpt_info(
+ &snapshot,
+ cx,
+ excerpt_1_id,
+ &buffer_1,
+ (buffer_len / 2)..buffer_len,
+ ),
+ make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len),
+ make_expected_excerpt_info(&snapshot, cx, excerpt_3_id, &buffer_3, 0..buffer_len / 2),
+ ];
+
+ let excerpts = snapshot
+ .excerpts_in_ranges(vec![expected_range.clone()].into_iter())
+ .map(|(excerpt_id, buffer, actual_range)| {
+ (
+ excerpt_id,
+ buffer.remote_id(),
+ map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
+ )
+ })
+ .collect_vec();
+
+ validate_excerpts(&excerpts, &expected_excerpts);
+ }
+
+ #[gpui::test]
+ fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) {
+ let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
+ let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
+ let buffer_len = buffer_1.read(cx).len();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+ let mut excerpt_1_id = ExcerptId(0);
+ let mut excerpt_2_id = ExcerptId(0);
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ excerpt_1_id = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: 0..buffer_1.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ excerpt_2_id = multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: 0..buffer_2.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ });
+
+ let snapshot = multibuffer.read(cx).snapshot(cx);
+
+ let ranges = vec![
+ 1..(buffer_len / 4),
+ (buffer_len / 3)..(buffer_len / 2),
+ (buffer_len / 4 * 3)..(buffer_len),
+ ];
+
+ let expected_excerpts = ranges
+ .iter()
+ .map(|range| {
+ make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, range.clone())
+ })
+ .collect_vec();
+
+ let ranges = ranges.into_iter().map(|range| {
+ map_range_from_excerpt(
+ &snapshot,
+ excerpt_1_id,
+ &buffer_1.read(cx).snapshot(),
+ range,
+ )
+ });
+
+ let excerpts = snapshot
+ .excerpts_in_ranges(ranges)
+ .map(|(excerpt_id, buffer, actual_range)| {
+ (
+ excerpt_id,
+ buffer.remote_id(),
+ map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
+ )
+ })
+ .collect_vec();
+
+ validate_excerpts(&excerpts, &expected_excerpts);
+ }
+
+ #[gpui::test]
+ fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) {
+ let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
+ let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
+ let buffer_len = buffer_1.read(cx).len();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+ let mut excerpt_1_id = ExcerptId(0);
+ let mut excerpt_2_id = ExcerptId(0);
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ excerpt_1_id = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: 0..buffer_1.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ excerpt_2_id = multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: 0..buffer_2.read(cx).len(),
+ primary: None,
+ }],
+ cx,
+ )[0];
+ });
+
+ let snapshot = multibuffer.read(cx).snapshot(cx);
+
+ let ranges = [0..buffer_len, (buffer_len / 3)..(buffer_len / 2)];
+
+ let expected_excerpts = vec![
+ make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, ranges[0].clone()),
+ make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, ranges[1].clone()),
+ ];
+
+ let ranges = [
+ map_range_from_excerpt(
+ &snapshot,
+ excerpt_1_id,
+ &buffer_1.read(cx).snapshot(),
+ ranges[0].clone(),
+ ),
+ map_range_from_excerpt(
+ &snapshot,
+ excerpt_2_id,
+ &buffer_2.read(cx).snapshot(),
+ ranges[1].clone(),
+ ),
+ ];
+
+ let excerpts = snapshot
+ .excerpts_in_ranges(ranges.into_iter())
+ .map(|(excerpt_id, buffer, actual_range)| {
+ (
+ excerpt_id,
+ buffer.remote_id(),
+ map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
+ )
+ })
+ .collect_vec();
+
+ validate_excerpts(&excerpts, &expected_excerpts);
+ }
}
diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs
index f264d796a9..37c49f34ed 100644
--- a/crates/search/src/buffer_search.rs
+++ b/crates/search/src/buffer_search.rs
@@ -3,7 +3,7 @@ mod registrar;
use crate::{
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
- ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,
+ ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
};
use any_vec::AnyVec;
use collections::HashMap;
@@ -48,6 +48,8 @@ pub struct Deploy {
pub focus: bool,
#[serde(default)]
pub replace_enabled: bool,
+ #[serde(default)]
+ pub selection_search_enabled: bool,
}
impl_actions!(buffer_search, [Deploy]);
@@ -59,6 +61,7 @@ impl Deploy {
Self {
focus: true,
replace_enabled: false,
+ selection_search_enabled: false,
}
}
}
@@ -90,6 +93,7 @@ pub struct BufferSearchBar {
search_history: SearchHistory,
search_history_cursor: SearchHistoryCursor,
replace_enabled: bool,
+ selection_search_enabled: bool,
scroll_handle: ScrollHandle,
editor_scroll_handle: ScrollHandle,
editor_needed_width: Pixels,
@@ -228,7 +232,7 @@ impl Render for BufferSearchBar {
}),
)
}))
- .children(supported_options.word.then(|| {
+ .children(supported_options.regex.then(|| {
self.render_search_option_button(
SearchOptions::REGEX,
cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
@@ -251,6 +255,26 @@ impl Render for BufferSearchBar {
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
)
})
+ .when(supported_options.selection, |this| {
+ this.child(
+ IconButton::new(
+ "buffer-search-bar-toggle-search-selection-button",
+ IconName::SearchSelection,
+ )
+ .style(ButtonStyle::Subtle)
+ .when(self.selection_search_enabled, |button| {
+ button.style(ButtonStyle::Filled)
+ })
+ .on_click(cx.listener(|this, _: &ClickEvent, cx| {
+ this.toggle_selection(&ToggleSelection, cx);
+ }))
+ .selected(self.selection_search_enabled)
+ .size(ButtonSize::Compact)
+ .tooltip(|cx| {
+ Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
+ }),
+ )
+ })
.child(
h_flex()
.flex_none()
@@ -359,6 +383,9 @@ impl Render for BufferSearchBar {
.when(self.supported_options().regex, |this| {
this.on_action(cx.listener(Self::toggle_regex))
})
+ .when(self.supported_options().selection, |this| {
+ this.on_action(cx.listener(Self::toggle_selection))
+ })
.gap_2()
.child(
h_flex()
@@ -440,6 +467,11 @@ impl BufferSearchBar {
this.toggle_whole_word(action, cx);
}
}));
+ registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
+ if this.supported_options().selection {
+ this.toggle_selection(action, cx);
+ }
+ }));
registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
@@ -497,6 +529,7 @@ impl BufferSearchBar {
search_history_cursor: Default::default(),
active_search: None,
replace_enabled: false,
+ selection_search_enabled: false,
scroll_handle: ScrollHandle::new(),
editor_scroll_handle: ScrollHandle::new(),
editor_needed_width: px(0.),
@@ -516,8 +549,11 @@ impl BufferSearchBar {
searchable_item.clear_matches(cx);
}
}
- if let Some(active_editor) = self.active_searchable_item.as_ref() {
+ if let Some(active_editor) = self.active_searchable_item.as_mut() {
+ self.selection_search_enabled = false;
+ self.replace_enabled = false;
active_editor.search_bar_visibility_changed(false, cx);
+ active_editor.toggle_filtered_search_ranges(false, cx);
let handle = active_editor.focus_handle(cx);
cx.focus(&handle);
}
@@ -530,8 +566,12 @@ impl BufferSearchBar {
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext) -> bool {
if self.show(cx) {
+ if let Some(active_item) = self.active_searchable_item.as_mut() {
+ active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
+ }
self.search_suggested(cx);
self.replace_enabled = deploy.replace_enabled;
+ self.selection_search_enabled = deploy.selection_search_enabled;
if deploy.focus {
let mut handle = self.query_editor.focus_handle(cx).clone();
let mut select_query = true;
@@ -539,9 +579,11 @@ impl BufferSearchBar {
handle = self.replacement_editor.focus_handle(cx).clone();
select_query = false;
};
+
if select_query {
self.select_query(cx);
}
+
cx.focus(&handle);
}
return true;
@@ -823,6 +865,15 @@ impl BufferSearchBar {
self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
}
+ fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext) {
+ if let Some(active_item) = self.active_searchable_item.as_mut() {
+ self.selection_search_enabled = !self.selection_search_enabled;
+ active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
+ let _ = self.update_matches(cx);
+ cx.notify();
+ }
+ }
+
fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext) {
self.toggle_search_option(SearchOptions::REGEX, cx)
}
@@ -1090,9 +1141,9 @@ mod tests {
use std::ops::Range;
use super::*;
- use editor::{display_map::DisplayRow, DisplayPoint, Editor};
+ use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
- use language::Buffer;
+ use language::{Buffer, Point};
use project::Project;
use smol::stream::StreamExt as _;
use unindent::Unindent as _;
@@ -1405,6 +1456,15 @@ mod tests {
});
}
+ fn display_points_of(
+ background_highlights: Vec<(Range, Hsla)>,
+ ) -> Vec> {
+ background_highlights
+ .into_iter()
+ .map(|(range, _)| range)
+ .collect::>()
+ }
+
#[gpui::test]
async fn test_search_option_handling(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
@@ -1417,12 +1477,6 @@ mod tests {
})
.await
.unwrap();
- let display_points_of = |background_highlights: Vec<(Range, Hsla)>| {
- background_highlights
- .into_iter()
- .map(|(range, _)| range)
- .collect::>()
- };
editor.update(cx, |editor, cx| {
assert_eq!(
display_points_of(editor.all_text_background_highlights(cx)),
@@ -2032,15 +2086,156 @@ mod tests {
.await;
}
+ #[gpui::test]
+ async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
+ cx: &mut TestAppContext,
+ ) {
+ init_globals(cx);
+ let buffer = cx.new_model(|cx| {
+ Buffer::local(
+ r#"
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ "#
+ .unindent(),
+ cx,
+ )
+ });
+ let cx = cx.add_empty_window();
+ let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = cx.new_view(|cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
+ })
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ let deploy = Deploy {
+ focus: true,
+ replace_enabled: false,
+ selection_search_enabled: true,
+ };
+ search_bar.deploy(&deploy, cx);
+ });
+
+ cx.run_until_parked();
+
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
+ .await
+ .unwrap();
+
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.search_background_highlights(cx),
+ &[
+ Point::new(1, 0)..Point::new(1, 3),
+ Point::new(1, 8)..Point::new(1, 11),
+ Point::new(2, 0)..Point::new(2, 3),
+ ]
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
+ cx: &mut TestAppContext,
+ ) {
+ init_globals(cx);
+ let text = r#"
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ aaa bbb aaa ccc
+ "#
+ .unindent();
+
+ let cx = cx.add_empty_window();
+ let editor = cx.new_view(|cx| {
+ let multibuffer = MultiBuffer::build_multi(
+ [
+ (
+ &text,
+ vec![
+ Point::new(0, 0)..Point::new(2, 0),
+ Point::new(4, 0)..Point::new(5, 0),
+ ],
+ ),
+ (&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
+ ],
+ cx,
+ );
+ Editor::for_multibuffer(multibuffer, None, false, cx)
+ });
+
+ let search_bar = cx.new_view(|cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges(vec![
+ Point::new(1, 0)..Point::new(1, 4),
+ Point::new(5, 3)..Point::new(6, 4),
+ ])
+ })
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ let deploy = Deploy {
+ focus: true,
+ replace_enabled: false,
+ selection_search_enabled: true,
+ };
+ search_bar.deploy(&deploy, cx);
+ });
+
+ cx.run_until_parked();
+
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
+ .await
+ .unwrap();
+
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.search_background_highlights(cx),
+ &[
+ Point::new(1, 0)..Point::new(1, 3),
+ Point::new(5, 8)..Point::new(5, 11),
+ Point::new(6, 0)..Point::new(6, 3),
+ ]
+ );
+ });
+ }
+
#[gpui::test]
async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
- let display_points_of = |background_highlights: Vec<(Range, Hsla)>| {
- background_highlights
- .into_iter()
- .map(|(range, _)| range)
- .collect::>()
- };
// Search using valid regexp
search_bar
.update(cx, |search_bar, cx| {
diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs
index e086ebae51..220863dfce 100644
--- a/crates/search/src/search.rs
+++ b/crates/search/src/search.rs
@@ -25,6 +25,7 @@ actions!(
ToggleIncludeIgnored,
ToggleRegex,
ToggleReplace,
+ ToggleSelection,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,
diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs
index bea068439b..0a2f5d863a 100644
--- a/crates/terminal_view/src/terminal_view.rs
+++ b/crates/terminal_view/src/terminal_view.rs
@@ -972,6 +972,7 @@ impl SearchableItem for TerminalView {
word: false,
regex: true,
replacement: false,
+ selection: false,
}
}
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index cb72386b59..79b4d87750 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -169,6 +169,7 @@ pub enum IconName {
Save,
Screen,
SelectAll,
+ SearchSelection,
Server,
Settings,
Shift,
@@ -293,6 +294,7 @@ impl IconName {
IconName::Save => "icons/save.svg",
IconName::Screen => "icons/desktop.svg",
IconName::SelectAll => "icons/select_all.svg",
+ IconName::SearchSelection => "icons/search_selection.svg",
IconName::Server => "icons/server.svg",
IconName::Settings => "icons/file_icons/settings.svg",
IconName::Shift => "icons/shift.svg",
diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs
index 950efde9e2..0d037f6bed 100644
--- a/crates/workspace/src/searchable.rs
+++ b/crates/workspace/src/searchable.rs
@@ -39,8 +39,9 @@ pub struct SearchOptions {
pub case: bool,
pub word: bool,
pub regex: bool,
- /// Specifies whether the item supports search & replace.
+ /// Specifies whether the supports search & replace.
pub replacement: bool,
+ pub selection: bool,
}
pub trait SearchableItem: Item + EventEmitter {
@@ -52,15 +53,18 @@ pub trait SearchableItem: Item + EventEmitter {
word: true,
regex: true,
replacement: true,
+ selection: true,
}
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext) {}
fn has_filtered_search_ranges(&mut self) -> bool {
- false
+ Self::supported_options().selection
}
+ fn toggle_filtered_search_ranges(&mut self, _enabled: bool, _cx: &mut ViewContext) {}
+
fn clear_matches(&mut self, cx: &mut ViewContext);
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext);
fn query_suggestion(&mut self, cx: &mut ViewContext) -> String;
@@ -138,6 +142,8 @@ pub trait SearchableItemHandle: ItemHandle {
cx: &mut WindowContext,
) -> Option;
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext);
+
+ fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext);
}
impl SearchableItemHandle for View {
@@ -240,6 +246,12 @@ impl SearchableItemHandle for View {
this.search_bar_visibility_changed(visible, cx)
});
}
+
+ fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| {
+ this.toggle_filtered_search_ranges(enabled, cx)
+ });
+ }
}
impl From> for AnyView {