matchmaker/nucleo/
query.rs

1// Original code from https://github.com/helix-editor/helix (MPL 2.0)
2// Modified by Squirreljetpack, 2025
3
4use std::{collections::HashMap, mem, ops::Range, sync::Arc};
5
6pub struct PickerQuery {
7    /// The column names of the picker.
8    column_names: Box<[Arc<str>]>,
9    /// The index of the primary column in `column_names`.
10    /// The primary column is selected by default unless another
11    /// field is specified explicitly with `%fieldname`.
12    primary_column: usize,
13    /// The mapping between column names and input in the query
14    /// for those columns.
15    inner: HashMap<Arc<str>, Arc<str>>,
16    /// The byte ranges of the input text which are used as input for each column.
17    /// This is calculated at parsing time for use in [Self::active_column].
18    /// This Vec is naturally sorted in ascending order and ranges do not overlap.
19    column_ranges: Vec<(Range<usize>, Option<Arc<str>>)>,
20
21    empty_column: bool,
22}
23
24impl PartialEq<HashMap<Arc<str>, Arc<str>>> for PickerQuery {
25    fn eq(&self, other: &HashMap<Arc<str>, Arc<str>>) -> bool {
26        self.inner.eq(other)
27    }
28}
29
30impl PickerQuery {
31    pub fn new<I: Iterator<Item = Arc<str>>>(column_names: I, primary_column: usize) -> Self {
32        let column_names: Box<[_]> = column_names.collect();
33        let inner = HashMap::with_capacity(column_names.len());
34        let column_ranges = vec![(0..usize::MAX, Some(column_names[primary_column].clone()))];
35        let empty_column = column_names.iter().any(|c| c.is_empty());
36
37        Self {
38            column_names,
39            primary_column,
40            inner,
41            column_ranges,
42            empty_column,
43        }
44    }
45
46    pub fn get(&self, column: &str) -> Option<&Arc<str>> {
47        self.inner.get(column)
48    }
49
50    pub fn parse(&mut self, input: &str) -> HashMap<Arc<str>, Arc<str>> {
51        let mut fields: HashMap<Arc<str>, String> = HashMap::new();
52        let primary_field = &self.column_names[self.primary_column];
53        let mut escaped = false;
54        let mut in_field = false;
55        let mut field = None;
56        let mut text = String::new();
57        self.column_ranges.clear();
58        self.column_ranges
59        .push((0..usize::MAX, Some(primary_field.clone())));
60
61        macro_rules! finish_field {
62            () => {
63                let key = field.take().unwrap_or(primary_field);
64
65                // Trims one space from the end, enabling leading and trailing
66                // spaces in search patterns, while also retaining spaces as separators
67                // between column filters.
68                let pat = text.strip_suffix(' ').unwrap_or(&text);
69
70                if let Some(pattern) = fields.get_mut(key) {
71                    pattern.push(' ');
72                    pattern.push_str(pat);
73                } else {
74                    fields.insert(key.clone(), pat.to_string());
75                }
76                text.clear();
77            };
78        }
79
80        for (idx, ch) in input.char_indices() {
81            match ch {
82                // Backslash escaping
83                _ if escaped => {
84                    // '%' is the only character that is special cased.
85                    // You can escape it to prevent parsing the text that
86                    // follows it as a field name.
87                    if ch != '%' {
88                        text.push('\\');
89                    }
90                    text.push(ch);
91                    escaped = false;
92                }
93                '\\' => escaped = !escaped,
94                '%' => {
95                    if !text.is_empty() {
96                        finish_field!();
97                    }
98                    let (range, _field) = self
99                    .column_ranges
100                    .last_mut()
101                    .expect("column_ranges is non-empty");
102                    range.end = idx;
103                    in_field = true;
104                }
105                ' ' if in_field => {
106                    text.clear();
107                    in_field = false;
108                    if text.is_empty() && self.empty_column {
109                        field = self.column_names.iter().find(|x| x.is_empty());
110                    }
111                }
112                _ if in_field => {
113                    text.push(ch);
114                    // Go over all columns and their indices, find all that starts with field key,
115                    // select a column that fits key the most.
116                    field = self
117                    .column_names
118                    .iter()
119                    .filter(|col| col.starts_with(&text))
120                    // select "fittest" column
121                    .min_by_key(|col| col.len());
122
123                    // Update the column range for this column.
124                    if let Some((_range, current_field)) = self
125                    .column_ranges
126                    .last_mut()
127                    .filter(|(range, _)| range.end == usize::MAX)
128                    {
129                        *current_field = field.cloned();
130                    } else {
131                        self.column_ranges.push((idx..usize::MAX, field.cloned()));
132                    }
133                }
134                _ => text.push(ch),
135            }
136        }
137
138        if !in_field && !text.is_empty() {
139            finish_field!();
140        }
141
142        let new_inner: HashMap<_, _> = fields
143        .into_iter()
144        .map(|(field, query)| (field, query.as_str().into()))
145        .collect();
146
147        mem::replace(&mut self.inner, new_inner)
148    }
149
150    /// Finds the column which the cursor is 'within' in the last parse.
151    ///
152    /// The cursor is considered to be within a column when it is placed within any
153    /// of a column's text. See the `active_column_test` unit test below for examples.
154    ///
155    /// `cursor` is a byte index that represents the location of the prompt's cursor.
156    pub fn active_column(&self, cursor: usize) -> Option<&Arc<str>> {
157        let point = self
158        .column_ranges
159        .partition_point(|(range, _field)| cursor > range.end);
160
161        self.column_ranges
162        .get(point)
163        .filter(|(range, _field)| cursor >= range.start && cursor <= range.end)
164        .and_then(|(_range, field)| field.as_ref())
165    }
166}