outline: Switch the outline search to use fuzzy_nucleo (#56477)

Nucleo tracking issue:
https://github.com/zed-industries/zed/issues/55872

This PR switches the outline picker to use `fuzzy_nucleo`. 

with that I refactored the outline fuzzy picking to take advantage of
nucleo and its multi-atom query support. The previous implementation had
a lot going on specifically to work around the lack of multiple atoms,
basically taking matters into its own hands to accomplish the same goals
within the constraints it had.

Instead of having two lists of candidates that we have to run the query
against, we just run a single query and take advantage of the fact that
nucleo chooses the matches that are the furtherest towards the end of
the haystack to implement the same ancestor/leaf filtering. It retains
the leaf only matching on single atom queries.

video:


https://github.com/user-attachments/assets/64baa8d7-fd77-452c-86d1-e08561422d85

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] 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:

- outline: switch to fuzzy_nucleo
This commit is contained in:
Finn Eitreim authored and GitHub committed 2026-06-08 14:41:04 +00:00
1 parent a20358c831
commit 5b71e46f2e
13 files changed
+219 -160

No files matched your search

Generated
+2 -3
View File
@@ -9588,7 +9588,7 @@ dependencies = [
"fs",
"futures 0.3.32",
"futures-lite 1.13.0",
"fuzzy",
"fuzzy_nucleo",
"globset",
"gpui",
"http_client",
@@ -12386,13 +12386,12 @@ version = "0.1.0"
dependencies = [
"editor",
"futures 0.3.32",
"fuzzy",
"fuzzy_nucleo",
"gpui",
"indoc",
"language",
"lsp",
"menu",
"ordered-float 2.10.1",
"picker",
"project",
"rope",
+1 -1
View File
@@ -324,7 +324,7 @@ impl ChannelView {
return;
};
let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx);
let link = channel.notes_link(closest_heading.map(|heading| heading.text.to_string()), cx);
cx.write_to_clipboard(ClipboardItem::new_string(link));
self.workspace
.update(cx, |workspace, cx| {
+1 -1
View File
@@ -284,7 +284,7 @@ fn highlights_from_buffer(
.collect::<String>();
let mut outline_text_highlights = Vec::new();
match search_text.find(outline_text) {
match search_text.find(outline_text.as_str()) {
Some(start_index) => {
let multibuffer_start = search_start_offset + MultiBufferOffset(start_index);
let multibuffer_end = multibuffer_start + MultiBufferOffset(outline_text.len());
+1 -1
View File
@@ -10690,7 +10690,7 @@ impl Editor {
};
breadcrumbs.extend(symbols.iter().map(|symbol| HighlightedText {
text: symbol.text.clone().into(),
text: symbol.text.clone(),
highlights: symbol.highlight_ranges.clone(),
}));
Some(breadcrumbs)
+3 -3
View File
@@ -24,8 +24,8 @@ pub struct StringMatchCandidate {
}
impl StringMatchCandidate {
pub fn new(id: usize, string: impl ToString) -> Self {
Self::from_shared(id, SharedString::new(string.to_string()))
pub fn new(id: usize, string: impl Into<SharedString>) -> Self {
Self::from_shared(id, string.into())
}
pub fn from_shared(id: usize, string: SharedString) -> Self {
@@ -298,7 +298,7 @@ mod tests {
strings
.iter()
.enumerate()
.map(|(id, s)| StringMatchCandidate::new(id, s))
.map(|(id, s)| StringMatchCandidate::new(id, *s))
.collect()
}
+1 -1
View File
@@ -36,7 +36,7 @@ encoding_rs.workspace = true
fs.workspace = true
futures.workspace = true
futures-lite.workspace = true
fuzzy.workspace = true
fuzzy_nucleo.workspace = true
globset.workspace = true
gpui.workspace = true
http_client.workspace = true
+1 -1
View File
@@ -4600,7 +4600,7 @@ impl BufferSnapshot {
depth: 0, // We'll calculate the depth later
range: item_point_range,
source_range_for_text: source_range_for_text.to_point(self),
text,
text: text.into(),
highlight_ranges,
name_ranges,
body_range: open_point.zip(close_point).map(|(start, end)| start..end),
+23 -16
View File
@@ -850,23 +850,26 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
]
);
// Without space, we only match on names
// Single-atom queries (no whitespace): all matched chars must land in the leaf,
// so items whose ancestor path coincidentally contains the query chars don't
// show up unless the leaf itself matches.
assert_eq!(
search(&outline, "oon", cx).await,
&[
("mod module", vec![]), // included as the parent of a match
("enum LoginState", vec![]), // included as the parent of a match
("LoggingOn", vec![1, 7, 8]), // matches
("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
("mod module", vec![]), // parent context for LoggingOn
("enum LoginState", vec![]), // parent context for LoggingOn
("LoggingOn", vec![1, 7, 8]), // all three chars in leaf
("impl Eq for Person", vec![9, 16, 17]), // o-o-n in "for Person"
("impl Drop for Person", vec![11, 18, 19]), // o-o-n in "for Person"
]
);
// Multi-atom queries: rows whose match lives entirely in an ancestor
// are kept as context (empty positions, score zeroed) so descendants
// of a matched container surface alongside it.
assert_eq!(
search(&outline, "dp p", cx).await,
&[
("impl Drop for Person", vec![5, 8, 9, 14]),
("fn drop", vec![]),
]
&[("impl Drop for Person", vec![5, 14]), ("fn drop", vec![]),]
);
assert_eq!(
search(&outline, "dpn", cx).await,
@@ -875,8 +878,8 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
assert_eq!(
search(&outline, "impl ", cx).await,
&[
("impl Eq for Person", vec![0, 1, 2, 3, 4]),
("impl Drop for Person", vec![0, 1, 2, 3, 4]),
("impl Eq for Person", vec![0, 1, 2, 3]),
("impl Drop for Person", vec![0, 1, 2, 3]),
("fn drop", vec![]),
]
);
@@ -891,12 +894,16 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
query: &'a str,
cx: &'a gpui::TestAppContext,
) -> Vec<(&'a str, Vec<usize>)> {
let matches = cx
let entries = cx
.update(|cx| outline.search(query, cx.background_executor().clone()))
.await;
matches
entries
.into_iter()
.map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions))
.map(|entry| {
let candidate_id = entry.candidate_id();
let positions = entry.into_match().map(|m| m.positions).unwrap_or_default();
(outline.items[candidate_id].text.as_str(), positions)
})
.collect::<Vec<_>>()
}
}
@@ -999,7 +1006,7 @@ fn test_outline_annotations(cx: &mut App) {
.items
.into_iter()
.map(|item| (
item.text,
item.text.to_string(),
item.depth,
item.annotation_range
.map(|range| { buffer.read(cx).text_for_range(range).collect::<String>() })
@@ -1099,7 +1106,7 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
.into_iter()
.map(|item| {
(
item.text,
item.text.to_string(),
item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
)
})
+153 -104
View File
@@ -1,15 +1,18 @@
use crate::{BufferSnapshot, Point, ToPoint, ToTreeSitterPoint};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{BackgroundExecutor, HighlightStyle};
use fuzzy_nucleo::{Case, LengthPenalty, StringMatch, StringMatchCandidate};
use gpui::{BackgroundExecutor, HighlightStyle, SharedString};
use std::ops::Range;
/// An outline of all the symbols contained in a buffer.
#[derive(Debug)]
pub struct Outline<T> {
pub items: Vec<OutlineItem<T>>,
/// Candidates contain the full path of each item, used for matching.
candidates: Vec<StringMatchCandidate>,
pub path_candidates: Vec<StringMatchCandidate>,
path_candidate_prefixes: Vec<usize>,
/// leaf_offsets stores the byte offset within that full path where the
/// item's own text starts. Anything before this offset is ancestor
/// path text used purely as match context.
leaf_offsets: Vec<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
@@ -17,7 +20,7 @@ pub struct OutlineItem<T> {
pub depth: usize,
pub range: Range<T>,
pub source_range_for_text: Range<T>,
pub text: String,
pub text: SharedString,
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
pub name_ranges: Vec<Range<usize>>,
pub body_range: Option<Range<T>>,
@@ -25,7 +28,42 @@ pub struct OutlineItem<T> {
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SymbolPath(pub String);
pub struct SymbolPath(pub SharedString);
/// Result of [`Outline::search`]. Real fuzzy matches are `Match`; `Ancestor`
/// rows are synthetic entries pointing at parent items, included so callers can
/// show the full path of each match (Even when the ancestor has been filtered
/// out due to not matching) but treat those synthetic ancestors differently
/// from an entry that actually matched (e.g. they are not eligible for
/// auto-selection).
#[derive(Clone, Debug)]
pub enum OutlineSearchEntry {
Match(StringMatch),
Ancestor { candidate_id: usize },
}
impl OutlineSearchEntry {
pub fn candidate_id(&self) -> usize {
match self {
Self::Match(m) => m.candidate_id,
Self::Ancestor { candidate_id } => *candidate_id,
}
}
pub fn as_match(&self) -> Option<&StringMatch> {
match self {
Self::Match(m) => Some(m),
Self::Ancestor { .. } => None,
}
}
pub fn into_match(self) -> Option<StringMatch> {
match self {
Self::Match(m) => Some(m),
Self::Ancestor { .. } => None,
}
}
}
impl<T: ToPoint> OutlineItem<T> {
/// Converts to an equivalent outline item, but with parameterized over Points.
@@ -108,9 +146,8 @@ impl<T: ToPoint> OutlineItem<T> {
impl<T> Outline<T> {
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
let mut candidates = Vec::new();
let mut path_candidates = Vec::new();
let mut path_candidate_prefixes = Vec::new();
let mut candidates = Vec::with_capacity(items.len());
let mut leaf_offsets = Vec::with_capacity(items.len());
let mut path_text = String::new();
let mut path_stack = Vec::new();
@@ -122,25 +159,16 @@ impl<T> Outline<T> {
if !path_text.is_empty() {
path_text.push(' ');
}
path_candidate_prefixes.push(path_text.len());
leaf_offsets.push(path_text.len());
path_text.push_str(&item.text);
path_stack.push(path_text.len());
let candidate_text = item
.name_ranges
.iter()
.map(|range| &item.text[range.start..range.end])
.collect::<String>();
path_candidates.push(StringMatchCandidate::new(id, &path_text));
candidates.push(StringMatchCandidate::new(id, &candidate_text));
candidates.push(StringMatchCandidate::new(id, &path_text));
}
Self {
candidates,
path_candidates,
path_candidate_prefixes,
items,
candidates,
leaf_offsets,
}
}
@@ -149,7 +177,7 @@ impl<T> Outline<T> {
const SIMILARITY_THRESHOLD: f64 = 0.6;
let (position, similarity) = self
.path_candidates
.candidates
.iter()
.enumerate()
.map(|(index, candidate)| {
@@ -159,7 +187,7 @@ impl<T> Outline<T> {
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())?;
if similarity >= SIMILARITY_THRESHOLD {
self.path_candidates
self.candidates
.get(position)
.map(|candidate| SymbolPath(candidate.string.clone()))
.zip(self.items.get(position))
@@ -168,91 +196,106 @@ impl<T> Outline<T> {
}
}
/// Find all outline symbols according to a longest subsequence match with the query, ordered descending by match score.
pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
/// Find all outline symbols that match with the nucleo fuzzy matcher, ordered by tree position.
/// Each real match is preceded by [`OutlineSearchEntry::Ancestor`] rows carrying parent
/// `candidate_id`s, so callers can render tree context above the match.
pub async fn search(
&self,
query: &str,
executor: BackgroundExecutor,
) -> Vec<OutlineSearchEntry> {
let query = query.trim_start();
let is_path_query = query.contains(' ');
let smart_case = query.chars().any(|c| c.is_uppercase());
let mut matches = fuzzy::match_strings(
if is_path_query {
&self.path_candidates
} else {
&self.candidates
},
if query.is_empty() {
return Vec::new();
}
let mut matches = fuzzy_nucleo::match_strings_async(
&self.candidates,
query,
smart_case,
true,
Case::Smart,
LengthPenalty::On,
100,
&Default::default(),
executor.clone(),
executor,
)
.await;
matches.sort_unstable_by_key(|m| m.candidate_id);
let mut tree_matches = Vec::new();
let mut prev_item_ix = 0;
for mut string_match in matches {
let outline_match = &self.items[string_match.candidate_id];
string_match.string.clone_from(&outline_match.text);
if is_path_query {
let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
string_match
.positions
.retain(|position| *position >= prefix_len);
for position in &mut string_match.positions {
*position -= prefix_len;
}
} else {
let mut name_ranges = outline_match.name_ranges.iter();
let Some(mut name_range) = name_ranges.next() else {
continue;
};
let mut preceding_ranges_len = 0;
for position in &mut string_match.positions {
while *position >= preceding_ranges_len + name_range.len() {
preceding_ranges_len += name_range.len();
name_range = name_ranges.next().unwrap();
}
*position = name_range.start + (*position - preceding_ranges_len);
}
// Single-atom queries (no whitespace) require *all* matched chars to
// land in the leaf — typing "drop" should only surface leaves that
// actually contain "drop", not items whose ancestor path happens to.
// We can rely on that behavior because nucleo prefers matches at the
// end of the haystack, so the leafiest part of the candidate.
//
// Multi-atom queries (whitespace-separated) use the ancestor path
// for scoping. Rows whose entire match landed in an ancestor are
// kept as context, with empty positions and zero score, so
// descendants of a matched container surface alongside it. The
// picker's score-based auto-select skips them so they never steal
// focus from a row with real highlights.
let single_atom = !query.contains(char::is_whitespace);
matches.retain_mut(|string_match| {
let leaf_offset = self.leaf_offsets[string_match.candidate_id];
let total = string_match.positions.len();
string_match
.positions
.retain(|position| *position >= leaf_offset);
let kept = string_match.positions.len();
if single_atom && kept != total {
return false;
}
let insertion_ix = tree_matches.len();
let mut cur_depth = outline_match.depth;
for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
.iter()
.enumerate()
.rev()
{
if cur_depth == 0 {
break;
}
let candidate_index = ix + prev_item_ix;
if item.depth == cur_depth - 1 {
tree_matches.insert(
insertion_ix,
StringMatch {
candidate_id: candidate_index,
score: Default::default(),
positions: Default::default(),
string: Default::default(),
},
);
cur_depth -= 1;
}
if kept == 0 {
string_match.score = 0.0;
}
for position in &mut string_match.positions {
*position -= leaf_offset;
}
string_match
.string
.clone_from(&self.items[string_match.candidate_id].text);
true
});
prev_item_ix = string_match.candidate_id + 1;
tree_matches.push(string_match);
}
tree_matches
expand_tree(|i| self.items[i].depth, matches)
}
}
/// Interleaves synthetic [`OutlineSearchEntry::Ancestor`] rows before each match so callers
/// can render the parent chain as tree context above the match.
///
/// `matches` must be sorted ascending by `candidate_id` (which is what
/// [`Outline::search`] produces), this is so that we preserve the tree
/// structure of the outline. `depth_at` returns the tree depth for the item at
/// a given candidate index. Ancestors that already appear earlier in the output
/// either as their own match or as an ancestor of an earlier match, are not
/// duplicated.
fn expand_tree(
depth_at: impl Fn(usize) -> usize,
matches: Vec<StringMatch>,
) -> Vec<OutlineSearchEntry> {
debug_assert!(matches.is_sorted_by_key(|m| m.candidate_id));
let mut out = Vec::with_capacity(matches.len());
let mut prev_item_ix = 0;
for string_match in matches {
let insertion_ix = out.len();
let mut cur_depth = depth_at(string_match.candidate_id);
for ix in (prev_item_ix..string_match.candidate_id).rev() {
if cur_depth == 0 {
break;
}
if depth_at(ix) == cur_depth - 1 {
out.insert(
insertion_ix,
OutlineSearchEntry::Ancestor { candidate_id: ix },
);
cur_depth -= 1;
}
}
prev_item_ix = string_match.candidate_id + 1;
out.push(OutlineSearchEntry::Match(string_match));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
@@ -273,7 +316,7 @@ mod tests {
depth: 0,
range: range.clone(),
source_range_for_text: range,
text: "completion".to_string(),
text: "completion".into(),
highlight_ranges: Vec::new(),
name_ranges: Vec::new(),
body_range: None,
@@ -290,7 +333,7 @@ mod tests {
depth: 0,
range: Point::new(0, 0)..Point::new(5, 0),
source_range_for_text: Point::new(0, 0)..Point::new(0, 9),
text: "class Foo".to_string(),
text: "class Foo".into(),
highlight_ranges: vec![],
name_ranges: vec![6..9],
body_range: None,
@@ -300,21 +343,27 @@ mod tests {
depth: 0,
range: Point::new(2, 0)..Point::new(2, 7),
source_range_for_text: Point::new(0, 0)..Point::new(0, 7),
text: "private".to_string(),
text: "private".into(),
highlight_ranges: vec![],
name_ranges: vec![],
body_range: None,
annotation_range: None,
},
]);
assert!(
outline.search("", cx.executor()).await.is_empty(),
"empty queries return no matches; the picker handles 'show all' itself",
);
assert_eq!(
outline
.search(" ", cx.executor())
.search("foo", cx.executor())
.await
.into_iter()
.map(|mat| mat.string)
.collect::<Vec<String>>(),
vec!["class Foo".to_string()]
.filter_map(OutlineSearchEntry::into_match)
.map(|m| m.string)
.collect::<Vec<SharedString>>(),
vec![SharedString::from("class Foo")],
"'private' (empty name_ranges) is correctly excluded; only the matching 'class Foo' is returned",
);
}
@@ -325,7 +374,7 @@ mod tests {
depth: 0,
range: Point::new(0, 0)..Point::new(5, 0),
source_range_for_text: Point::new(0, 0)..Point::new(0, 10),
text: "fn process".to_string(),
text: "fn process".into(),
highlight_ranges: vec![],
name_ranges: vec![3..10],
body_range: None,
@@ -335,7 +384,7 @@ mod tests {
depth: 0,
range: Point::new(7, 0)..Point::new(12, 0),
source_range_for_text: Point::new(0, 0)..Point::new(0, 20),
text: "struct DataProcessor".to_string(),
text: "struct DataProcessor".into(),
highlight_ranges: vec![],
name_ranges: vec![7..20],
body_range: None,
+1 -2
View File
@@ -14,10 +14,9 @@ doctest = false
[dependencies]
editor.workspace = true
fuzzy.workspace = true
fuzzy_nucleo.workspace = true
gpui.workspace = true
language.workspace = true
ordered-float.workspace = true
picker.workspace = true
settings.workspace = true
theme.workspace = true
+29 -24
View File
@@ -4,14 +4,13 @@ use std::{cmp, sync::Arc};
use editor::scroll::ScrollOffset;
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
use editor::{MultiBufferOffset, RowHighlightOptions, SelectionEffects};
use fuzzy::StringMatch;
use fuzzy_nucleo::StringMatch;
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, WeakEntity, Window, div,
rems,
};
use language::{Outline, OutlineItem};
use ordered_float::OrderedFloat;
use language::{Outline, OutlineItem, OutlineSearchEntry};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use theme::ActiveTheme;
@@ -192,7 +191,7 @@ struct OutlineViewDelegate {
outline: Arc<Outline<Anchor>>,
selected_match_index: usize,
prev_scroll_position: Option<Point<ScrollOffset>>,
matches: Vec<StringMatch>,
matches: Vec<OutlineSearchEntry>,
}
enum OutlineRowHighlights {}
@@ -239,7 +238,7 @@ impl OutlineViewDelegate {
self.selected_match_index = ix;
if navigate {
let outline_item = &self.outline.items[selected_match.candidate_id];
let outline_item = &self.outline.items[selected_match.candidate_id()];
self.active_editor.update(cx, |active_editor, cx| {
active_editor.clear_row_highlights::<OutlineRowHighlights>();
@@ -304,11 +303,13 @@ impl PickerDelegate for OutlineViewDelegate {
.items
.iter()
.enumerate()
.map(|(index, _)| StringMatch {
candidate_id: index,
score: Default::default(),
positions: Default::default(),
string: Default::default(),
.map(|(index, _)| {
OutlineSearchEntry::Match(StringMatch {
candidate_id: index,
score: Default::default(),
positions: Default::default(),
string: Default::default(),
})
})
.collect()
} else {
@@ -333,8 +334,8 @@ impl PickerDelegate for OutlineViewDelegate {
.matches
.iter()
.enumerate()
.filter_map(|(ix, m)| {
let item = &this.delegate.outline.items[m.candidate_id];
.filter_map(|(ix, entry)| {
let item = &this.delegate.outline.items[entry.candidate_id()];
let range = item.range.to_offset(&buffer);
range.contains(&cursor_offset).then_some((ix, item.depth))
})
@@ -346,11 +347,13 @@ impl PickerDelegate for OutlineViewDelegate {
.matches
.iter()
.enumerate()
.max_by(|(ix_a, a), (ix_b, b)| {
OrderedFloat(a.score)
.cmp(&OrderedFloat(b.score))
.then(ix_b.cmp(ix_a))
.filter_map(|(ix, entry)| {
entry
.as_match()
.filter(|m| !m.positions.is_empty())
.map(|m| (ix, m.score))
})
.max_by(|(ix_a, a), (ix_b, b)| a.total_cmp(b).then(ix_b.cmp(ix_a)))
.map(|(ix, _)| ix)
.unwrap_or(0)
};
@@ -403,8 +406,9 @@ impl PickerDelegate for OutlineViewDelegate {
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = self.matches.get(ix)?;
let outline_item = self.outline.items.get(mat.candidate_id)?;
let entry = self.matches.get(ix)?;
let outline_item = self.outline.items.get(entry.candidate_id())?;
let ranges = entry.as_match().into_iter().flat_map(|m| m.ranges());
Some(
ListItem::new(ix)
@@ -415,7 +419,7 @@ impl PickerDelegate for OutlineViewDelegate {
div()
.text_ui(cx)
.pl(rems(outline_item.depth as f32))
.child(render_item(outline_item, mat.ranges(), cx)),
.child(render_item(outline_item, ranges, cx)),
),
)
}
@@ -664,7 +668,7 @@ mod tests {
.update(cx, |outline_view, cx| {
let delegate = &outline_view.delegate;
let selected_candidate_id =
delegate.matches[delegate.selected_match_index].candidate_id;
delegate.matches[delegate.selected_match_index].candidate_id();
let (buffer, cursor_offset) = delegate.active_editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let cursor_offset = editor
@@ -704,7 +708,7 @@ mod tests {
cx.run_until_parked();
let selected_candidate_id = outline_view.read_with(cx, |outline_view, _| {
let delegate = &outline_view.delegate;
delegate.matches[delegate.selected_match_index].candidate_id
delegate.matches[delegate.selected_match_index].candidate_id()
});
assert_eq!(
selected_candidate_id, 0,
@@ -848,11 +852,12 @@ mod tests {
let scored_ids = delegate
.matches
.iter()
.filter_map(|entry| entry.as_match())
.filter(|m| m.score > 0.0)
.map(|m| m.candidate_id)
.collect::<Vec<_>>();
(
selected_match.candidate_id,
selected_match.candidate_id(),
*scored_ids.first().unwrap(),
*scored_ids.last().unwrap(),
scored_ids.len(),
@@ -934,14 +939,14 @@ mod tests {
fn outline_names(
outline_view: &Entity<Picker<OutlineViewDelegate>>,
cx: &mut VisualTestContext,
) -> Vec<String> {
) -> Vec<SharedString> {
outline_view.read_with(cx, |outline_view, _| {
let items = &outline_view.delegate.outline.items;
outline_view
.delegate
.matches
.iter()
.map(|hit| items[hit.candidate_id].text.clone())
.map(|hit| items[hit.candidate_id()].text.clone())
.collect::<Vec<_>>()
})
}
+2 -2
View File
@@ -2567,7 +2567,7 @@ impl OutlinePanel {
depth,
annotation_range: None,
range: search_data.context_range.clone(),
text: search_data.context_text.clone(),
text: search_data.context_text.clone().into(),
source_range_for_text: search_data.context_range.clone(),
highlight_ranges: search_data
.highlights_data
@@ -8121,7 +8121,7 @@ outline: struct Foo <==== selected
// Helper function to move the cursor to the first column of a given row
// and return the selected outline entry's text.
let move_cursor_and_get_selection =
|row: u32, cx: &mut VisualTestContext| -> Option<String> {
|row: u32, cx: &mut VisualTestContext| -> Option<SharedString> {
cx.update(|window, cx| {
editor.update(cx, |editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -271,7 +271,7 @@ fn flatten_document_symbols(
depth,
range,
source_range_for_text,
text,
text: text.into(),
highlight_ranges: Vec::new(),
name_ranges,
body_range: None,