@@ -7,11 +7,12 @@ use crate::{
77 ui::{
88 self,
99 document::{render_document, LineDecoration, LinePos, TextRenderer},
10− fuzzy_match::FuzzyQuery,
1110 EditorView,
1211 },
1312};
1413use futures_util::{future::BoxFuture, FutureExt};
14+use nucleo::pattern::CaseMatching;
15+use nucleo::{Config, Nucleo, Utf32String};
1516use tui::{
1617 buffer::Buffer as Surface,
1718 layout::Constraint,
@@ -19,16 +20,23 @@ use tui::{
1920 widgets::{Block, BorderType, Borders, Cell, Table},
2021};
2122
22−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2323use tui::widgets::Widget;
2424
25−use std::cmp::{self, Ordering};
26−use std::{collections::HashMap, io::Read, path::PathBuf};
25+use std::{
26+ collections::HashMap,
27+ io::Read,
28+ path::PathBuf,
29+ sync::{
30+ atomic::{self, AtomicBool},
31+ Arc,
32+ },
33+};
2734
2835use crate::ui::{Prompt, PromptEvent};
2936use helix_core::{
30− char_idx_at_visual_offset, movement::Direction, text_annotations::TextAnnotations,
31− unicode::segmentation::UnicodeSegmentation, Position, Syntax,
37+ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
38+ text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
39+ Syntax,
3240};
3341use helix_view::{
3442 editor::Action,
@@ -114,20 +122,71 @@ impl Preview<'_, '_> {
114122 }
115123}
116124
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);
133+ }
134+ }
135+
136+ for cell in cells {
137+ text.push(' ');
138+ if let Some(cell) = cell.content.lines.first() {
139+ for span in &cell.0 {
140+ text.push_str(&span.content);
141+ }
142+ }
143+ }
144+ Some((item, text.into()))
145+}
146+
147+pub struct Injector<T: Item> {
148+ dst: nucleo::Injector<T>,
149+ editor_data: Arc<T::Data>,
150+ shutown: Arc<AtomicBool>,
151+}
152+
153+impl<T: Item> Clone for Injector<T> {
154+ fn clone(&self) -> Self {
155+ Injector {
156+ dst: self.dst.clone(),
157+ editor_data: self.editor_data.clone(),
158+ shutown: Arc::new(AtomicBool::new(false)),
159+ }
160+ }
161+}
162+
163+pub struct InjectorShutdown;
164+
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);
169+ }
170+
171+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
172+ self.dst.push(item, |dst| dst[0] = matcher_text);
173+ }
174+ Ok(())
175+ }
176+}
177+
117178pub struct Picker<T: Item> {
118− options: Vec<T>,
119− editor_data: T::Data,
120− // filter: String,
121− matcher: Box<Matcher>,
122− matches: Vec<PickerMatch>,
179+ editor_data: Arc<T::Data>,
180+ shutdown: Arc<AtomicBool>,
181+ matcher: Nucleo<T>,
123182
124183 /// Current height of the completions box
125184 completion_height: u16,
126185
127− cursor: usize,
128− // pattern: String,
186+ cursor: u32,
129187 prompt: Prompt,
130− previous_pattern: (String, FuzzyQuery),
188+ previous_pattern: String,
189+
131190 /// Whether to show the preview panel (default true)
132191 show_preview: bool,
133192 /// Constraints for tabular formatting
@@ -144,10 +203,59 @@ pub struct Picker<T: Item> {
144203}
145204
146205impl<T: Item + 'static> Picker<T> {
206+ pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
207+ let matcher = Nucleo::new(
208+ Config::DEFAULT,
209+ Arc::new(helix_event::request_redraw),
210+ None,
211+ 1,
212+ );
213+ let streamer = Injector {
214+ dst: matcher.injector(),
215+ editor_data: Arc::new(editor_data),
216+ shutown: Arc::new(AtomicBool::new(false)),
217+ };
218+ (matcher, streamer)
219+ }
220+
147221 pub fn new(
148222 options: Vec<T>,
149223 editor_data: T::Data,
150224 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
225+ ) -> Self {
226+ let matcher = Nucleo::new(
227+ Config::DEFAULT,
228+ Arc::new(helix_event::request_redraw),
229+ None,
230+ 1,
231+ );
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);
236+ }
237+ }
238+ Self::with(
239+ matcher,
240+ Arc::new(editor_data),
241+ Arc::new(AtomicBool::new(false)),
242+ callback_fn,
243+ )
244+ }
245+
246+ pub fn with_stream(
247+ matcher: Nucleo<T>,
248+ injector: Injector<T>,
249+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
250+ ) -> Self {
251+ Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
252+ }
253+
254+ fn with(
255+ matcher: Nucleo<T>,
256+ editor_data: Arc<T::Data>,
257+ shutdown: Arc<AtomicBool>,
258+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
151259 ) -> Self {
152260 let prompt = Prompt::new(
153261 "".into(),
@@ -156,14 +264,13 @@ impl<T: Item + 'static> Picker<T> {
156264 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
157265 );
158266
159− let mut picker = Self {
160− options,
267+ Self {
268+ matcher,
161269 editor_data,
162− matcher: Box::default(),
163− matches: Vec::new(),
270+ shutdown,
164271 cursor: 0,
165272 prompt,
166− previous_pattern: (String::new(), FuzzyQuery::default()),
273+ previous_pattern: String::new(),
167274 truncate_start: true,
168275 show_preview: true,
169276 callback_fn: Box::new(callback_fn),
@@ -172,24 +279,15 @@ impl<T: Item + 'static> Picker<T> {
172279 preview_cache: HashMap::new(),
173280 read_buffer: Vec::with_capacity(1024),
174281 file_fn: None,
175− };
176−
177− picker.calculate_column_widths();
178−
179− // scoring on empty input
180− // TODO: just reuse score()
181− picker
182− .matches
183− .extend(picker.options.iter().enumerate().map(|(index, option)| {
184− let text = option.filter_text(&picker.editor_data);
185− PickerMatch {
186− index,
187− score: 0,
188− len: text.chars().count(),
189− }
190− }));
282+ }
283+ }
191284
192− picker
285+ pub fn injector(&self) -> Injector<T> {
286+ Injector {
287+ dst: self.matcher.injector(),
288+ editor_data: self.editor_data.clone(),
289+ shutown: self.shutdown.clone(),
290+ }
193291 }
194292
195293 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -202,122 +300,25 @@ impl<T: Item + 'static> Picker<T> {
202300 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
203301 ) -> Self {
204302 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());
205306 self
206307 }
207308
208309 pub fn set_options(&mut self, new_options: Vec<T>) {
209− self.options = new_options;
210− self.cursor = 0;
211− self.force_score();
212− self.calculate_column_widths();
213− }
214−
215− /// Calculate the width constraints using the maximum widths of each column
216− /// for the current options.
217− fn calculate_column_widths(&mut self) {
218− let n = self
219− .options
220− .first()
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();
228− if width > *acc {
229− *acc = width;
230− }
310+ self.matcher.restart(false);
311+ let injector = self.matcher.injector();
312+ for item in new_options {
313+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
314+ injector.push(item, |dst| dst[0] = matcher_text);
231315 }
232− acc
233− });
234− self.widths = max_lens
235− .into_iter()
236− .map(|len| Constraint::Length(len as u16))
237− .collect();
238− }
239−
240− pub fn score(&mut self) {
241− let pattern = self.prompt.line();
242−
243− if pattern == &self.previous_pattern.0 {
244− return;
245316 }
246−
247− let (query, is_refined) = self
248− .previous_pattern
249− .1
250− .refine(pattern, &self.previous_pattern.0);
251−
252− if pattern.is_empty() {
253− // Fast path for no pattern.
254− self.matches.clear();
255− self.matches
256− .extend(self.options.iter().enumerate().map(|(index, option)| {
257− let text = option.filter_text(&self.editor_data);
258− PickerMatch {
259− index,
260− score: 0,
261− len: text.chars().count(),
262− }
263− }));
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);
270−
271− match query.fuzzy_match(&text, &self.matcher) {
272− Some(s) => {
273− // Update the score
274− pmatch.score = s;
275− true
276− }
277− None => false,
278− }
279− });
280−
281− self.matches.sort_unstable();
282− } else {
283− self.force_score();
284− }
285−
286− // reset cursor position
287− self.cursor = 0;
288− let pattern = self.prompt.line();
289− self.previous_pattern.0.clone_from(pattern);
290− self.previous_pattern.1 = query;
291− }
292−
293− pub fn force_score(&mut self) {
294− let pattern = self.prompt.line();
295−
296− let query = FuzzyQuery::new(pattern);
297− self.matches.clear();
298− self.matches.extend(
299− self.options
300− .iter()
301− .enumerate()
302− .filter_map(|(index, option)| {
303− let text = option.filter_text(&self.editor_data);
304−
305− query
306− .fuzzy_match(&text, &self.matcher)
307− .map(|score| PickerMatch {
308− index,
309− score,
310− len: text.chars().count(),
311− })
312− }),
313− );
314−
315− self.matches.sort_unstable();
316317 }
317318
318319 /// 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− let len = self.matches.len();
320+ pub fn move_by(&mut self, amount: u32, direction: Direction) {
321+ let len = self.matcher.snapshot().matched_item_count();
321322
322323 if len == 0 {
323324 // No results, can't move.
@@ -336,12 +337,12 @@ impl<T: Item + 'static> Picker<T> {
336337
337338 /// Move the cursor down by exactly one page. After the last page comes the first page.
338339 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);
340341 }
341342
342343 /// Move the cursor up by exactly one page. After the first page comes the last page.
343344 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);
345346 }
346347
347348 /// Move the cursor to the first entry
@@ -351,13 +352,18 @@ impl<T: Item + 'static> Picker<T> {
351352
352353 /// Move the cursor to the last entry
353354 pub fn to_end(&mut self) {
354− self.cursor = self.matches.len().saturating_sub(1);
355+ self.cursor = self
356+ .matcher
357+ .snapshot()
358+ .matched_item_count()
359+ .saturating_sub(1);
355360 }
356361
357362 pub fn selection(&self) -> Option<&T> {
358− self.matches
359− .get(self.cursor)
360− .map(|pmatch| &self.options[pmatch.index])
363+ self.matcher
364+ .snapshot()
365+ .get_matched_item(self.cursor)
366+ .map(|item| item.data)
361367 }
362368
363369 pub fn toggle_preview(&mut self) {
@@ -366,8 +372,17 @@ impl<T: Item + 'static> Picker<T> {
366372
367373 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
368374 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
369− // TODO: recalculate only if pattern changed
370− self.score();
375+ let pattern = self.prompt.line();
376+ // TODO: better track how the pattern has changed
377+ if pattern != &self.previous_pattern {
378+ self.matcher.pattern.reparse(
379+ 0,
380+ pattern,
381+ CaseMatching::Smart,
382+ pattern.starts_with(&self.previous_pattern),
383+ );
384+ self.previous_pattern = pattern.clone();
385+ }
371386 }
372387 EventResult::Consumed(None)
373388 }
@@ -411,12 +426,9 @@ impl<T: Item + 'static> Picker<T> {
411426 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
412427 CachedPreview::LargeFile
413428 }
414− _ => {
415− // TODO: enable syntax highlighting; blocked by async rendering
416− Document::open(path, None, None, editor.config.clone())
417− .map(|doc| CachedPreview::Document(Box::new(doc)))
418− .unwrap_or(CachedPreview::NotFound)
419− }
429+ _ => Document::open(path, None, None, editor.config.clone())
430+ .map(|doc| CachedPreview::Document(Box::new(doc)))
431+ .unwrap_or(CachedPreview::NotFound),
420432 },
421433 )
422434 .unwrap_or(CachedPreview::NotFound);
@@ -495,6 +507,14 @@ impl<T: Item + 'static> Picker<T> {
495507 }
496508
497509 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();
512+ if status.changed {
513+ self.cursor = self
514+ .cursor
515+ .min(snapshot.matched_item_count().saturating_sub(1))
516+ }
517+
498518 let text_style = cx.editor.theme.get("ui.text");
499519 let selected = cx.editor.theme.get("ui.text.focus");
500520 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -515,8 +535,15 @@ impl<T: Item + 'static> Picker<T> {
515535 // -- Render the input bar:
516536
517537 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);
518540
519− let count = format!("{}/{}", self.matches.len(), self.options.len());
541+ let count = format!(
542+ "{}{}/{}",
543+ if status.running { "(running) " } else { "" },
544+ snapshot.matched_item_count(),
545+ snapshot.item_count(),
546+ );
520547 surface.set_stringn(
521548 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
522549 area.y,
@@ -525,8 +552,6 @@ impl<T: Item + 'static> Picker<T> {
525552 text_style,
526553 );
527554
528− self.prompt.render(area, surface, cx);
529−
530555 // -- Separator
531556 let sep_style = cx.editor.theme.get("ui.background.separator");
532557 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -539,106 +564,89 @@ impl<T: Item + 'static> Picker<T> {
539564 // -- Render the contents:
540565 // subtract area of prompt from top
541566 let inner = inner.clip_top(2);
542−
543− let rows = inner.height;
544− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
567+ let rows = inner.height as u32;
568+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
545569 let cursor = self.cursor.saturating_sub(offset);
570+ let end = 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()
578+ }
546579
547− let options = self
548− .matches
549− .iter()
550− .skip(offset)
551− .take(rows as usize)
552− .map(|pmatch| &self.options[pmatch.index])
553− .map(|option| option.format(&self.editor_data))
554− .map(|mut row| {
555− const TEMP_CELL_SEP: &str = " ";
556−
557− let line = row.cell_text().fold(String::new(), |mut s, frag| {
558− s.push_str(&frag);
559− s.push_str(TEMP_CELL_SEP);
560− s
561− });
562−
563− // Items are filtered by using the text returned by menu::Item::filter_text
564− // but we do highlighting here using the text in Row and therefore there
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();
570−
571− let highlight_byte_ranges: Vec<_> = line
572− .char_indices()
573− .enumerate()
574− .filter_map(|(char_idx, (byte_offset, ch))| {
575− highlights
576− .contains(&char_idx)
577− .then(|| byte_offset..byte_offset + ch.len_utf8())
578− })
579− .collect();
580−
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) {
585− Some(s) => s,
586− None => {
587− cell_start_byte_offset += TEMP_CELL_SEP.len();
588− continue;
589− }
590− };
580+ let options = snapshot.matched_items(offset..end).map(|item| {
581+ snapshot.pattern().column_pattern(0).indices(
582+ item.matcher_columns[0].slice(..),
583+ &mut matcher,
584+ &mut indices,
585+ );
586+ indices.sort_unstable();
587+ indices.dedup();
588+ let mut row = item.data.format(&self.editor_data);
589+
590+ let mut grapheme_idx = 0u32;
591+ let mut indices = indices.drain(..);
592+ let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
593+ if self.widths.len() < row.cells.len() {
594+ self.widths.resize(row.cells.len(), Constraint::Length(0));
595+ }
596+ let mut widths = self.widths.iter_mut();
597+ for cell in &mut row.cells {
598+ let Some(Constraint::Length(max_width)) = widths.next() else {
599+ unreachable!();
600+ };
591601
592− let mut cell_len = 0;
593−
594− let graphemes_with_style: Vec<_> = spans
595− .0
596− .iter()
597− .flat_map(|span| {
598− span.content
599− .grapheme_indices(true)
600− .zip(std::iter::repeat(span.style))
601− })
602− .map(|((grapheme_byte_offset, grapheme), style)| {
603− cell_len += grapheme.len();
604− let start = cell_start_byte_offset;
605−
606− let grapheme_byte_range =
607− grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
608−
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
612− }) {
613− (grapheme, style.patch(highlight_style))
614− } else {
615− (grapheme, style)
616− }
617− })
618− .collect();
619−
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);
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();
606+ let mut width = 0;
607+
608+ let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
609+ for span in spans {
610+ // this looks like a bug on first glance, we are iterating
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
615+ for grapheme in span.content.graphemes(true) {
616+ let style = if grapheme_idx == next_highlight_idx {
617+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
618+ span.style.patch(highlight_style)
625619 } else {
626− span_list.push((String::from(grapheme), style))
620+ span.style
621+ };
622+ if style != current_style {
623+ if !current_span.is_empty() {
624+ span_list.push(Span::styled(current_span, current_style))
625+ }
626+ current_span = String::new();
627+ current_style = style;
627628 }
629+ current_span.push_str(grapheme);
630+ grapheme_idx += 1;
628631 }
632+ width += span.width();
633+ }
629634
630− let spans: Vec<Span> = span_list
631− .into_iter()
632− .map(|(string, style)| Span::styled(string, style))
633− .collect();
634− let spans: Spans = spans.into();
635− *cell = Cell::from(spans);
635+ span_list.push(Span::styled(current_span, current_style));
636+ if width as u16 > *max_width {
637+ *max_width = width as u16;
638+ }
639+ *cell = Cell::from(Spans::from(span_list));
636640
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);
638644 }
645+ grapheme_idx += 1;
646+ }
639647
640− row
641− });
648+ row
649+ });
642650
643651 let table = Table::new(options)
644652 .style(text_style)
@@ -654,7 +662,7 @@ impl<T: Item + 'static> Picker<T> {
654662 surface,
655663 &mut TableState {
656664 offset: 0,
657− selected: Some(cursor),
665+ selected: Some(cursor as usize),
658666 },
659667 self.truncate_start,
660668 );
@@ -755,7 +763,7 @@ impl<T: Item + 'static> Picker<T> {
755763 }
756764}
757765
758−impl<T: Item + 'static> Component for Picker<T> {
766+impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
759767 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
760768 // +---------+ +---------+
761769 // |prompt | |preview |
@@ -875,29 +883,10 @@ impl<T: Item + 'static> Component for Picker<T> {
875883 Some((width, height))
876884 }
877885}
878−
879−#[derive(PartialEq, Eq, Debug)]
880−struct PickerMatch {
881− score: i64,
882− index: usize,
883− len: usize,
884−}
885−
886−impl PickerMatch {
887− fn key(&self) -> impl Ord {
888− (cmp::Reverse(self.score), self.len, self.index)
889− }
890−}
891−
892−impl PartialOrd for PickerMatch {
893− fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
894− Some(self.cmp(other))
895− }
896−}
897−
898−impl Ord for PickerMatch {
899− fn cmp(&self, other: &Self) -> Ordering {
900− self.key().cmp(&other.key())
886+impl<T: Item> Drop for Picker<T> {
887+ fn drop(&mut self) {
888+ // ensure we cancel any ongoing background threads streaming into the picker
889+ self.shutdown.store(true, atomic::Ordering::Relaxed)
901890 }
902891}
903892
@@ -910,13 +899,13 @@ pub type DynQueryCallback<T> =
910899
911900/// A picker that updates its contents via a callback whenever the
912901/// 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> {
914903 file_picker: Picker<T>,
915904 query_callback: DynQueryCallback<T>,
916905 query: String,
917906}
918907
919−impl<T: ui::menu::Item + Send> DynamicPicker<T> {
908+impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
920909 pub const ID: &'static str = "dynamic-picker";
921910
922911 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
@@ -928,7 +917,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
928917 }
929918}
930919
931−impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
920+impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
932921 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
933922 self.file_picker.render(area, surface, cx);
934923 }