project search: Reduce hangs on main thread (#39857)

This takes the idea that @RemcoSmitsDev started on in
https://github.com/zed-industries/zed/pull/39354. We did away with
grabbing a snapshot of the display map when buffer coordinates were
sufficient.
Closes #37267

Release Notes:

- Reduced micro-stutters in project search with large multi-buffer
contents.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Piotr Osiewicz
2025-10-09 13:11:11 +02:00
committed by GitHub
co-authored by Smit Barmase
parent 37d676e2c6
commit ba2337ffb9
5 changed files with 84 additions and 41 deletions
+34 -24
View File
@@ -3172,7 +3172,7 @@ impl Editor {
self.refresh_code_actions(window, cx);
self.refresh_document_highlights(cx);
self.refresh_selected_text_highlights(false, window, cx);
refresh_matching_bracket_highlights(self, window, cx);
refresh_matching_bracket_highlights(self, cx);
self.update_visible_edit_prediction(window, cx);
self.edit_prediction_requires_modifier_in_indent_conflict = true;
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
@@ -6607,26 +6607,32 @@ impl Editor {
&self.context_menu
}
fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> {
let newest_selection = self.selections.newest_anchor().clone();
let newest_selection_adjusted = self.selections.newest_adjusted(cx);
let buffer = self.buffer.read(cx);
if newest_selection.head().diff_base_anchor.is_some() {
return None;
}
let (start_buffer, start) =
buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
let (end_buffer, end) =
buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
if start_buffer != end_buffer {
return None;
}
fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
.await;
let (start_buffer, start, _, end, newest_selection) = this
.update(cx, |this, cx| {
let newest_selection = this.selections.newest_anchor().clone();
if newest_selection.head().diff_base_anchor.is_some() {
return None;
}
let newest_selection_adjusted = this.selections.newest_adjusted(cx);
let buffer = this.buffer.read(cx);
let (start_buffer, start) =
buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
let (end_buffer, end) =
buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
Some((start_buffer, start, end_buffer, end, newest_selection))
})?
.filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)
.context(
"Expected selection to lie in a single buffer when refreshing code actions",
)?;
let (providers, tasks) = this.update_in(cx, |this, window, cx| {
let providers = this.code_action_providers.clone();
let tasks = this
@@ -6667,7 +6673,6 @@ impl Editor {
cx.notify();
})
}));
None
}
fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -6917,19 +6922,24 @@ impl Editor {
if self.selections.count() != 1 || self.selections.line_mode() {
return None;
}
let selection = self.selections.newest::<Point>(cx);
if selection.is_empty() || selection.start.row != selection.end.row {
let selection = self.selections.newest_anchor();
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let selection_point_range = selection.start.to_point(&multi_buffer_snapshot)
..selection.end.to_point(&multi_buffer_snapshot);
// If the selection spans multiple rows OR it is empty
if selection_point_range.start.row != selection_point_range.end.row
|| selection_point_range.start.column == selection_point_range.end.column
{
return None;
}
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot);
let query = multi_buffer_snapshot
.text_for_range(selection_anchor_range.clone())
.text_for_range(selection.range())
.collect::<String>();
if query.trim().is_empty() {
return None;
}
Some((query, selection_anchor_range))
Some((query, selection.range()))
}
fn update_selection_occurrence_highlights(
@@ -20805,7 +20815,7 @@ impl Editor {
self.refresh_code_actions(window, cx);
self.refresh_selected_text_highlights(true, window, cx);
self.refresh_single_line_folds(window, cx);
refresh_matching_bracket_highlights(self, window, cx);
refresh_matching_bracket_highlights(self, cx);
if self.has_active_edit_prediction() {
self.update_visible_edit_prediction(window, cx);
}
+15 -16
View File
@@ -1,47 +1,46 @@
use crate::{Editor, RangeToAnchorExt};
use gpui::{Context, HighlightStyle, Window};
use gpui::{Context, HighlightStyle};
use language::CursorShape;
use multi_buffer::ToOffset;
use theme::ActiveTheme;
enum MatchingBracketHighlight {}
pub fn refresh_matching_bracket_highlights(
editor: &mut Editor,
window: &mut Window,
cx: &mut Context<Editor>,
) {
pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut Context<Editor>) {
editor.clear_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx);
let buffer_snapshot = editor.buffer.read(cx).snapshot(cx);
let newest_selection = editor
.selections
.newest_anchor()
.map(|anchor| anchor.to_offset(&buffer_snapshot));
// Don't highlight brackets if the selection isn't empty
if !newest_selection.is_empty() {
return;
}
let snapshot = editor.snapshot(window, cx);
let head = newest_selection.head();
if head > snapshot.buffer_snapshot().len() {
if head > buffer_snapshot.len() {
log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
return;
}
let mut tail = head;
if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow)
&& head < snapshot.buffer_snapshot().len()
&& head < buffer_snapshot.len()
{
if let Some(tail_ch) = snapshot.buffer_snapshot().chars_at(tail).next() {
if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
tail += tail_ch.len_utf8();
}
}
if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot()
.innermost_enclosing_bracket_ranges(head..tail, None)
if let Some((opening_range, closing_range)) =
buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
{
editor.highlight_text::<MatchingBracketHighlight>(
vec![
opening_range.to_anchors(&snapshot.buffer_snapshot()),
closing_range.to_anchors(&snapshot.buffer_snapshot()),
opening_range.to_anchors(&buffer_snapshot),
closing_range.to_anchors(&buffer_snapshot),
],
HighlightStyle {
background_color: Some(
@@ -184,6 +184,27 @@ impl SelectionsCollection {
selections
}
/// Returns all of the selections, adjusted to take into account the selection line_mode. Uses a provided snapshot to resolve selections.
pub fn all_adjusted_with_snapshot(
&self,
snapshot: &MultiBufferSnapshot,
) -> Vec<Selection<Point>> {
let mut selections = self
.disjoint
.iter()
.chain(self.pending_anchor())
.map(|anchor| anchor.map(|anchor| anchor.to_point(&snapshot)))
.collect::<Vec<_>>();
if self.line_mode {
for selection in &mut selections {
let new_range = snapshot.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
}
}
selections
}
/// Returns the newest selection, adjusted to take into account the selection line_mode
pub fn newest_adjusted(&self, cx: &mut App) -> Selection<Point> {
let mut selection = self.newest::<Point>(cx);
+3 -1
View File
@@ -113,7 +113,9 @@ impl CursorPosition {
let mut last_selection = None::<Selection<Point>>;
let snapshot = editor.buffer().read(cx).snapshot(cx);
if snapshot.excerpts().count() > 0 {
for selection in editor.selections.all_adjusted(cx) {
for selection in
editor.selections.all_adjusted_with_snapshot(&snapshot)
{
let selection_summary = snapshot
.text_summary_for_range::<text::TextSummary, _>(
selection.start..selection.end,
+11
View File
@@ -6385,6 +6385,17 @@ impl MultiBufferSnapshot {
debug_ranges.insert(key, text_ranges, format!("{value:?}").into())
});
}
// used by line_mode selections and tries to match vim behavior
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
let new_start = MultiBufferPoint::new(range.start.row, 0);
let new_end = if range.end.column > 0 {
MultiBufferPoint::new(range.end.row, self.line_len(MultiBufferRow(range.end.row)))
} else {
range.end
};
new_start..new_end
}
}
#[cfg(any(test, feature = "test-support"))]