Skip to main content

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 primary_column_query(&self) -> Option<&str> {
51        let name = self.column_names.get(self.primary_column)?;
52        self.inner.get(name).map(|s| &**s)
53    }
54
55    pub fn primary_column_name(&self) -> Option<&str> {
56        self.column_names.get(self.primary_column).map(|s| &**s)
57    }
58
59    pub fn parse(&mut self, input: &str) -> HashMap<Arc<str>, Arc<str>> {
60        let (new_inner, new_ranges) = self.parse_impl(input);
61        self.column_ranges = new_ranges;
62        mem::replace(&mut self.inner, new_inner)
63    }
64
65    fn parse_impl(
66        &self,
67        input: &str,
68    ) -> (
69        HashMap<Arc<str>, Arc<str>>,
70        Vec<(std::ops::Range<usize>, Option<Arc<str>>)>,
71    ) {
72        let mut fields: std::collections::HashMap<Arc<str>, String> =
73            std::collections::HashMap::new();
74        let primary_field = &self.column_names[self.primary_column];
75        let mut column_ranges: Vec<(std::ops::Range<usize>, Option<Arc<str>>)> =
76            vec![(0..usize::MAX, Some(primary_field.clone()))];
77
78        let mut escaped = false;
79        let mut in_field = false;
80        let mut field = None;
81        let mut text = String::new();
82
83        macro_rules! finish_field {
84            () => {
85                let key = field.take().unwrap_or(primary_field);
86
87                // Trims one space from the end, enabling leading and trailing
88                // spaces in search patterns, while also retaining spaces as separators
89                // between column filters.
90                let pat = text.strip_suffix(' ').unwrap_or(&text);
91
92                if let Some(pattern) = fields.get_mut(key) {
93                    pattern.push(' ');
94                    pattern.push_str(pat);
95                } else {
96                    fields.insert(key.clone(), pat.to_string());
97                }
98                text.clear();
99            };
100        }
101
102        for (idx, ch) in input.char_indices() {
103            match ch {
104                // Backslash escaping
105                _ if escaped => {
106                    // '%' is the only character that is special cased.
107                    // You can escape it to prevent parsing the text that
108                    // follows it as a field name.
109                    if ch != '%' {
110                        text.push('\\');
111                    }
112                    text.push(ch);
113                    escaped = false;
114                }
115                '\\' => escaped = !escaped,
116                '%' => {
117                    if !text.is_empty() {
118                        finish_field!();
119                    }
120                    // Update the last column range end
121                    if let Some((range, _)) = column_ranges.last_mut() {
122                        range.end = idx;
123                    }
124                    in_field = true;
125                }
126                ' ' if in_field => {
127                    text.clear();
128                    in_field = false;
129                    // If the text is empty and self.empty_column is set, pick an empty column
130                    if text.is_empty() && self.empty_column {
131                        field = self.column_names.iter().find(|x| x.is_empty());
132                    }
133                }
134                _ if in_field => {
135                    text.push(ch);
136                    // Go over all columns and their indices, find all that starts with field key,
137                    // select a column that fits key the most.
138                    field = self
139                        .column_names
140                        .iter()
141                        .filter(|col| col.starts_with(&text))
142                        // select "fittest" column
143                        .min_by_key(|col| col.len());
144
145                    // Update the column range for this column.
146                    if let Some((_range, current_field)) = column_ranges
147                        .last_mut()
148                        .filter(|(range, _)| range.end == usize::MAX)
149                    {
150                        *current_field = field.cloned();
151                    } else {
152                        column_ranges.push((idx..usize::MAX, field.cloned()));
153                    }
154                }
155                _ => text.push(ch),
156            }
157        }
158
159        // Finish the last field if we're not in a field and there's leftover text
160        if !in_field && !text.is_empty() {
161            finish_field!();
162        }
163
164        let new_fields: HashMap<Arc<str>, Arc<str>> = fields
165            .into_iter()
166            .map(|(field, query)| (field, query.into()))
167            .collect();
168
169        (new_fields, column_ranges)
170    }
171
172    /// Finds the column which the cursor is 'within' in the last parse.
173    ///
174    /// The cursor is considered to be within a column when it is placed within any
175    /// of a column's text. See the `active_column_test` unit test below for examples.
176    ///
177    /// `cursor` is a byte index that represents the location of the prompt's cursor.
178    pub fn current_column(&self, cursor: usize) -> Option<&Arc<str>> {
179        let point = self
180            .column_ranges
181            .partition_point(|(range, _field)| cursor > range.end);
182
183        self.column_ranges
184            .get(point)
185            .filter(|(range, _field)| cursor >= range.start && cursor <= range.end)
186            .and_then(|(_range, field)| field.as_ref())
187    }
188
189    pub fn active_column_name(&self, input: &str) -> String {
190        let (_, ranges) = self.parse_impl(input);
191        ranges
192            .last()
193            .and_then(|x| x.1.as_deref())
194            .unwrap_or(self.column_names[self.primary_column].as_ref())
195            .to_string()
196    }
197
198    /// Finds the index of the column which the cursor is 'within' in the last parse.
199    /// Returns the primary column index if no specific column is active at the cursor.
200    // pub fn active_column_index(&mut self, input: &str) -> usize {
201    //     self.trial_parse(input);
202    //     self.column_ranges
203    //         .last()
204    //         .and_then(|x| {
205    //             x.1.as_ref()
206    //                 .and_then(|name| self.column_names.iter().position(|c| c == name))
207    //         })
208    //         .unwrap_or(self.primary_column)
209    // }
210
211    pub fn active_column_index(&self, cursor: usize) -> usize {
212        self.current_column(cursor)
213            .and_then(|name| self.column_names.iter().position(|c| c == name))
214            .unwrap_or(self.primary_column)
215    }
216
217    /// Sets the default column index.
218    ///
219    /// # Warning
220    /// This method does not check if the index exists.
221    pub fn with_default_column(mut self, index: usize) -> Self {
222        self.primary_column = index;
223        let primary_field = &self.column_names[self.primary_column];
224        self.column_ranges.clear();
225        self.column_ranges
226            .push((0..usize::MAX, Some(primary_field.clone())));
227        self
228    }
229}