nucleo_picker/
match_list.rs

1//! # The list of match candidates
2//!
3//! ## Layout rules
4//! ### Layout rules for vertical alignment
5//!
6//! The core layout rules (in decreasing order of priority) are as follows.
7//!
8//! 1. Respect the padding below and above, except when the cursor is near 0.
9//! 2. Render as much of the selection as possible.
10//! 3. When the screen size increases, render new elements with lower index, and then elemenets
11//!    with higher index.
12//! 4. When the screen size decreases, delete whitespace, and then delete elements with higher
13//!    index, and then elements with lower index.
14//! 5. Change the location of the cursor on the screen as little as possible.
15//!
16//! ### Layout rules for horizontal alignment of items
17//!
18//! 1. Multi-line items must have the same amount of scroll for each line.
19//! 2. Do not hide highlighted characters.
20//! 3. Prefer to make the scroll as small as possible.
21//!
22//! ## Module organization
23//! This module contains the core [`MatchList`] struct. See the various update methods:
24//!
25//! 1. [`MatchList::resize`] for screen size changes.
26//! 2. [`MatchList::update_items`] for item list changes.
27//! 3. [`MatchList::selection_incr`] if the selection increases.
28//! 4. [`MatchList::selection_decr`] if the selection decreases.
29//! 5. [`MatchList::reset`] to set the cursor to 0 and completely redraw the screen.
30//! 6. [`MatchList::reparse`] to change the prompt string.
31//! 7. [`MatchList::update`] to wait for any changes in the match engine.
32//!
33//! Instead of merging the various methods together, we individually maintain methods for the
34//! various changes for performance so that only the layout computations required for the relevant
35//! changes are performed. For instance, on most frame renders, item updates are the most common.
36//!
37//! The actual implementations of the various layout methods are contained in the relevant
38//! sub-modules.
39#[cfg(test)]
40mod tests;
41
42mod draw;
43mod item;
44mod layout;
45mod span;
46mod unicode;
47
48use std::{ops::Range, sync::Arc};
49
50use self::{
51    layout::{reset, resize, selection, update},
52    unicode::Span,
53};
54use crate::{Injector, Render, incremental::Incremental};
55
56use nucleo::{
57    self as nc,
58    pattern::{CaseMatching, Normalization},
59};
60
61/// An event that modifies the selection in the match list.
62#[derive(Debug, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum MatchListEvent {
65    /// Move the selection up `usize` items.
66    Up(usize),
67    /// Move the selection down `usize` items.
68    Down(usize),
69    /// Reset the selection to the start of the match list.
70    Reset,
71}
72
73/// A trait to describe items with a certain size.
74pub trait ItemSize {
75    /// The size of the item on the screen.
76    fn size(&self) -> usize;
77}
78
79/// A list of items with variable sizes.
80pub trait ItemList {
81    /// The item type of list.
82    type Item<'a>: ItemSize
83    where
84        Self: 'a;
85
86    /// The total number items in the list.
87    fn total(&self) -> u32;
88
89    /// An iterator over items below the cursor, iterating downwards.
90    fn lower(&self, cursor: u32) -> impl DoubleEndedIterator<Item = Self::Item<'_>>;
91
92    /// An iterator over items below and including the cursor, iterating downwards.
93    fn lower_inclusive(&self, cursor: u32) -> impl DoubleEndedIterator<Item = Self::Item<'_>>;
94
95    /// An iterator over items above cursor, iterating upwards.
96    fn higher(&self, cursor: u32) -> impl DoubleEndedIterator<Item = Self::Item<'_>>;
97
98    /// An iterator over items above and including the cursor, iterating upwards.
99    fn higher_inclusive(&self, selection: u32) -> impl DoubleEndedIterator<Item = Self::Item<'_>>;
100}
101
102/// An automatic extension trait for an [`ItemList`].
103trait ItemListExt: ItemList {
104    /// Wrap the item sizes returned by [`lower`](ItemList::lower)
105    /// into a [`Incremental`].
106    fn sizes_lower<'a>(
107        &self,
108        cursor: u32,
109        vec: &'a mut Vec<usize>,
110    ) -> Incremental<&'a mut Vec<usize>, impl Iterator<Item = usize>> {
111        vec.clear();
112        Incremental::new(vec, self.lower(cursor).map(|item| item.size()))
113    }
114
115    /// Wrap the item sizes returned by [`lower_inclusive`](ItemList::lower_inclusive)
116    /// into a [`Incremental`].
117    fn sizes_lower_inclusive<'a>(
118        &self,
119        cursor: u32,
120        vec: &'a mut Vec<usize>,
121    ) -> Incremental<&'a mut Vec<usize>, impl Iterator<Item = usize>> {
122        vec.clear();
123        Incremental::new(vec, self.lower_inclusive(cursor).map(|item| item.size()))
124    }
125
126    /// Wrap the item sizes returned by [`higher`](ItemList::higher)
127    /// into an [`Incremental`].
128    fn sizes_higher<'a>(
129        &self,
130        cursor: u32,
131        vec: &'a mut Vec<usize>,
132    ) -> Incremental<&'a mut Vec<usize>, impl Iterator<Item = usize>> {
133        vec.clear();
134        Incremental::new(vec, self.higher(cursor).map(|item| item.size()))
135    }
136
137    /// Wrap the item sizes returned by [`higher_inclusive`](ItemList::higher)
138    /// into an [`Incremental`].
139    fn sizes_higher_inclusive<'a>(
140        &self,
141        cursor: u32,
142        vec: &'a mut Vec<usize>,
143    ) -> Incremental<&'a mut Vec<usize>, impl Iterator<Item = usize>> {
144        vec.clear();
145        Incremental::new(vec, self.higher_inclusive(cursor).map(|item| item.size()))
146    }
147}
148
149impl<B: ItemList> ItemListExt for B {}
150
151/// Context from the previous render used to update the screen correctly.
152#[derive(Debug)]
153struct MatchListState {
154    selection: u32,
155    below: u16,
156    above: u16,
157    size: u16,
158}
159
160/// Configuration used internally in the [`PickerState`].
161#[derive(Debug, Clone)]
162#[non_exhaustive]
163pub struct MatchListConfig {
164    /// Whether or not to do match highlighting.
165    pub highlight: bool,
166    /// Whether or not the screen is reversed.
167    pub reversed: bool,
168    /// The amount of padding around highlighted matches.
169    pub highlight_padding: u16,
170    /// The amount of padding when scrolling.
171    pub scroll_padding: u16,
172    /// Case matching behaviour for matches.
173    pub case_matching: CaseMatching,
174    /// Normalization behaviour for matches.
175    pub normalization: Normalization,
176}
177
178impl Default for MatchListConfig {
179    fn default() -> Self {
180        Self {
181            highlight: true,
182            reversed: false,
183            highlight_padding: 3,
184            scroll_padding: 3,
185            case_matching: CaseMatching::default(),
186            normalization: Normalization::default(),
187        }
188    }
189}
190
191/// A buffer to hold match highlight information and cached line information in an underlying
192/// string slice.
193pub struct IndexBuffer {
194    /// Spans used to render items.
195    spans: Vec<Span>,
196    /// Sub-slices of `spans` corresponding to lines.
197    lines: Vec<Range<usize>>,
198    /// Indices generated from a match.
199    indices: Vec<u32>,
200}
201
202impl IndexBuffer {
203    /// Create a new buffer.
204    pub fn new() -> Self {
205        Self {
206            spans: Vec::with_capacity(16),
207            lines: Vec::with_capacity(4),
208            indices: Vec::with_capacity(16),
209        }
210    }
211}
212
213/// A component for representing the list of successful matches.
214///
215/// This component has two main parts: the internal [`nucleo::Nucleo`] match engine, as well as a
216/// stateful representation of the match items which are currently on the screen. See the module
217/// level documentation for more detail.
218pub struct MatchList<T: Send + Sync + 'static, R> {
219    /// The current selection; this corresponds to a valid index if and only if the current
220    /// snapshot has more than one element.
221    selection: u32,
222    /// The size of the screen last time the screen changed.
223    size: u16,
224    /// The layout buffer below and including the matched item.
225    below: Vec<usize>,
226    /// The layout buffer above the matched item.
227    above: Vec<usize>,
228    /// Configuration for drawing.
229    config: MatchListConfig,
230    /// The internal matcher engine.
231    nucleo: nc::Nucleo<T>,
232    /// Scratch space for index computations during rendering.
233    scratch: IndexBuffer,
234    /// The method which actually renders the items.
235    render: Arc<R>,
236    /// The internal matcher.
237    matcher: nc::Matcher,
238    /// A cache of the prompt, used to decide if the prompt has changed.
239    prompt: String,
240}
241
242impl<T: Send + Sync + 'static, R: Render<T>> MatchList<T, R> {
243    /// Initialize a new [`MatchList`] with the provided configuration and initial state.
244    pub fn new(
245        config: MatchListConfig,
246        nucleo_config: nc::Config,
247        nucleo: nc::Nucleo<T>,
248        render: Arc<R>,
249    ) -> Self {
250        Self {
251            size: 0,
252            selection: 0,
253            below: Vec::with_capacity(128),
254            above: Vec::with_capacity(128),
255            config,
256            nucleo,
257            matcher: nc::Matcher::new(nucleo_config),
258            render,
259            scratch: IndexBuffer::new(),
260            prompt: String::with_capacity(32),
261        }
262    }
263
264    pub fn reversed(&self) -> bool {
265        self.config.reversed
266    }
267
268    /// A convenience function to render a given item using the internal [`Render`] implementation.
269    pub fn render<'a>(&self, item: &'a T) -> <R as Render<T>>::Str<'a> {
270        self.render.render(item)
271    }
272
273    /// Replace the renderer with a new instance, immediately restarting the matcher engine.
274    pub fn reset_renderer(&mut self, render: R) {
275        self.restart();
276        self.render = render.into();
277    }
278
279    /// Get an [`Injector`] to add new match elements.
280    pub fn injector(&self) -> Injector<T, R> {
281        Injector::new(self.nucleo.injector(), self.render.clone())
282    }
283
284    /// Clear all of the items and restart the match engine.
285    pub fn restart(&mut self) {
286        self.nucleo.restart(true);
287        self.update_items();
288    }
289
290    /// Replace the internal [`nucleo`] configuration.
291    pub fn update_nucleo_config(&mut self, config: nc::Config) {
292        self.nucleo.update_config(config);
293    }
294
295    /// Returns a self-contained representation of the screen state required for correct layout
296    /// update computations.
297    fn state(&self) -> MatchListState {
298        let below = self.below.iter().sum::<usize>() as u16;
299        let above = self.above.iter().sum::<usize>() as u16;
300        MatchListState {
301            selection: self.selection,
302            below: self.size - above,
303            above: self.size - below,
304            size: self.size,
305        }
306    }
307
308    /// The total amount of whitespace present in the displayed match list.
309    fn whitespace(&self) -> u16 {
310        self.size
311            - self.below.iter().sum::<usize>() as u16
312            - self.above.iter().sum::<usize>() as u16
313    }
314
315    /// The amount of padding corresponding to the provided size.
316    pub fn padding(&self, size: u16) -> u16 {
317        self.config.scroll_padding.min(size.saturating_sub(1) / 2)
318    }
319
320    /// Replace the prompt string with an updated value.
321    pub fn reparse(&mut self, new: &str) {
322        // appending if the new value has the previous value as a prefix and also does not end in a
323        // trailing unescaped '\\'
324        let appending = match new.strip_prefix(&self.prompt) {
325            Some(rest) => {
326                if rest.is_empty() {
327                    // the strings are the same so we don't need to do anything
328                    return;
329                } else {
330                    // TODO: fixed in nucleo 0.5.1; remove when updated
331                    (self
332                        .prompt
333                        .bytes()
334                        .rev()
335                        .take_while(|ch| *ch == b'\\')
336                        .count()
337                        % 2)
338                        == 0
339                }
340            }
341            None => false,
342        };
343        self.nucleo.pattern.reparse(
344            0,
345            new,
346            self.config.case_matching,
347            self.config.normalization,
348            appending,
349        );
350        self.prompt = new.to_owned();
351    }
352
353    /// Whether or not the list of items is empty.
354    pub fn is_empty(&self) -> bool {
355        self.nucleo.snapshot().matched_item_count() == 0
356    }
357
358    pub fn selection(&self) -> u32 {
359        self.selection
360    }
361
362    pub fn max_selection(&self) -> u32 {
363        self.nucleo
364            .snapshot()
365            .matched_item_count()
366            .saturating_sub(1)
367    }
368
369    pub fn get_item(&self, n: u32) -> Option<nc::Item<'_, T>> {
370        self.nucleo.snapshot().get_matched_item(n)
371    }
372
373    /// Return the range corresponding to the matched items visible on the screen.
374    pub fn selection_range(&self) -> std::ops::RangeInclusive<u32> {
375        if self.config.reversed {
376            self.selection - self.above.len() as u32..=self.selection + self.below.len() as u32 - 1
377        } else {
378            self.selection + 1 - self.below.len() as u32..=self.selection + self.above.len() as u32
379        }
380    }
381
382    /// Recompute the match layout when the screen size has changed.
383    pub fn resize(&mut self, total_size: u16) {
384        // check for zero, so the 'clamp' call dows not fail
385        if total_size == 0 {
386            self.size = 0;
387            self.above.clear();
388            self.below.clear();
389            return;
390        }
391
392        let buffer = self.nucleo.snapshot();
393
394        // check for no elements, so the `sizes_below` and `sizes_above` calls do not fail
395        if buffer.total() == 0 {
396            self.size = total_size;
397            return;
398        }
399
400        let padding = self.padding(total_size);
401
402        let mut previous = self.state();
403
404        if self.config.reversed {
405            // since the padding could change, make sure the value of 'below' is valid for the new
406            // padding values
407            previous.below = previous.below.clamp(padding, total_size - padding - 1);
408
409            let sizes_below_incl = buffer.sizes_higher_inclusive(self.selection, &mut self.below);
410            let sizes_above = buffer.sizes_lower(self.selection, &mut self.above);
411
412            if self.size <= total_size {
413                resize::larger_rev(previous, total_size, padding, sizes_below_incl, sizes_above);
414            } else {
415                resize::smaller_rev(
416                    previous,
417                    total_size,
418                    padding,
419                    padding,
420                    sizes_below_incl,
421                    sizes_above,
422                );
423            }
424        } else {
425            // since the padding could change, make sure the value of 'above' is valid for the new
426            // padding values
427            previous.above = previous.above.clamp(padding, total_size - padding - 1);
428
429            let sizes_below_incl = buffer.sizes_lower_inclusive(self.selection, &mut self.below);
430            let sizes_above = buffer.sizes_higher(self.selection, &mut self.above);
431
432            if self.size <= total_size {
433                resize::larger(previous, total_size, sizes_below_incl, sizes_above);
434            } else {
435                resize::smaller(previous, total_size, padding, sizes_below_incl, sizes_above);
436            }
437        }
438
439        self.size = total_size;
440    }
441
442    /// Check if the internal match workers have returned any new updates for matched items.
443    pub fn update(&mut self, millis: u64) -> bool {
444        let status = self.nucleo.tick(millis);
445        if status.changed {
446            self.update_items();
447        }
448        status.changed
449    }
450
451    /// Reset the layout, setting the cursor to '0' and rendering the items.
452    pub fn reset(&mut self) -> bool {
453        let buffer = self.nucleo.snapshot();
454        let padding = self.padding(self.size);
455        if self.selection != 0 {
456            if self.config.reversed {
457                let sizes_below_incl = buffer.sizes_higher_inclusive(0, &mut self.below);
458                self.above.clear();
459
460                reset::reset_rev(self.size, sizes_below_incl);
461            } else {
462                let sizes_below_incl = buffer.sizes_lower_inclusive(0, &mut self.below);
463                let sizes_above = buffer.sizes_higher(0, &mut self.above);
464
465                reset::reset(self.size, padding, sizes_below_incl, sizes_above);
466            }
467
468            self.selection = 0;
469            true
470        } else {
471            false
472        }
473    }
474
475    /// Update the layout with the modified item list.
476    pub fn update_items(&mut self) {
477        let buffer = self.nucleo.snapshot();
478        // clamp the previous cursor in case it has become invalid for the updated items
479        self.selection = self.selection.min(buffer.total().saturating_sub(1));
480        let previous = self.state();
481        let padding = self.padding(self.size);
482
483        if buffer.total() > 0 {
484            if self.config.reversed {
485                let sizes_below_incl =
486                    buffer.sizes_higher_inclusive(self.selection, &mut self.below);
487                let sizes_above = buffer.sizes_lower(self.selection, &mut self.above);
488
489                update::items_rev(previous, padding, sizes_below_incl, sizes_above);
490            } else {
491                let sizes_below_incl =
492                    buffer.sizes_lower_inclusive(self.selection, &mut self.below);
493                let sizes_above = buffer.sizes_higher(self.selection, &mut self.above);
494
495                update::items(previous, padding, sizes_below_incl, sizes_above);
496            }
497        } else {
498            self.below.clear();
499            self.above.clear();
500            self.selection = 0;
501        }
502    }
503
504    #[inline]
505    pub fn set_selection(&mut self, new_selection: u32) -> bool {
506        let buffer = self.nucleo.snapshot();
507        let new_selection = new_selection.min(buffer.total().saturating_sub(1));
508
509        let previous = self.state();
510        let padding = self.padding(self.size);
511
512        if new_selection == 0 {
513            self.reset()
514        } else if new_selection > self.selection {
515            if self.config.reversed {
516                let sizes_below_incl =
517                    buffer.sizes_higher_inclusive(new_selection, &mut self.below);
518                let sizes_above = buffer.sizes_lower(new_selection, &mut self.above);
519
520                selection::incr_rev(
521                    previous,
522                    new_selection,
523                    padding,
524                    padding,
525                    sizes_below_incl,
526                    sizes_above,
527                );
528            } else {
529                let sizes_below_incl = buffer.sizes_lower_inclusive(new_selection, &mut self.below);
530                let sizes_above = buffer.sizes_higher(new_selection, &mut self.above);
531
532                selection::incr(
533                    previous,
534                    new_selection,
535                    padding,
536                    sizes_below_incl,
537                    sizes_above,
538                );
539            }
540
541            self.selection = new_selection;
542
543            true
544        } else if new_selection < self.selection {
545            if self.config.reversed {
546                let sizes_below_incl =
547                    buffer.sizes_higher_inclusive(new_selection, &mut self.below);
548                let sizes_above = buffer.sizes_lower(new_selection, &mut self.above);
549
550                selection::decr_rev(
551                    previous,
552                    new_selection,
553                    padding,
554                    sizes_below_incl,
555                    sizes_above,
556                );
557            } else {
558                let sizes_below_incl = buffer.sizes_lower_inclusive(new_selection, &mut self.below);
559                let sizes_above = buffer.sizes_higher(new_selection, &mut self.above);
560
561                selection::decr(
562                    previous,
563                    new_selection,
564                    padding,
565                    padding,
566                    sizes_below_incl,
567                    sizes_above,
568                );
569            }
570
571            self.selection = new_selection;
572
573            true
574        } else {
575            false
576        }
577    }
578
579    /// Increment the selection by the given amount.
580    pub fn selection_incr(&mut self, increase: u32) -> bool {
581        let new_selection = self
582            .selection
583            .saturating_add(increase)
584            .min(self.nucleo.snapshot().total().saturating_sub(1));
585
586        self.set_selection(new_selection)
587    }
588
589    /// Decrement the selection by the given amount.
590    pub fn selection_decr(&mut self, decrease: u32) -> bool {
591        let new_selection = self.selection.saturating_sub(decrease);
592
593        self.set_selection(new_selection)
594    }
595}