Improve project search performance (#20211)

Follow-up of https://github.com/zed-industries/zed/pull/20171

Reduces time Zed needs to reach maximum search results by an order of a
magnitude.

Methodology: 
* plugged-in mac with Instruments and Zed open
* Zed is restarted before each measurement, `zed` project is opened, a
*.rs file is opened and rust-analyzer is fully loaded, file is closed
then
* from an "empty" state, a `test` word is searched in the project search
* each version is checked with project panel; and then, separately,
without it
* after we reach maximum test results (the counter stops at `10191+`),
the measurement stops

Zed Dev is compiled and installed with `./script/bundle-mac -li`

------------------------


[measurements.trace.zip](https://github.com/user-attachments/files/17625516/measurements.trace.zip)

Before:

* Zed Nightly with outline panel open

<img width="1113" alt="image"
src="https://github.com/user-attachments/assets/62b29a69-c266-4d46-8c3c-0e9534ca7967">

Took over 30s to load the result set

* Zed Nightly without outline panel

<img width="1109" alt="image"
src="https://github.com/user-attachments/assets/82d8d9d6-e8f2-4e67-af55-3f54a7c1d92d">

Took over 24s to load the result set

* Zed Dev with outline panel open

<img width="1131" alt="image"
src="https://github.com/user-attachments/assets/15605ff8-0787-428e-bbb6-f8496f7e1d43">

Took around 6s to load the result set (the profile was running a bit
longer)

* Zed Dev without outline panel

<img width="1147" alt="image"
src="https://github.com/user-attachments/assets/0715d73e-f41a-4d74-a604-a3a96ad8d585">

Took around 5s to load the result set

---------------------

Improvements in the outline panel:

* https://github.com/zed-industries/zed/pull/20171 ensured we reuse
previous rendered search results from the outline panel
* all search results are now rendered in the background thread
* only the entries that are rendered with gpui are sent to the
background thread for rendering
* FS entries' update logic does nothing before the debounce now

Improvements in the editor:

* cursor update operations are debounced and all calculations start
after the debounce only
* linked edits are now debounced and all work is done after the debounce
only

Further possible improvements:

* we could batch calculations of text coordinates, related to the search
entries: right now, each search match range is expanded around and
clipped, then fitted to the closest surrounding whitespace (if any,
otherwise it's just trimmed).
Each such calculation requires multiple tree traversals, which is
suboptimal and causes more CPU usage than we could use.

* linked edits are always calculated, even if the language settings have
it disabled, or the corresponding language having no corresponding
capabilities

Release Notes:

- Improve large project search performance
This commit is contained in:
Kirill Bulatov 2024-11-05 03:49:37 +02:00 committed by GitHub
parent 81dd4ca1c9
commit 3856599853
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 330 additions and 244 deletions

View file

@ -1,4 +1,4 @@
use std::ops::Range;
use std::{ops::Range, time::Duration};
use collections::HashMap;
use itertools::Itertools;
@ -36,35 +36,53 @@ impl LinkedEditingRanges {
self.0.is_empty()
}
}
pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Editor>) -> Option<()> {
if this.pending_rename.is_some() {
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
// TODO do not refresh anything at all, if the settings/capabilities do not have it enabled.
pub(super) fn refresh_linked_ranges(
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
) -> Option<()> {
if editor.pending_rename.is_some() {
return None;
}
let project = this.project.clone()?;
let selections = this.selections.all::<usize>(cx);
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();
let start_position = snapshot.anchor_before(cursor_position);
let end_position = snapshot.anchor_after(selection.tail());
if start_position.buffer_id != end_position.buffer_id || end_position.buffer_id.is_none() {
// Throw away selections spanning multiple buffers.
continue;
let project = editor.project.as_ref()?.downgrade();
editor.linked_editing_range_task = Some(cx.spawn(|editor, mut cx| async move {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
let mut applicable_selections = Vec::new();
editor
.update(&mut cx, |editor, cx| {
let selections = editor.selections.all::<usize>(cx);
let snapshot = editor.buffer.read(cx).snapshot(cx);
let buffer = editor.buffer.read(cx);
for selection in selections {
let cursor_position = selection.head();
let start_position = snapshot.anchor_before(cursor_position);
let end_position = snapshot.anchor_after(selection.tail());
if start_position.buffer_id != end_position.buffer_id
|| end_position.buffer_id.is_none()
{
// Throw away selections spanning multiple buffers.
continue;
}
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
applicable_selections.push((
buffer,
start_position.text_anchor,
end_position.text_anchor,
));
}
}
})
.ok()?;
if applicable_selections.is_empty() {
return None;
}
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
applicable_selections.push((
buffer,
start_position.text_anchor,
end_position.text_anchor,
));
}
}
if applicable_selections.is_empty() {
return None;
}
this.linked_editing_range_task = Some(cx.spawn(|this, mut cx| async move {
let highlights = project
.update(&mut cx, |project, cx| {
let mut linked_edits_tasks = vec![];
@ -110,37 +128,38 @@ pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Edit
}
linked_edits_tasks
})
.log_err()?;
.ok()?;
let highlights = futures::future::join_all(highlights).await;
this.update(&mut cx, |this, cx| {
this.linked_edit_ranges.0.clear();
if this.pending_rename.is_some() {
return;
}
for (buffer_id, ranges) in highlights.into_iter().flatten() {
this.linked_edit_ranges
.0
.entry(buffer_id)
.or_default()
.extend(ranges);
}
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
let Some(snapshot) = this
.buffer
.read(cx)
.buffer(*buffer_id)
.map(|buffer| buffer.read(cx).snapshot())
else {
continue;
};
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
}
editor
.update(&mut cx, |this, cx| {
this.linked_edit_ranges.0.clear();
if this.pending_rename.is_some() {
return;
}
for (buffer_id, ranges) in highlights.into_iter().flatten() {
this.linked_edit_ranges
.0
.entry(buffer_id)
.or_default()
.extend(ranges);
}
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
let Some(snapshot) = this
.buffer
.read(cx)
.buffer(*buffer_id)
.map(|buffer| buffer.read(cx).snapshot())
else {
continue;
};
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
}
cx.notify();
})
.log_err();
cx.notify();
})
.ok()?;
Some(())
}));

View file

@ -1,9 +1,9 @@
use editor::{Editor, ToPoint};
use gpui::{AppContext, Subscription, View, WeakView};
use gpui::{AppContext, Subscription, Task, View, WeakView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::fmt::Write;
use std::{fmt::Write, time::Duration};
use text::{Point, Selection};
use ui::{
div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
@ -23,6 +23,7 @@ pub struct CursorPosition {
position: Option<Point>,
selected_count: SelectionStats,
workspace: WeakView<Workspace>,
update_position: Task<()>,
_observe_active_editor: Option<Subscription>,
}
@ -32,40 +33,61 @@ impl CursorPosition {
position: None,
selected_count: Default::default(),
workspace: workspace.weak_handle(),
update_position: Task::ready(()),
_observe_active_editor: None,
}
}
fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
fn update_position(
&mut self,
editor: View<Editor>,
debounce: Option<Duration>,
cx: &mut ViewContext<Self>,
) {
let editor = editor.downgrade();
self.update_position = cx.spawn(|cursor_position, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
self.selected_count = Default::default();
self.selected_count.selections = editor.selections.count();
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections.all::<usize>(cx) {
self.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
self.selected_count.lines += (selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
self.selected_count.lines += 1;
}
}
}
self.position = last_selection.map(|s| s.head().to_point(&buffer));
editor
.update(&mut cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
cursor_position.update(cx, |cursor_position, cx| {
cursor_position.selected_count = SelectionStats::default();
cursor_position.selected_count.selections = editor.selections.count();
let mut last_selection = None::<Selection<usize>>;
for selection in editor.selections.all::<usize>(cx) {
cursor_position.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
cursor_position.selected_count.lines +=
(selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
cursor_position.selected_count.lines += 1;
}
}
}
cursor_position.position =
last_selection.map(|s| s.head().to_point(&buffer));
cx.notify();
})
})
.ok()
.transpose()
.ok()
.flatten();
});
cx.notify();
}
fn write_position(&self, text: &mut String, cx: &AppContext) {
@ -154,6 +176,8 @@ impl Render for CursorPosition {
}
}
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
impl StatusItemView for CursorPosition {
fn set_active_pane_item(
&mut self,
@ -161,8 +185,11 @@ impl StatusItemView for CursorPosition {
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
self.update_position(editor, cx);
self._observe_active_editor =
Some(cx.observe(&editor, |cursor_position, editor, cx| {
Self::update_position(cursor_position, editor, Some(UPDATE_DEBOUNCE), cx)
}));
self.update_position(editor, None, cx);
} else {
self.position = None;
self._observe_active_editor = None;

View file

@ -229,7 +229,7 @@ mod tests {
use indoc::indoc;
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use workspace::{AppState, Workspace};
#[gpui::test]
@ -379,6 +379,7 @@ mod tests {
.downcast::<Editor>()
.unwrap();
cx.executor().advance_clock(Duration::from_millis(200));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {
@ -397,6 +398,7 @@ mod tests {
);
});
editor.update(cx, |editor, cx| editor.select_all(&SelectAll, cx));
cx.executor().advance_clock(Duration::from_millis(200));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {

View file

@ -1,7 +1,6 @@
mod outline_panel_settings;
use std::{
cell::OnceCell,
cmp,
hash::Hash,
ops::Range,
@ -133,7 +132,7 @@ enum ItemsDisplayMode {
struct SearchState {
kind: SearchKind,
query: String,
matches: Vec<(Range<editor::Anchor>, OnceCell<Arc<SearchData>>)>,
matches: Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>,
highlight_search_match_tx: channel::Sender<HighlightArguments>,
_search_match_highlighter: Task<()>,
_search_match_notify: Task<()>,
@ -141,37 +140,55 @@ struct SearchState {
struct HighlightArguments {
multi_buffer_snapshot: MultiBufferSnapshot,
search_data: Arc<SearchData>,
match_range: Range<editor::Anchor>,
search_data: Arc<OnceLock<SearchData>>,
}
impl SearchState {
fn new(
kind: SearchKind,
query: String,
previous_matches: HashMap<Range<editor::Anchor>, Arc<OnceLock<SearchData>>>,
new_matches: Vec<Range<editor::Anchor>>,
theme: Arc<SyntaxTheme>,
cx: &mut ViewContext<'_, OutlinePanel>,
) -> Self {
let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();
let (notify_tx, notify_rx) = channel::bounded::<()>(1);
let (notify_tx, notify_rx) = channel::unbounded::<()>();
Self {
kind,
query,
matches: new_matches
.into_iter()
.map(|range| (range, OnceCell::new()))
.map(|range| {
let search_data = previous_matches
.get(&range)
.map(Arc::clone)
.unwrap_or_default();
(range, search_data)
})
.collect(),
highlight_search_match_tx,
_search_match_highlighter: cx.background_executor().spawn(async move {
while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {
let highlight_data = &highlight_arguments.search_data.highlights_data;
let needs_init = highlight_arguments.search_data.get().is_none();
let search_data = highlight_arguments.search_data.get_or_init(|| {
SearchData::new(
&highlight_arguments.match_range,
&highlight_arguments.multi_buffer_snapshot,
)
});
if needs_init {
notify_tx.try_send(()).ok();
}
let highlight_data = &search_data.highlights_data;
if highlight_data.get().is_some() {
continue;
}
let mut left_whitespaces_count = 0;
let mut non_whitespace_symbol_occurred = false;
let context_offset_range = highlight_arguments
.search_data
let context_offset_range = search_data
.context_range
.to_offset(&highlight_arguments.multi_buffer_snapshot);
let mut offset = context_offset_range.start;
@ -222,13 +239,20 @@ impl SearchState {
let trimmed_text = context_text[left_whitespaces_count..].to_owned();
debug_assert_eq!(
trimmed_text, highlight_arguments.search_data.context_text,
trimmed_text, search_data.context_text,
"Highlighted text that does not match the buffer text"
);
}
}),
_search_match_notify: cx.spawn(|outline_panel, mut cx| async move {
while let Ok(()) = notify_rx.recv().await {
loop {
match notify_rx.recv().await {
Ok(()) => {}
Err(_) => break,
};
while let Ok(()) = notify_rx.try_recv() {
//
}
let update_result = outline_panel.update(&mut cx, |_, cx| {
cx.notify();
});
@ -239,24 +263,6 @@ impl SearchState {
}),
}
}
fn highlight_search_match(
&mut self,
match_range: &Range<editor::Anchor>,
multi_buffer_snapshot: &MultiBufferSnapshot,
) {
if let Some((_, search_data)) = self.matches.iter().find(|(range, _)| range == match_range)
{
let search_data = search_data
.get_or_init(|| Arc::new(SearchData::new(match_range, multi_buffer_snapshot)));
self.highlight_search_match_tx
.send_blocking(HighlightArguments {
multi_buffer_snapshot: multi_buffer_snapshot.clone(),
search_data: Arc::clone(search_data),
})
.ok();
}
}
}
#[derive(Debug)]
@ -356,7 +362,7 @@ enum PanelEntry {
struct SearchEntry {
match_range: Range<editor::Anchor>,
kind: SearchKind,
render_data: Arc<SearchData>,
render_data: Arc<OnceLock<SearchData>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
@ -1890,18 +1896,30 @@ impl OutlinePanel {
&mut self,
multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
match_range: &Range<editor::Anchor>,
search_data: &Arc<SearchData>,
render_data: &Arc<OnceLock<SearchData>>,
kind: SearchKind,
depth: usize,
string_match: Option<&StringMatch>,
cx: &mut ViewContext<Self>,
) -> Stateful<Div> {
if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
search_state.highlight_search_match(match_range, multi_buffer_snapshot);
) -> Option<Stateful<Div>> {
let search_data = match render_data.get() {
Some(search_data) => search_data,
None => {
if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
search_state
.highlight_search_match_tx
.try_send(HighlightArguments {
multi_buffer_snapshot: multi_buffer_snapshot.clone(),
match_range: match_range.clone(),
search_data: Arc::clone(render_data),
})
.ok();
}
}
return None;
}
}
};
let search_matches = string_match
.iter()
.flat_map(|string_match| string_match.ranges())
@ -1948,11 +1966,11 @@ impl OutlinePanel {
})) => match_range == selected_match_range,
_ => false,
};
self.entry_element(
Some(self.entry_element(
PanelEntry::Search(SearchEntry {
kind,
match_range: match_range.clone(),
render_data: Arc::clone(search_data),
render_data: render_data.clone(),
}),
ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
depth,
@ -1960,7 +1978,7 @@ impl OutlinePanel {
is_active,
entire_label,
cx,
)
))
}
#[allow(clippy::too_many_arguments)]
@ -2065,56 +2083,64 @@ impl OutlinePanel {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
let active_multi_buffer = active_editor.read(cx).buffer().clone();
let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
let mut new_collapsed_entries = self.collapsed_entries.clone();
let mut new_unfolded_dirs = self.unfolded_dirs.clone();
let mut root_entries = HashSet::default();
let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
HashMap::default(),
|mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
let buffer_id = buffer_snapshot.remote_id();
let file = File::from_dyn(buffer_snapshot.file());
let entry_id = file.and_then(|file| file.project_entry_id(cx));
let worktree = file.map(|file| file.worktree.read(cx).snapshot());
let is_new =
new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
buffer_excerpts
.entry(buffer_id)
.or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
.1
.push(excerpt_id);
let outlines = match self
.excerpts
.get(&buffer_id)
.and_then(|excerpts| excerpts.get(&excerpt_id))
{
Some(old_excerpt) => match &old_excerpt.outlines {
ExcerptOutlines::Outlines(outlines) => {
ExcerptOutlines::Outlines(outlines.clone())
}
ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
},
None => ExcerptOutlines::NotFetched,
};
new_excerpts.entry(buffer_id).or_default().insert(
excerpt_id,
Excerpt {
range: excerpt_range,
outlines,
},
);
buffer_excerpts
},
);
self.updating_fs_entries = true;
self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
let mut new_collapsed_entries = HashSet::default();
let mut new_unfolded_dirs = HashMap::default();
let mut root_entries = HashSet::default();
let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
let Ok(buffer_excerpts) = outline_panel.update(&mut cx, |outline_panel, cx| {
new_collapsed_entries = outline_panel.collapsed_entries.clone();
new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
HashMap::default(),
|mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
let buffer_id = buffer_snapshot.remote_id();
let file = File::from_dyn(buffer_snapshot.file());
let entry_id = file.and_then(|file| file.project_entry_id(cx));
let worktree = file.map(|file| file.worktree.read(cx).snapshot());
let is_new = new_entries.contains(&excerpt_id)
|| !outline_panel.excerpts.contains_key(&buffer_id);
buffer_excerpts
.entry(buffer_id)
.or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
.1
.push(excerpt_id);
let outlines = match outline_panel
.excerpts
.get(&buffer_id)
.and_then(|excerpts| excerpts.get(&excerpt_id))
{
Some(old_excerpt) => match &old_excerpt.outlines {
ExcerptOutlines::Outlines(outlines) => {
ExcerptOutlines::Outlines(outlines.clone())
}
ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
},
None => ExcerptOutlines::NotFetched,
};
new_excerpts.entry(buffer_id).or_default().insert(
excerpt_id,
Excerpt {
range: excerpt_range,
outlines,
},
);
buffer_excerpts
},
);
buffer_excerpts
}) else {
return;
};
let Some((
new_collapsed_entries,
new_unfolded_dirs,
@ -3257,11 +3283,13 @@ impl OutlinePanel {
OutlineEntry::Excerpt(..) => {}
},
PanelEntry::Search(new_search_entry) => {
state.match_candidates.push(StringMatchCandidate {
id,
char_bag: new_search_entry.render_data.context_text.chars().collect(),
string: new_search_entry.render_data.context_text.clone(),
});
if let Some(search_data) = new_search_entry.render_data.get() {
state.match_candidates.push(StringMatchCandidate {
id,
char_bag: search_data.context_text.chars().collect(),
string: search_data.context_text.clone(),
});
}
}
}
}
@ -3381,20 +3409,26 @@ impl OutlinePanel {
)
};
update_cached_entries = match &self.mode {
let mut previous_matches = HashMap::default();
update_cached_entries = match &mut self.mode {
ItemsDisplayMode::Search(current_search_state) => {
current_search_state.query != new_search_query
let update = current_search_state.query != new_search_query
|| current_search_state.kind != kind
|| current_search_state.matches.is_empty()
|| current_search_state.matches.iter().enumerate().any(
|(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
)
);
if current_search_state.kind == kind {
previous_matches.extend(current_search_state.matches.drain(..));
}
update
}
ItemsDisplayMode::Outline => true,
};
self.mode = ItemsDisplayMode::Search(SearchState::new(
kind,
new_search_query,
previous_matches,
new_search_matches,
cx.theme().syntax().clone(),
cx,
@ -3475,7 +3509,7 @@ impl OutlinePanel {
is_singleton: bool,
cx: &mut ViewContext<Self>,
) {
let Some(active_editor) = self.active_editor() else {
if self.active_editor().is_none() {
return;
};
let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
@ -3493,56 +3527,16 @@ impl OutlinePanel {
.collect::<HashSet<_>>();
let depth = if is_singleton { 0 } else { parent_depth + 1 };
let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
let new_search_matches = search_state.matches.iter().filter(|(match_range, _)| {
related_excerpts.contains(&match_range.start.excerpt_id)
|| related_excerpts.contains(&match_range.end.excerpt_id)
});
let previous_search_matches = self
.cached_entries
.iter()
.filter_map(|entry| {
if let PanelEntry::Search(search_entry) = &entry.entry {
Some(search_entry)
} else {
None
}
})
.filter(|search_entry| search_entry.kind == kind)
.filter(|search_entry| {
related_excerpts.contains(&search_entry.match_range.start.excerpt_id)
|| related_excerpts.contains(&search_entry.match_range.end.excerpt_id)
})
.map(|search_entry| (&search_entry.match_range, &search_entry.render_data))
.collect::<HashMap<_, _>>();
let new_search_entries = new_search_matches
.map(|(match_range, search_data)| {
let previous_search_data = previous_search_matches.get(&match_range).copied();
let render_data = search_data
.get()
.or(previous_search_data)
.unwrap_or_else(|| {
search_data.get_or_init(|| {
Arc::new(SearchData::new(match_range, &multi_buffer_snapshot))
})
});
if let (Some(previous_highlights), None) = (
previous_search_data.and_then(|data| data.highlights_data.get()),
render_data.highlights_data.get(),
) {
render_data
.highlights_data
.set(previous_highlights.clone())
.ok();
}
SearchEntry {
match_range: match_range.clone(),
kind,
render_data: Arc::clone(render_data),
}
.map(|(match_range, search_data)| SearchEntry {
match_range: match_range.clone(),
kind,
render_data: Arc::clone(search_data),
})
.collect::<Vec<_>>();
for new_search_entry in new_search_entries {
@ -3807,7 +3801,11 @@ impl OutlinePanel {
.map(|label| label.len())
.unwrap_or_default(),
PanelEntry::Outline(OutlineEntry::Outline(_, _, outline)) => outline.text.len(),
PanelEntry::Search(search) => search.render_data.context_text.len(),
PanelEntry::Search(search) => search
.render_data
.get()
.map(|data| data.context_text.len())
.unwrap_or_default(),
};
(item_text_chars + depth) as u64
@ -3913,7 +3911,7 @@ impl OutlinePanel {
render_data,
kind,
..
}) => Some(outline_panel.render_search_match(
}) => outline_panel.render_search_match(
multi_buffer_snapshot.as_ref(),
&match_range,
&render_data,
@ -3921,7 +3919,7 @@ impl OutlinePanel {
cached_entry.depth,
cached_entry.string_match.as_ref(),
cx,
)),
),
})
.collect()
}
@ -4480,6 +4478,7 @@ mod tests {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4493,6 +4492,7 @@ mod tests {
outline_panel.select_parent(&SelectParent, cx);
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4503,9 +4503,10 @@ mod tests {
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
});
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4538,6 +4539,7 @@ mod tests {
outline_panel.select_parent(&SelectParent, cx);
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4549,6 +4551,7 @@ mod tests {
outline_panel.select_parent(&SelectParent, cx);
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4560,9 +4563,10 @@ mod tests {
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
});
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4583,9 +4587,10 @@ mod tests {
outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
});
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4661,9 +4666,13 @@ mod tests {
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(&outline_panel.cached_entries, None,),
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
None,
),
all_matches,
);
});
@ -4678,9 +4687,13 @@ mod tests {
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(&outline_panel.cached_entries, None),
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
None,
),
all_matches
.lines()
.filter(|item| item.contains(filter_text))
@ -4697,9 +4710,13 @@ mod tests {
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(&outline_panel.cached_entries, None,),
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
None,
),
all_matches,
);
});
@ -4784,16 +4801,17 @@ mod tests {
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
r#"/
public/lottie/
syntax-tree.json
search: { "something": "static" }
search: { "something": "static" } <==== selected
src/
app/(site)/
(about)/jobs/[slug]/
@ -4809,17 +4827,17 @@ mod tests {
});
outline_panel.update(cx, |outline_panel, cx| {
// After the search is done, we have updated the outline panel contents and caret is not in any excerot, so there are no selections.
// Move to 5th element in the list (0th action will selection the first element)
for _ in 0..6 {
// Move to 5th element in the list, 3 items down.
for _ in 0..2 {
outline_panel.select_next(&SelectNext, cx);
}
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
});
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry()
),
@ -4870,6 +4888,7 @@ mod tests {
}
fn display_entries(
multi_buffer_snapshot: &MultiBufferSnapshot,
cached_entries: &[CachedEntry],
selected_entry: Option<&PanelEntry>,
) -> String {
@ -4909,8 +4928,17 @@ mod tests {
OutlineEntry::Excerpt(_, _, _) => continue,
OutlineEntry::Outline(_, _, outline) => format!("outline: {}", outline.text),
},
PanelEntry::Search(SearchEntry { render_data, .. }) => {
format!("search: {}", render_data.context_text)
PanelEntry::Search(SearchEntry {
render_data,
match_range,
..
}) => {
format!(
"search: {}",
render_data
.get_or_init(|| SearchData::new(match_range, &multi_buffer_snapshot))
.context_text
)
}
};
@ -5153,4 +5181,14 @@ mod tests {
)
.unwrap()
}
fn snapshot(outline_panel: &OutlinePanel, cx: &AppContext) -> MultiBufferSnapshot {
outline_panel
.active_editor()
.unwrap()
.read(cx)
.buffer()
.read(cx)
.snapshot(cx)
}
}