From 81210daa6f5d5080ea0f4668cd9e892fa6fb42b5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:55:04 +0100 Subject: [PATCH 01/14] editor: Add a benchmark for find/replace Co-authored-by: Smit Barmase --- Cargo.lock | 18 +++ Cargo.toml | 1 + crates/editor_benchmarks/Cargo.toml | 22 ++++ crates/editor_benchmarks/src/main.rs | 178 +++++++++++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 crates/editor_benchmarks/Cargo.toml create mode 100644 crates/editor_benchmarks/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index cea560d65a501e..48e486af705b1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5549,6 +5549,24 @@ dependencies = [ "ztracing", ] +[[package]] +name = "editor_benchmarks" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor", + "gpui", + "gpui_platform", + "language", + "multi_buffer", + "project", + "release_channel", + "semver", + "settings", + "theme", + "workspace", +] + [[package]] name = "either" version = "1.15.0" diff --git a/Cargo.toml b/Cargo.toml index 72356e7ea35e2a..47b8eddf1486e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "crates/edit_prediction_types", "crates/edit_prediction_ui", "crates/editor", + "crates/editor_benchmarks", "crates/encoding_selector", "crates/env_var", "crates/etw_tracing", diff --git a/crates/editor_benchmarks/Cargo.toml b/crates/editor_benchmarks/Cargo.toml new file mode 100644 index 00000000000000..8db5d4b26aefd8 --- /dev/null +++ b/crates/editor_benchmarks/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "editor_benchmarks" +version = "0.1.0" +publish.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +editor.workspace = true +gpui.workspace = true +gpui_platform.workspace = true +language.workspace = true +multi_buffer.workspace = true +project.workspace = true +release_channel.workspace = true +semver.workspace = true +settings.workspace = true +theme.workspace = true +workspace.workspace = true + +[lints] +workspace = true diff --git a/crates/editor_benchmarks/src/main.rs b/crates/editor_benchmarks/src/main.rs new file mode 100644 index 00000000000000..0a6888bf30d390 --- /dev/null +++ b/crates/editor_benchmarks/src/main.rs @@ -0,0 +1,178 @@ +use std::sync::Arc; + +use editor::Editor; +use gpui::{ + AppContext as _, AsyncApp, AsyncWindowContext, WeakEntity, WindowBounds, WindowOptions, +}; +use language::Buffer; +use multi_buffer::Anchor; +use project::search::SearchQuery; +use workspace::searchable::SearchableItem; + +#[derive(Debug)] +struct Args { + file: String, + query: String, + replace: Option, + regex: bool, + whole_word: bool, + case_sensitive: bool, +} + +fn parse_args() -> Args { + let mut args_iter = std::env::args().skip(1); + let mut parsed = Args { + file: String::new(), + query: String::new(), + replace: None, + regex: false, + whole_word: false, + case_sensitive: false, + }; + + let mut positional = Vec::new(); + while let Some(arg) = args_iter.next() { + match arg.as_str() { + "--regex" => parsed.regex = true, + "--whole-word" => parsed.whole_word = true, + "--case-sensitive" => parsed.case_sensitive = true, + "-r" | "--replace" => { + parsed.replace = args_iter.next(); + } + "--help" | "-h" => { + eprintln!( + "Usage: editor_benchmarks [OPTIONS] \n\n\ + Arguments:\n \ + Path to the file to search in\n \ + The search query string\n\n\ + Options:\n \ + -r, --replace Replacement text (runs replace_all)\n \ + --regex Treat query as regex\n \ + --whole-word Match whole words only\n \ + --case-sensitive Case-sensitive matching\n \ + -h, --help Print help" + ); + std::process::exit(0); + } + other => positional.push(other.to_string()), + } + } + + if positional.len() < 2 { + eprintln!("Usage: editor_benchmarks [OPTIONS] "); + std::process::exit(1); + } + parsed.file = positional.remove(0); + parsed.query = positional.remove(0); + parsed +} + +fn main() { + let args = parse_args(); + + dbg!(&args); + let file_contents = std::fs::read_to_string(&args.file).expect("failed to read input file"); + let file_len = file_contents.len(); + println!("Read {} ({file_len} bytes)", args.file); + + let mut query = if args.regex { + SearchQuery::regex( + &args.query, + args.whole_word, + args.case_sensitive, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .expect("invalid regex query") + } else { + SearchQuery::text( + &args.query, + args.whole_word, + args.case_sensitive, + false, + Default::default(), + Default::default(), + false, + None, + ) + .expect("invalid text query") + }; + + if let Some(replacement) = args.replace.as_deref() { + query = query.with_replacement(replacement.to_string()); + } + + let query = Arc::new(query); + let has_replacement = args.replace.is_some(); + + gpui_platform::headless().run(move |cx| { + release_channel::init_test( + semver::Version::new(0, 0, 0), + release_channel::ReleaseChannel::Dev, + cx, + ); + settings::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + editor::init(cx); + + let buffer = cx.new(|cx| Buffer::local(file_contents, cx)); + + let window_handle = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(gpui::Bounds { + origin: Default::default(), + size: gpui::size(gpui::px(800.0), gpui::px(600.0)), + })), + focus: false, + show: false, + ..Default::default() + }, + |window, cx| cx.new(|cx| Editor::for_buffer(buffer, None, window, cx)), + ) + .expect("failed to open window"); + + window_handle.update(cx, move |this, window, cx| { + cx.spawn_in( + window, + async move |weak: WeakEntity, cx: &mut AsyncWindowContext| { + dbg!("A"); + let find_task = weak.update_in(cx, |editor, window, cx| { + editor.find_matches(query.clone(), window, cx) + })?; + + println!("Finding matches..."); + let timer = std::time::Instant::now(); + let matches: Vec> = find_task.await; + let find_elapsed = timer.elapsed(); + println!("Found {} matches in {find_elapsed:?}", matches.len()); + + if has_replacement && !matches.is_empty() { + window_handle.update(cx, |editor: &mut Editor, window, cx| { + let mut match_iter = matches.iter(); + println!("Replacing all matches..."); + let timer = std::time::Instant::now(); + editor.replace_all( + &mut match_iter, + &query, + Default::default(), + window, + cx, + ); + let replace_elapsed = timer.elapsed(); + println!("Replaced {} matches in {replace_elapsed:?}", matches.len()); + })?; + } + + cx.update(|_, cx: &mut gpui::App| cx.quit()); + anyhow::Ok(()) + }, + ) + .detach(); + }); + }); +} From 269d092692d0480dd00c04e682a82a78787435c6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:23:17 +0100 Subject: [PATCH 02/14] text: batch fragment insertions before turning them into a SumTree This commit introduces a FragmentBuilder which reduces the time needed to run a replace_all on large files by 30%. It does pretty much what a SumTree would, except that it doesn't have to rebalance the tree on each insertion Co-authored-by: Smit Barmase --- crates/editor_benchmarks/src/main.rs | 2 -- crates/text/src/text.rs | 47 +++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/crates/editor_benchmarks/src/main.rs b/crates/editor_benchmarks/src/main.rs index 0a6888bf30d390..52bf9137fd8419 100644 --- a/crates/editor_benchmarks/src/main.rs +++ b/crates/editor_benchmarks/src/main.rs @@ -70,7 +70,6 @@ fn parse_args() -> Args { fn main() { let args = parse_args(); - dbg!(&args); let file_contents = std::fs::read_to_string(&args.file).expect("failed to read input file"); let file_len = file_contents.len(); println!("Read {} ({file_len} bytes)", args.file); @@ -140,7 +139,6 @@ fn main() { cx.spawn_in( window, async move |weak: WeakEntity, cx: &mut AsyncWindowContext| { - dbg!("A"); let find_task = weak.update_in(cx, |editor, window, cx| { editor.find_matches(query.clone(), window, cx) })?; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 026f1272790740..d65753802f3566 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -38,7 +38,7 @@ use std::{ }; pub use subscription::*; pub use sum_tree::Bias; -use sum_tree::{Dimensions, FilterCursor, SumTree, TreeMap, TreeSet}; +use sum_tree::{Dimensions, FilterCursor, SumTree, Summary, TreeMap, TreeSet}; use undo_map::UndoMap; use util::debug_panic; @@ -912,7 +912,8 @@ impl Buffer { let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); let mut old_fragments = self.fragments.cursor::(&None); - let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right); + let mut new_fragments = + FragmentBuilder::new(old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right)); new_ropes.append(new_fragments.summary().text); let mut fragment_start = old_fragments.start().visible; @@ -1044,7 +1045,7 @@ impl Buffer { let (visible_text, deleted_text) = new_ropes.finish(); drop(old_fragments); - self.snapshot.fragments = new_fragments; + self.snapshot.fragments = new_fragments.to_sum_tree(&None); self.snapshot.insertions.edit(new_insertions, ()); self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; @@ -1127,8 +1128,9 @@ impl Buffer { let mut old_fragments = self .fragments .cursor::>(&cx); - let mut new_fragments = - old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left); + let mut new_fragments = FragmentBuilder::new( + old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left), + ); new_ropes.append(new_fragments.summary().text); let mut fragment_start = old_fragments.start().0.full_offset(); @@ -1291,7 +1293,7 @@ impl Buffer { let (visible_text, deleted_text) = new_ropes.finish(); drop(old_fragments); - self.snapshot.fragments = new_fragments; + self.snapshot.fragments = new_fragments.to_sum_tree(&None); self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; self.snapshot.insertions.edit(new_insertions, ()); @@ -1303,7 +1305,7 @@ impl Buffer { new_text: &str, timestamp: clock::Lamport, insertion_offset: &mut u32, - new_fragments: &mut SumTree, + new_fragments: &mut FragmentBuilder, new_insertions: &mut Vec>, insertion_slices: &mut Vec, new_ropes: &mut RopeBuilder, @@ -2836,6 +2838,37 @@ impl BufferSnapshot { } } +struct FragmentBuilder { + fragments: Vec, + summary: FragmentSummary, +} + +impl FragmentBuilder { + fn new(init: SumTree) -> Self { + Self { + summary: init.summary().clone(), + fragments: init.iter().cloned().collect(), + } + } + fn append(&mut self, items: SumTree, cx: &Option) { + self.summary.add_summary(items.summary(), cx); + self.fragments.extend(items.iter().cloned()); + } + fn push(&mut self, fragment: Fragment, cx: &Option) { + self.append(SumTree::from_item(fragment, cx), cx); + } + fn to_sum_tree(self, cx: &Option) -> SumTree { + if self.fragments.len() > 1024 { + SumTree::from_par_iter(self.fragments, cx) + } else { + SumTree::from_iter(self.fragments.into_iter(), cx) + } + } + fn summary(&self) -> &FragmentSummary { + &self.summary + } +} + struct RopeBuilder<'a> { old_visible_cursor: rope::Cursor<'a>, old_deleted_cursor: rope::Cursor<'a>, From 5b241c3b6f7926a144ec551ba790c0d2b488529f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:25:58 +0100 Subject: [PATCH 03/14] clippyyyy Co-authored-by: Smit Barmase --- crates/text/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index d65753802f3566..4d66b8b6cf7f8f 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2861,7 +2861,7 @@ impl FragmentBuilder { if self.fragments.len() > 1024 { SumTree::from_par_iter(self.fragments, cx) } else { - SumTree::from_iter(self.fragments.into_iter(), cx) + SumTree::from_iter(self.fragments, cx) } } fn summary(&self) -> &FragmentSummary { From 0bcda0dff33590d65b14f7b09d2a71772070c5a5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:26:45 +0100 Subject: [PATCH 04/14] add a license file Co-authored-by: Smit Barmase --- crates/editor_benchmarks/LICENSE-GPL | 1 + 1 file changed, 1 insertion(+) create mode 120000 crates/editor_benchmarks/LICENSE-GPL diff --git a/crates/editor_benchmarks/LICENSE-GPL b/crates/editor_benchmarks/LICENSE-GPL new file mode 120000 index 00000000000000..89e542f750cd38 --- /dev/null +++ b/crates/editor_benchmarks/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file From e968fee92f039bb40889c48a75e870eb25b04e3d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:57:07 +0100 Subject: [PATCH 05/14] Clean up editor_benchmarks build Co-authored-by: Smit Barmase --- crates/editor_benchmarks/src/main.rs | 79 +++++++++++++++------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/crates/editor_benchmarks/src/main.rs b/crates/editor_benchmarks/src/main.rs index 52bf9137fd8419..bdd3f1c6beb7e8 100644 --- a/crates/editor_benchmarks/src/main.rs +++ b/crates/editor_benchmarks/src/main.rs @@ -1,9 +1,7 @@ use std::sync::Arc; use editor::Editor; -use gpui::{ - AppContext as _, AsyncApp, AsyncWindowContext, WeakEntity, WindowBounds, WindowOptions, -}; +use gpui::{AppContext as _, AsyncWindowContext, WeakEntity, WindowBounds, WindowOptions}; use language::Buffer; use multi_buffer::Anchor; use project::search::SearchQuery; @@ -135,42 +133,47 @@ fn main() { ) .expect("failed to open window"); - window_handle.update(cx, move |this, window, cx| { - cx.spawn_in( - window, - async move |weak: WeakEntity, cx: &mut AsyncWindowContext| { - let find_task = weak.update_in(cx, |editor, window, cx| { - editor.find_matches(query.clone(), window, cx) - })?; - - println!("Finding matches..."); - let timer = std::time::Instant::now(); - let matches: Vec> = find_task.await; - let find_elapsed = timer.elapsed(); - println!("Found {} matches in {find_elapsed:?}", matches.len()); - - if has_replacement && !matches.is_empty() { - window_handle.update(cx, |editor: &mut Editor, window, cx| { - let mut match_iter = matches.iter(); - println!("Replacing all matches..."); - let timer = std::time::Instant::now(); - editor.replace_all( - &mut match_iter, - &query, - Default::default(), - window, - cx, - ); - let replace_elapsed = timer.elapsed(); - println!("Replaced {} matches in {replace_elapsed:?}", matches.len()); + window_handle + .update(cx, move |_, window, cx| { + cx.spawn_in( + window, + async move |weak: WeakEntity, cx: &mut AsyncWindowContext| { + let find_task = weak.update_in(cx, |editor, window, cx| { + editor.find_matches(query.clone(), window, cx) })?; - } - cx.update(|_, cx: &mut gpui::App| cx.quit()); - anyhow::Ok(()) - }, - ) - .detach(); - }); + println!("Finding matches..."); + let timer = std::time::Instant::now(); + let matches: Vec> = find_task.await; + let find_elapsed = timer.elapsed(); + println!("Found {} matches in {find_elapsed:?}", matches.len()); + + if has_replacement && !matches.is_empty() { + window_handle.update(cx, |editor: &mut Editor, window, cx| { + let mut match_iter = matches.iter(); + println!("Replacing all matches..."); + let timer = std::time::Instant::now(); + editor.replace_all( + &mut match_iter, + &query, + Default::default(), + window, + cx, + ); + let replace_elapsed = timer.elapsed(); + println!( + "Replaced {} matches in {replace_elapsed:?}", + matches.len() + ); + })?; + } + + std::process::exit(0); + anyhow::Ok(()) + }, + ) + .detach(); + }) + .unwrap(); }); } From d38bb62556c47e72d4fe6d6b6d206570a33376bb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:57:24 +0100 Subject: [PATCH 06/14] editor: Parallelize find_all_matches Co-authored-by: Smit Barmase --- crates/editor/src/items.rs | 136 ++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 125f09c96614e1..0dea1f778a486f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -11,7 +11,7 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; use file_icons::FileIcons; use fs::MTime; -use futures::future::try_join_all; +use futures::{channel::oneshot, future::try_join_all}; use git::status::GitSummary; use gpui::{ AnyElement, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, Font, @@ -22,22 +22,24 @@ use language::{ SelectionGoal, proto::serialize_anchor as serialize_text_anchor, }; use lsp::DiagnosticSeverity; -use multi_buffer::{MultiBufferOffset, PathKey}; +use multi_buffer::{BufferOffset, MultiBufferOffset, PathKey}; use project::{ File, Project, ProjectItem as _, ProjectPath, lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, }; +use rope::TextSummary; use rpc::proto::{self, update_view}; use settings::Settings; use std::{ any::{Any, TypeId}, borrow::Cow, cmp::{self, Ordering}, + num::NonZeroU32, ops::Range, path::{Path, PathBuf}, sync::Arc, }; -use text::{BufferId, BufferSnapshot, Selection}; +use text::{BufferId, BufferSnapshot, OffsetRangeExt, Selection}; use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt, rel_path::RelPath}; use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams}; @@ -1871,6 +1873,7 @@ impl SearchableItem for Editor { ranges.iter().cloned().collect::>() }); + let executor = cx.background_executor().clone(); cx.background_spawn(async move { let mut ranges = Vec::new(); @@ -1879,38 +1882,71 @@ impl SearchableItem for Editor { } else { search_within_ranges }; - + let num_cpus = executor.num_cpus(); for range in search_within_ranges { for (search_buffer, search_range, deleted_hunk_anchor) in buffer.range_to_buffer_ranges_with_deleted_hunks(range) { - ranges.extend( - query - .search( - search_buffer, - Some(search_range.start.0..search_range.end.0), - ) - .await - .into_iter() - .filter_map(|match_range| { - if let Some(deleted_hunk_anchor) = deleted_hunk_anchor { - let start = search_buffer - .anchor_after(search_range.start + match_range.start); - let end = search_buffer - .anchor_before(search_range.start + match_range.end); - Some( - deleted_hunk_anchor.with_diff_base_anchor(start) - ..deleted_hunk_anchor.with_diff_base_anchor(end), - ) - } else { - 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.buffer_anchor_range_to_anchor_range(start..end) - } - }), - ); + let query = query.clone(); + + let mut results = Vec::new(); + executor + .scoped(|scope| { + for search_range in chunk_search_range( + search_buffer.text.clone(), + &query, + num_cpus as u32, + search_range, + ) { + let query = query.clone(); + let buffer = buffer.clone(); + + let (tx, rx) = oneshot::channel(); + results.push(rx); + scope.spawn(async move { + let chunk_result = query + .search( + search_buffer, + Some(search_range.start..search_range.end), + ) + .await + .into_iter() + .filter_map(|match_range| { + if let Some(deleted_hunk_anchor) = deleted_hunk_anchor { + let start = search_buffer.anchor_after( + search_range.start + match_range.start, + ); + let end = search_buffer.anchor_before( + search_range.start + match_range.end, + ); + Some( + deleted_hunk_anchor.with_diff_base_anchor(start) + ..deleted_hunk_anchor + .with_diff_base_anchor(end), + ) + } else { + 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 + .buffer_anchor_range_to_anchor_range(start..end) + } + }) + .collect::>(); + _ = tx.send(chunk_result); + }); + } + }) + .await; + + for rx in results { + if let Ok(results) = rx.await { + ranges.extend(results.into_iter()); + } + } } } @@ -2109,6 +2145,44 @@ fn deserialize_path_key(path_key: proto::PathKey) -> Option { }) } +fn chunk_search_range( + buffer: BufferSnapshot, + query: &SearchQuery, + num_cpus: u32, + initial_range: Range, +) -> Box> + 'static> { + let range = initial_range.to_offset(&buffer); + let summary: TextSummary = buffer.text_summary_for_range(initial_range); + let num_chunks = if !query.is_regex() && !query.as_str().contains('\n') { + NonZeroU32::new(summary.lines.row.min(num_cpus)) + } else { + NonZeroU32::new(1) + }; + + let Some(num_chunks) = num_chunks else { + return Box::new(std::iter::empty()); + }; + + let mut chunk_start = range.start; + let rope = buffer.as_rope().clone(); + let total_bytes = summary.len; + let average_chunk_length = total_bytes / (num_chunks.get() as usize); + Box::new(std::iter::from_fn(move || { + if chunk_start >= total_bytes { + return None; + } + let candidate_position = chunk_start + average_chunk_length; + let adjusted = rope.ceil_char_boundary(candidate_position); + let mut as_point = rope.offset_to_point(adjusted); + as_point.row += 1; + as_point.column = 0; + let end_offset = buffer.point_to_offset(as_point).min(total_bytes); + let ret = chunk_start..end_offset; + chunk_start = end_offset; + Some(ret) + })) +} + #[cfg(test)] mod tests { use crate::editor_tests::init_test; From 1b87957f0f7d3adc07618c7683b2e107034210f7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:01:29 +0100 Subject: [PATCH 07/14] clippy Co-authored-by: Smit Barmase --- crates/editor_benchmarks/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/editor_benchmarks/src/main.rs b/crates/editor_benchmarks/src/main.rs index bdd3f1c6beb7e8..1adda3088ada1c 100644 --- a/crates/editor_benchmarks/src/main.rs +++ b/crates/editor_benchmarks/src/main.rs @@ -169,7 +169,6 @@ fn main() { } std::process::exit(0); - anyhow::Ok(()) }, ) .detach(); From 15af1c3a5cfa7a53af6272d64071a3f4273c4e6d Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 2 Apr 2026 15:28:48 +0530 Subject: [PATCH 08/14] fix editor benchmark --- crates/editor_benchmarks/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor_benchmarks/src/main.rs b/crates/editor_benchmarks/src/main.rs index 1adda3088ada1c..81df55334014da 100644 --- a/crates/editor_benchmarks/src/main.rs +++ b/crates/editor_benchmarks/src/main.rs @@ -137,7 +137,9 @@ fn main() { .update(cx, move |_, window, cx| { cx.spawn_in( window, - async move |weak: WeakEntity, cx: &mut AsyncWindowContext| { + async move |weak: WeakEntity, + cx: &mut AsyncWindowContext| + -> anyhow::Result<()> { let find_task = weak.update_in(cx, |editor, window, cx| { editor.find_matches(query.clone(), window, cx) })?; From 49ae39c91588d8bf3c7acaf9abe33db1fc62ed5e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:43:21 +0200 Subject: [PATCH 09/14] Big squeeze on multi-buffer anchor resolutioon perf cc @cole-miller Co-authored-by: Smit Barmase --- crates/multi_buffer/src/multi_buffer.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index de0a43bac914a8..71a0af3f7cbc05 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5295,6 +5295,19 @@ impl MultiBufferSnapshot { &self, text_anchor: Range, ) -> Option> { + 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 +5329,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) { From 97cb66c0b2f6cfb6083ed964075c6b8186fedf46 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 9 Apr 2026 13:02:34 +0530 Subject: [PATCH 10/14] skip redudant excerpts check per match --- crates/editor/src/items.rs | 3 +-- crates/multi_buffer/src/multi_buffer.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0dea1f778a486f..432f2c6571da79 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1931,8 +1931,7 @@ impl SearchableItem for Editor { let end = search_buffer.anchor_before( search_range.start + match_range.end, ); - buffer - .buffer_anchor_range_to_anchor_range(start..end) + buffer.anchor_range_in_buffer(start..end) } }) .collect::>(); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 71a0af3f7cbc05..74eaeef53ebf1a 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -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) -> Option> { + 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 { let excerpts = { From 4a2a0b6edc18063e91565e5c5a4f8d214da77560 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 5 May 2026 09:58:27 +0530 Subject: [PATCH 11/14] do not append empty sum tree --- crates/text/src/text.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 4d66b8b6cf7f8f..4b947234054f10 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2851,8 +2851,10 @@ impl FragmentBuilder { } } fn append(&mut self, items: SumTree, cx: &Option) { - self.summary.add_summary(items.summary(), cx); - self.fragments.extend(items.iter().cloned()); + if !items.is_empty() { + self.summary.add_summary(items.summary(), cx); + self.fragments.extend(items.iter().cloned()); + } } fn push(&mut self, fragment: Fragment, cx: &Option) { self.append(SumTree::from_item(fragment, cx), cx); From cb4ba78fd49408769bde9d8cca8525a8a91338bf Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 6 May 2026 12:29:49 +0530 Subject: [PATCH 12/14] clippy --- crates/editor/src/items.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 432f2c6571da79..65b84fb8a8dee5 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1943,7 +1943,7 @@ impl SearchableItem for Editor { for rx in results { if let Ok(results) = rx.await { - ranges.extend(results.into_iter()); + ranges.extend(results); } } } From 0ba1a88e89cf52634c64b0e6f54a1d53e9f8fbce Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 6 May 2026 13:37:11 +0530 Subject: [PATCH 13/14] fix absolute buffer offsets compare against relative byte length --- crates/editor/src/items.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 65b84fb8a8dee5..f71b2301c07c0c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -2151,9 +2151,13 @@ fn chunk_search_range( initial_range: Range, ) -> Box> + 'static> { let range = initial_range.to_offset(&buffer); + if range.is_empty() { + return Box::new(std::iter::empty()); + } + let summary: TextSummary = buffer.text_summary_for_range(initial_range); let num_chunks = if !query.is_regex() && !query.as_str().contains('\n') { - NonZeroU32::new(summary.lines.row.min(num_cpus)) + NonZeroU32::new(summary.lines.row.saturating_add(1).min(num_cpus.max(1))) } else { NonZeroU32::new(1) }; @@ -2164,10 +2168,10 @@ fn chunk_search_range( let mut chunk_start = range.start; let rope = buffer.as_rope().clone(); - let total_bytes = summary.len; - let average_chunk_length = total_bytes / (num_chunks.get() as usize); + let range_end = range.end; + let average_chunk_length = summary.len.div_ceil(num_chunks.get() as usize); Box::new(std::iter::from_fn(move || { - if chunk_start >= total_bytes { + if chunk_start >= range_end { return None; } let candidate_position = chunk_start + average_chunk_length; @@ -2175,7 +2179,7 @@ fn chunk_search_range( let mut as_point = rope.offset_to_point(adjusted); as_point.row += 1; as_point.column = 0; - let end_offset = buffer.point_to_offset(as_point).min(total_bytes); + let end_offset = buffer.point_to_offset(as_point).min(range_end); let ret = chunk_start..end_offset; chunk_start = end_offset; Some(ret) From 82486c2558f46454716960882731eb773b675203 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 6 May 2026 14:02:05 +0530 Subject: [PATCH 14/14] add tests --- crates/editor/src/items.rs | 109 +++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f71b2301c07c0c..c352ec9d03f17e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -2211,6 +2211,115 @@ mod tests { assert_eq!(path_for_file(&file, 0, false, cx), None); } + #[gpui::test] + fn test_chunk_search_range_multi_line(cx: &mut App) { + let text = "line one\nline two\nline three\nline four\nline five\nline six\n"; + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let snapshot = buffer.read(cx).snapshot(); + + let chunks = chunk_search_range_for_test(&snapshot, "line", 4, 0..text.len()); + + assert_chunks_are_contiguous(&chunks, 0..text.len()); + assert!( + chunks.len() <= 4, + "got {} chunks, expected <= num_cpus (4)", + chunks.len() + ); + for chunk in &chunks { + let end = chunk.end; + assert!( + end == text.len() || text.as_bytes()[end - 1] == b'\n', + "chunk ending at {end} is not a line boundary", + ); + } + } + + #[gpui::test] + fn test_chunk_search_range_single_line(cx: &mut App) { + let text = "hello world hello again"; + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let snapshot = buffer.read(cx).snapshot(); + + let chunks = chunk_search_range_for_test(&snapshot, "hello", 4, 0..text.len()); + assert_chunks_are_contiguous(&chunks, 0..text.len()); + } + + #[gpui::test] + fn test_chunk_search_range_empty_range(cx: &mut App) { + let buffer = cx.new(|cx| Buffer::local("hello world", cx)); + let snapshot = buffer.read(cx).snapshot(); + + let chunks = chunk_search_range_for_test(&snapshot, "hello", 4, 5..5); + assert!(chunks.is_empty()); + } + + #[gpui::test] + fn test_chunk_search_range_does_not_start_at_zero(cx: &mut App) { + let line = "abcdefghij\n"; + let text = line.repeat(20); + let buffer = cx.new(|cx| Buffer::local(text.clone(), cx)); + let snapshot = buffer.read(cx).snapshot(); + + let start = line.len() * 7; + let end = line.len() * 14; + let chunks = chunk_search_range_for_test(&snapshot, "abc", 4, start..end); + + assert_chunks_are_contiguous(&chunks, start..end); + } + + fn chunk_search_range_for_test( + snapshot: &language::BufferSnapshot, + query: &str, + num_cpus: u32, + range: Range, + ) -> Vec> { + let query = SearchQuery::text( + query, + false, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .unwrap(); + chunk_search_range( + snapshot.text.clone(), + &query, + num_cpus, + BufferOffset(range.start)..BufferOffset(range.end), + ) + .collect() + } + + #[track_caller] + fn assert_chunks_are_contiguous(chunks: &[Range], expected: Range) { + assert!(!chunks.is_empty(), "expected at least one chunk"); + assert_eq!( + chunks.first().unwrap().start, + expected.start, + "first chunk does not start at {}", + expected.start + ); + assert_eq!( + chunks.last().unwrap().end, + expected.end, + "last chunk does not end at {}", + expected.end + ); + for chunk in chunks { + assert!(chunk.start < chunk.end, "empty chunk: {:?}", chunk); + } + for window in chunks.windows(2) { + assert_eq!( + window[0].end, window[1].start, + "gap or overlap between chunks {:?} and {:?}", + window[0], window[1], + ); + } + } + async fn deserialize_editor( item_id: ItemId, workspace_id: WorkspaceId,