@@ -7,11 +7,12 @@ use crate::{
9 document::{render_document, LineDecoration, LinePos, TextRenderer},
9 document::{render_document, LineDecoration, LinePos, TextRenderer},
10− fuzzy_match::FuzzyQuery,
11 EditorView,
10 EditorView,
14use futures_util::{future::BoxFuture, FutureExt};
13use futures_util::{future::BoxFuture, FutureExt};
14+use nucleo::pattern::CaseMatching;
15+use nucleo::{Config, Nucleo, Utf32String};
16 buffer::Buffer as Surface,
17 buffer::Buffer as Surface,
17 layout::Constraint,
18 layout::Constraint,
@@ -19,16 +20,23 @@ use tui::{
19 widgets::{Block, BorderType, Borders, Cell, Table},
20 widgets::{Block, BorderType, Borders, Cell, Table},
22−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
23use tui::widgets::Widget;
23use tui::widgets::Widget;
25−use std::cmp::{self, Ordering};
25+use std::{
26−use std::{collections::HashMap, io::Read, path::PathBuf};
26+ collections::HashMap,
30+ atomic::{self, AtomicBool},
28use crate::ui::{Prompt, PromptEvent};
35use crate::ui::{Prompt, PromptEvent};
29use helix_core::{
36use helix_core::{
30− char_idx_at_visual_offset, movement::Direction, text_annotations::TextAnnotations,
37+ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
31− unicode::segmentation::UnicodeSegmentation, Position, Syntax,
38+ text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
33use helix_view::{
41use helix_view::{
34 editor::Action,
42 editor::Action,
@@ -114,20 +122,71 @@ impl Preview<'_, '_> {
125+fn item_to_nucleo<T: Item>(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> {
126+ let row = item.format(editor_data);
127+ let mut cells = row.cells.iter();
128+ let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum());
129+ let cell = cells.next()?;
130+ if let Some(cell) = cell.content.lines.first() {
131+ for span in &cell.0 {
132+ text.push_str(&span.content);
138+ if let Some(cell) = cell.content.lines.first() {
139+ for span in &cell.0 {
140+ text.push_str(&span.content);
144+ Some((item, text.into()))
147+pub struct Injector<T: Item> {
148+ dst: nucleo::Injector<T>,
149+ editor_data: Arc<T::Data>,
150+ shutown: Arc<AtomicBool>,
153+impl<T: Item> Clone for Injector<T> {
154+ fn clone(&self) -> Self {
156+ dst: self.dst.clone(),
157+ editor_data: self.editor_data.clone(),
158+ shutown: Arc::new(AtomicBool::new(false)),
163+pub struct InjectorShutdown;
165+impl<T: Item> Injector<T> {
166+ pub fn push(&self, item: T) -> Result<(), InjectorShutdown> {
167+ if self.shutown.load(atomic::Ordering::Relaxed) {
168+ return Err(InjectorShutdown);
171+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
172+ self.dst.push(item, |dst| dst[0] = matcher_text);
117pub struct Picker<T: Item> {
178pub struct Picker<T: Item> {
119− editor_data: T::Data,
179+ editor_data: Arc<T::Data>,
120− // filter: String,
180+ shutdown: Arc<AtomicBool>,
121− matcher: Box<Matcher>,
181+ matcher: Nucleo<T>,
122− matches: Vec<PickerMatch>,
124 /// Current height of the completions box
183 /// Current height of the completions box
125 completion_height: u16,
184 completion_height: u16,
127− cursor: usize,
186+ cursor: u32,
129 prompt: Prompt,
187 prompt: Prompt,
130− previous_pattern: (String, FuzzyQuery),
188+ previous_pattern: String,
131 /// Whether to show the preview panel (default true)
190 /// Whether to show the preview panel (default true)
132 show_preview: bool,
191 show_preview: bool,
133 /// Constraints for tabular formatting
192 /// Constraints for tabular formatting
@@ -144,10 +203,59 @@ pub struct Picker<T: Item> {
146impl<T: Item + 'static> Picker<T> {
205impl<T: Item + 'static> Picker<T> {
206+ pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
207+ let matcher = Nucleo::new(
209+ Arc::new(helix_event::request_redraw),
213+ let streamer = Injector {
214+ dst: matcher.injector(),
215+ editor_data: Arc::new(editor_data),
216+ shutown: Arc::new(AtomicBool::new(false)),
147 pub fn new(
221 pub fn new(
148 options: Vec<T>,
222 options: Vec<T>,
149 editor_data: T::Data,
223 editor_data: T::Data,
150 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
224 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
226+ let matcher = Nucleo::new(
228+ Arc::new(helix_event::request_redraw),
232+ let injector = matcher.injector();
233+ for item in options {
234+ if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) {
235+ injector.push(item, |dst| dst[0] = matcher_text);
240+ Arc::new(editor_data),
241+ Arc::new(AtomicBool::new(false)),
248+ injector: Injector<T>,
249+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
251+ Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
256+ editor_data: Arc<T::Data>,
257+ shutdown: Arc<AtomicBool>,
258+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
151 ) -> Self {
259 ) -> Self {
152 let prompt = Prompt::new(
260 let prompt = Prompt::new(
153 "".into(),
261 "".into(),
@@ -156,14 +264,13 @@ impl<T: Item + 'static> Picker<T> {
156 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
264 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
159− let mut picker = Self {
267+ Self {
160− options,
268+ matcher,
161 editor_data,
269 editor_data,
162− matcher: Box::default(),
270+ shutdown,
163− matches: Vec::new(),
164 cursor: 0,
271 cursor: 0,
166− previous_pattern: (String::new(), FuzzyQuery::default()),
273+ previous_pattern: String::new(),
167 truncate_start: true,
274 truncate_start: true,
168 show_preview: true,
275 show_preview: true,
169 callback_fn: Box::new(callback_fn),
276 callback_fn: Box::new(callback_fn),
@@ -172,24 +279,15 @@ impl<T: Item + 'static> Picker<T> {
172 preview_cache: HashMap::new(),
279 preview_cache: HashMap::new(),
173 read_buffer: Vec::with_capacity(1024),
280 read_buffer: Vec::with_capacity(1024),
174 file_fn: None,
281 file_fn: None,
177− picker.calculate_column_widths();
179− // scoring on empty input
180− // TODO: just reuse score()
183− .extend(picker.options.iter().enumerate().map(|(index, option)| {
184− let text = option.filter_text(&picker.editor_data);
188− len: text.chars().count(),
192− picker
285+ pub fn injector(&self) -> Injector<T> {
287+ dst: self.matcher.injector(),
288+ editor_data: self.editor_data.clone(),
289+ shutown: self.shutdown.clone(),
195 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
293 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -202,122 +300,25 @@ impl<T: Item + 'static> Picker<T> {
202 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
300 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
203 ) -> Self {
301 ) -> Self {
204 self.file_fn = Some(Box::new(preview_fn));
302 self.file_fn = Some(Box::new(preview_fn));
303+ // assumption: if we have a preview we are matching paths... If this is ever
304+ // not true this could be a separate builder function
305+ self.matcher.update_config(Config::DEFAULT.match_paths());
208 pub fn set_options(&mut self, new_options: Vec<T>) {
309 pub fn set_options(&mut self, new_options: Vec<T>) {
310+ self.matcher.restart(false);
311+ let injector = self.matcher.injector();
209− self.options = new_options;
312+ for item in new_options {
210− self.cursor = 0;
313+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
211− self.force_score();
314+ injector.push(item, |dst| dst[0] = matcher_text);
212− self.calculate_column_widths();
215− /// Calculate the width constraints using the maximum widths of each column
216− /// for the current options.
217− fn calculate_column_widths(&mut self) {
221− .map(|option| option.format(&self.editor_data).cells.len())
222− .unwrap_or_default();
223− let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
224− let row = option.format(&self.editor_data);
225− // maintain max for each column
226− for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
227− let width = cell.content.width();
234− self.widths = max_lens
236− .map(|len| Constraint::Length(len as u16))
240− pub fn score(&mut self) {
241− let pattern = self.prompt.line();
243− if pattern == &self.previous_pattern.0 {
247− let (query, is_refined) = self
250− .refine(pattern, &self.previous_pattern.0);
252− if pattern.is_empty() {
253− // Fast path for no pattern.
254− self.matches.clear();
256− .extend(self.options.iter().enumerate().map(|(index, option)| {
257− let text = option.filter_text(&self.editor_data);
261− len: text.chars().count(),
264− } else if is_refined {
265− // optimization: if the pattern is a more specific version of the previous one
266− // then we can score the filtered set.
267− self.matches.retain_mut(|pmatch| {
268− let option = &self.options[pmatch.index];
269− let text = option.sort_text(&self.editor_data);
271− match query.fuzzy_match(&text, &self.matcher) {
281− self.matches.sort_unstable();
286− // reset cursor position
288− let pattern = self.prompt.line();
289− self.previous_pattern.0.clone_from(pattern);
290− self.previous_pattern.1 = query;
293− pub fn force_score(&mut self) {
294− let pattern = self.prompt.line();
296− let query = FuzzyQuery::new(pattern);
297− self.matches.clear();
298− self.matches.extend(
302− .filter_map(|(index, option)| {
303− let text = option.filter_text(&self.editor_data);
306− .fuzzy_match(&text, &self.matcher)
307− .map(|score| PickerMatch {
310− len: text.chars().count(),
315− self.matches.sort_unstable();
318 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
319 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
319− pub fn move_by(&mut self, amount: usize, direction: Direction) {
320+ pub fn move_by(&mut self, amount: u32, direction: Direction) {
320− let len = self.matches.len();
321+ let len = self.matcher.snapshot().matched_item_count();
322 if len == 0 {
323 if len == 0 {
323 // No results, can't move.
324 // No results, can't move.
@@ -336,12 +337,12 @@ impl<T: Item + 'static> Picker<T> {
337 /// Move the cursor down by exactly one page. After the last page comes the first page.
338 /// Move the cursor down by exactly one page. After the last page comes the first page.
338 pub fn page_up(&mut self) {
339 pub fn page_up(&mut self) {
339− self.move_by(self.completion_height as usize, Direction::Backward);
340+ self.move_by(self.completion_height as u32, Direction::Backward);
342 /// Move the cursor up by exactly one page. After the first page comes the last page.
343 /// Move the cursor up by exactly one page. After the first page comes the last page.
343 pub fn page_down(&mut self) {
344 pub fn page_down(&mut self) {
344− self.move_by(self.completion_height as usize, Direction::Forward);
345+ self.move_by(self.completion_height as u32, Direction::Forward);
347 /// Move the cursor to the first entry
348 /// Move the cursor to the first entry
@@ -351,13 +352,18 @@ impl<T: Item + 'static> Picker<T> {
352 /// Move the cursor to the last entry
353 /// Move the cursor to the last entry
353 pub fn to_end(&mut self) {
354 pub fn to_end(&mut self) {
358+ .matched_item_count()
354− self.cursor = self.matches.len().saturating_sub(1);
359+ .saturating_sub(1);
357 pub fn selection(&self) -> Option<&T> {
362 pub fn selection(&self) -> Option<&T> {
358− self.matches
363+ self.matcher
359− .get(self.cursor)
365+ .get_matched_item(self.cursor)
360− .map(|pmatch| &self.options[pmatch.index])
366+ .map(|item| item.data)
363 pub fn toggle_preview(&mut self) {
369 pub fn toggle_preview(&mut self) {
@@ -366,8 +372,17 @@ impl<T: Item + 'static> Picker<T> {
367 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
373 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
368 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
374 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
375+ let pattern = self.prompt.line();
369− // TODO: recalculate only if pattern changed
376+ // TODO: better track how the pattern has changed
370− self.score();
377+ if pattern != &self.previous_pattern {
378+ self.matcher.pattern.reparse(
381+ CaseMatching::Smart,
382+ pattern.starts_with(&self.previous_pattern),
384+ self.previous_pattern = pattern.clone();
372 EventResult::Consumed(None)
387 EventResult::Consumed(None)
@@ -411,12 +426,9 @@ impl<T: Item + 'static> Picker<T> {
411 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
426 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
412 CachedPreview::LargeFile
427 CachedPreview::LargeFile
415− // TODO: enable syntax highlighting; blocked by async rendering
416− Document::open(path, None, None, editor.config.clone())
429+ _ => Document::open(path, None, None, editor.config.clone())
417− .map(|doc| CachedPreview::Document(Box::new(doc)))
430+ .map(|doc| CachedPreview::Document(Box::new(doc)))
418− .unwrap_or(CachedPreview::NotFound)
431+ .unwrap_or(CachedPreview::NotFound),
422 .unwrap_or(CachedPreview::NotFound);
434 .unwrap_or(CachedPreview::NotFound);
@@ -495,6 +507,14 @@ impl<T: Item + 'static> Picker<T> {
497 fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
509 fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
510+ let status = self.matcher.tick(10);
511+ let snapshot = self.matcher.snapshot();
515+ .min(snapshot.matched_item_count().saturating_sub(1))
498 let text_style = cx.editor.theme.get("ui.text");
518 let text_style = cx.editor.theme.get("ui.text");
499 let selected = cx.editor.theme.get("ui.text.focus");
519 let selected = cx.editor.theme.get("ui.text.focus");
500 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
520 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -515,8 +535,15 @@ impl<T: Item + 'static> Picker<T> {
515 // -- Render the input bar:
535 // -- Render the input bar:
517 let area = inner.clip_left(1).with_height(1);
537 let area = inner.clip_left(1).with_height(1);
538+ // render the prompt first since it will clear its background
539+ self.prompt.render(area, surface, cx);
519− let count = format!("{}/{}", self.matches.len(), self.options.len());
541+ let count = format!(
543+ if status.running { "(running) " } else { "" },
544+ snapshot.matched_item_count(),
545+ snapshot.item_count(),
520 surface.set_stringn(
547 surface.set_stringn(
521 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
548 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
@@ -525,8 +552,6 @@ impl<T: Item + 'static> Picker<T> {
525 text_style,
552 text_style,
528− self.prompt.render(area, surface, cx);
530 // -- Separator
555 // -- Separator
531 let sep_style = cx.editor.theme.get("ui.background.separator");
556 let sep_style = cx.editor.theme.get("ui.background.separator");
532 let borders = BorderType::line_symbols(BorderType::Plain);
557 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -539,106 +564,89 @@ impl<T: Item + 'static> Picker<T> {
539 // -- Render the contents:
564 // -- Render the contents:
540 // subtract area of prompt from top
565 // subtract area of prompt from top
541 let inner = inner.clip_top(2);
566 let inner = inner.clip_top(2);
543− let rows = inner.height;
567+ let rows = inner.height as u32;
544− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
568+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
545 let cursor = self.cursor.saturating_sub(offset);
569 let cursor = self.cursor.saturating_sub(offset);
571+ .saturating_add(rows)
572+ .min(snapshot.matched_item_count());
573+ let mut indices = Vec::new();
574+ let mut matcher = MATCHER.lock();
575+ matcher.config = Config::DEFAULT;
576+ if self.file_fn.is_some() {
577+ matcher.config.set_match_paths()
547− let options = self
580+ let options = snapshot.matched_items(offset..end).map(|item| {
548− .matches
581+ snapshot.pattern().column_pattern(0).indices(
549− .iter()
582+ item.matcher_columns[0].slice(..),
550− .skip(offset)
583+ &mut matcher,
551− .take(rows as usize)
584+ &mut indices,
552− .map(|pmatch| &self.options[pmatch.index])
585+ );
586+ indices.sort_unstable();
553− .map(|option| option.format(&self.editor_data))
588+ let mut row = item.data.format(&self.editor_data);
554− .map(|mut row| {
589+
555− const TEMP_CELL_SEP: &str = " ";
590+ let mut grapheme_idx = 0u32;
556−
591+ let mut indices = indices.drain(..);
557− let line = row.cell_text().fold(String::new(), |mut s, frag| {
592+ let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
558− s.push_str(&frag);
593+ if self.widths.len() < row.cells.len() {
559− s.push_str(TEMP_CELL_SEP);
594+ self.widths.resize(row.cells.len(), Constraint::Length(0));
561− });
596+ let mut widths = self.widths.iter_mut();
562−
597+ for cell in &mut row.cells {
563− // Items are filtered by using the text returned by menu::Item::filter_text
598+ let Some(Constraint::Length(max_width)) = widths.next() else {
564− // but we do highlighting here using the text in Row and therefore there
599+ unreachable!();
565− // might be inconsistencies. This is the best we can do since only the
566− // text in Row is displayed to the end user.
567− let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
568− .fuzzy_indices(&line, &self.matcher)
569− .unwrap_or_default();
571− let highlight_byte_ranges: Vec<_> = line
574− .filter_map(|(char_idx, (byte_offset, ch))| {
576− .contains(&char_idx)
577− .then(|| byte_offset..byte_offset + ch.len_utf8())
581− // The starting byte index of the current (iterating) cell
582− let mut cell_start_byte_offset = 0;
583− for cell in row.cells.iter_mut() {
584− let spans = match cell.content.lines.get(0) {
587− cell_start_byte_offset += TEMP_CELL_SEP.len();
602+ // merge index highlights on top of existing hightlights
603+ let mut span_list = Vec::new();
604+ let mut current_span = String::new();
605+ let mut current_style = Style::default();
592− let mut cell_len = 0;
606+ let mut width = 0;
594− let graphemes_with_style: Vec<_> = spans
608+ let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
595− .0
609+ for span in spans {
596− .iter()
610+ // this looks like a bug on first glance, we are iterating
597− .flat_map(|span| {
611+ // graphemes but treating them as char indices. The reason that
612+ // this is correct is that nucleo will only ever consider the first char
613+ // of a grapheme (and discard the rest of the grapheme) so the indices
614+ // returned by nucleo are essentially grapheme indecies
598− span.content
615+ for grapheme in span.content.graphemes(true) {
599− .grapheme_indices(true)
616+ let style = if grapheme_idx == next_highlight_idx {
600− .zip(std::iter::repeat(span.style))
617+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
602− .map(|((grapheme_byte_offset, grapheme), style)| {
603− cell_len += grapheme.len();
604− let start = cell_start_byte_offset;
606− let grapheme_byte_range =
607− grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
609− if highlight_byte_ranges.iter().any(|hl_rng| {
610− hl_rng.start >= start + grapheme_byte_range.start
611− && hl_rng.end <= start + grapheme_byte_range.end
613− (grapheme, style.patch(highlight_style))
618+ span.style.patch(highlight_style)
620− let mut span_list: Vec<(String, Style)> = Vec::new();
621− for (grapheme, style) in graphemes_with_style {
622− if span_list.last().map(|(_, sty)| sty) == Some(&style) {
623− let (string, _) = span_list.last_mut().unwrap();
624− string.push_str(grapheme);
626− span_list.push((String::from(grapheme), style))
620+ span.style
622+ if style != current_style {
623+ if !current_span.is_empty() {
624+ span_list.push(Span::styled(current_span, current_style))
626+ current_span = String::new();
627+ current_style = style;
629+ current_span.push_str(grapheme);
632+ width += span.width();
630− let spans: Vec<Span> = span_list
635+ span_list.push(Span::styled(current_span, current_style));
631− .into_iter()
636+ if width as u16 > *max_width {
632− .map(|(string, style)| Span::styled(string, style))
637+ *max_width = width as u16;
634− let spans: Spans = spans.into();
635− *cell = Cell::from(spans);
639+ *cell = Cell::from(Spans::from(span_list));
637− cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
641+ // spacer
642+ if grapheme_idx == next_highlight_idx {
643+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
643 let table = Table::new(options)
651 let table = Table::new(options)
644 .style(text_style)
652 .style(text_style)
@@ -654,7 +662,7 @@ impl<T: Item + 'static> Picker<T> {
655 &mut TableState {
663 &mut TableState {
656 offset: 0,
664 offset: 0,
657− selected: Some(cursor),
665+ selected: Some(cursor as usize),
659 self.truncate_start,
667 self.truncate_start,
@@ -755,7 +763,7 @@ impl<T: Item + 'static> Picker<T> {
758−impl<T: Item + 'static> Component for Picker<T> {
766+impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
759 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
767 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
760 // +---------+ +---------+
768 // +---------+ +---------+
761 // |prompt | |preview |
769 // |prompt | |preview |
@@ -875,29 +883,10 @@ impl<T: Item + 'static> Component for Picker<T> {
875 Some((width, height))
883 Some((width, height))
878−
886+impl<T: Item> Drop for Picker<T> {
879−#[derive(PartialEq, Eq, Debug)]
887− fn key(&self) -> impl Ord {
887+ fn drop(&mut self) {
888− (cmp::Reverse(self.score), self.len, self.index)
888+ // ensure we cancel any ongoing background threads streaming into the picker
889− }
889+ self.shutdown.store(true, atomic::Ordering::Relaxed)
892−impl PartialOrd for PickerMatch {
893− fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
894− Some(self.cmp(other))
898−impl Ord for PickerMatch {
899− fn cmp(&self, other: &Self) -> Ordering {
900− self.key().cmp(&other.key())
@@ -910,13 +899,13 @@ pub type DynQueryCallback<T> =
911/// A picker that updates its contents via a callback whenever the
900/// A picker that updates its contents via a callback whenever the
912/// query string changes. Useful for live grep, workspace symbols, etc.
901/// query string changes. Useful for live grep, workspace symbols, etc.
913−pub struct DynamicPicker<T: ui::menu::Item + Send> {
902+pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
914 file_picker: Picker<T>,
903 file_picker: Picker<T>,
915 query_callback: DynQueryCallback<T>,
904 query_callback: DynQueryCallback<T>,
916 query: String,
905 query: String,
919−impl<T: ui::menu::Item + Send> DynamicPicker<T> {
908+impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
920 pub const ID: &'static str = "dynamic-picker";
909 pub const ID: &'static str = "dynamic-picker";
922 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
911 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
@@ -928,7 +917,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
931−impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
920+impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
932 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
921 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
933 self.file_picker.render(area, surface, cx);
922 self.file_picker.render(area, surface, cx);