editor: Improve find_matches and replace_all perf (#51941)

Helps with https://github.com/zed-industries/zed/issues/38927

- **editor: Add a benchmark for find/replace**
- **text: batch fragment insertions before turning them into a SumTree**

## Context

<!-- What does this PR do, and why? How is it expected to impact users?
     Not just what changed, but what motivated it and why this approach.

Link to Linear issue (e.g., ENG-123) or GitHub issue (e.g., Closes #456)
     if one exists — helps with traceability. -->

## How to Review

<!-- Help reviewers focus their attention:
- For small PRs: note what to focus on (e.g., "error handling in
foo.rs")
- For large PRs (>400 LOC): provide a guided tour — numbered list of
files/commits to read in order. (The `large-pr` label is applied
automatically.)
     - See the review process guidelines for comment conventions -->

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [ ] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Improved performance of "Replace All" in buffer search

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Piotr OsiewiczandSmit Barmase authored and GitHub committed 2026-05-06 09:28:05 +00:00
1 parent 156a47d0d0
commit cd67c9f84d
8 files changed
+506 -39

No files matched your search

+25 -1
View File
@@ -5258,6 +5258,16 @@ impl MultiBufferSnapshot {
Some(Anchor::in_buffer(path_key_index, anchor))
}
/// Lifts a buffer anchor range to a multibuffer anchor range without checking against excerpt boundaries. Returns `None` if there are no excerpts for the buffer.
pub fn anchor_range_in_buffer(&self, range: Range<text::Anchor>) -> Option<Range<Anchor>> {
if range.start.buffer_id != range.end.buffer_id {
return None;
}
let path_key_index = self.path_key_index_for_buffer(range.start.buffer_id)?;
Some(Anchor::range_in_buffer(path_key_index, range))
}
/// Creates a multibuffer anchor for the given buffer anchor, if it is contained in any excerpt.
pub fn anchor_in_excerpt(&self, text_anchor: text::Anchor) -> Option<Anchor> {
let excerpts = {
@@ -5295,6 +5305,19 @@ impl MultiBufferSnapshot {
&self,
text_anchor: Range<text::Anchor>,
) -> Option<Range<Anchor>> {
if self.is_singleton() {
let excerpt = self.excerpts.first()?;
let buffer_snapshot = excerpt.buffer_snapshot(self);
if excerpt.range.contains(&text_anchor.start, &buffer_snapshot)
&& excerpt.range.contains(&text_anchor.end, &buffer_snapshot)
{
return Some(Anchor::range_in_buffer(excerpt.path_key_index, text_anchor));
}
}
// for each search match
let mut buffer_snapshot = None;
for excerpt in {
let this = &self;
let buffer_id = text_anchor.start.buffer_id;
@@ -5316,7 +5339,8 @@ impl MultiBufferSnapshot {
.into_iter()
.flatten()
} {
let buffer_snapshot = excerpt.buffer_snapshot(self);
let buffer_snapshot =
buffer_snapshot.get_or_insert_with(|| excerpt.buffer_snapshot(self));
if excerpt.range.contains(&text_anchor.start, &buffer_snapshot)
&& excerpt.range.contains(&text_anchor.end, &buffer_snapshot)
{