Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/picker/query.rs')
| -rw-r--r-- | helix-term/src/ui/picker/query.rs | 94 |
1 files changed, 90 insertions, 4 deletions
diff --git a/helix-term/src/ui/picker/query.rs b/helix-term/src/ui/picker/query.rs index 89ade95f..e433a11f 100644 --- a/helix-term/src/ui/picker/query.rs +++ b/helix-term/src/ui/picker/query.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, mem, sync::Arc}; +use std::{collections::HashMap, mem, ops::Range, sync::Arc}; #[derive(Debug)] pub(super) struct PickerQuery { @@ -11,6 +11,10 @@ pub(super) struct PickerQuery { /// The mapping between column names and input in the query /// for those columns. inner: HashMap<Arc<str>, Arc<str>>, + /// The byte ranges of the input text which are used as input for each column. + /// This is calculated at parsing time for use in [Self::active_column]. + /// This Vec is naturally sorted in ascending order and ranges do not overlap. + column_ranges: Vec<(Range<usize>, Option<Arc<str>>)>, } impl PartialEq<HashMap<Arc<str>, Arc<str>>> for PickerQuery { @@ -26,10 +30,12 @@ impl PickerQuery { ) -> Self { let column_names: Box<[_]> = column_names.collect(); let inner = HashMap::with_capacity(column_names.len()); + let column_ranges = vec![(0..usize::MAX, Some(column_names[primary_column].clone()))]; Self { column_names, primary_column, inner, + column_ranges, } } @@ -44,6 +50,9 @@ impl PickerQuery { let mut in_field = false; let mut field = None; let mut text = String::new(); + self.column_ranges.clear(); + self.column_ranges + .push((0..usize::MAX, Some(primary_field.clone()))); macro_rules! finish_field { () => { @@ -59,7 +68,7 @@ impl PickerQuery { }; } - for ch in input.chars() { + for (idx, ch) in input.char_indices() { match ch { // Backslash escaping _ if escaped => { @@ -77,9 +86,19 @@ impl PickerQuery { if !text.is_empty() { finish_field!(); } + let (range, _field) = self + .column_ranges + .last_mut() + .expect("column_ranges is non-empty"); + range.end = idx; in_field = true; } ' ' if in_field => { + text.clear(); + in_field = false; + } + _ if in_field => { + text.push(ch); // Go over all columns and their indices, find all that starts with field key, // select a column that fits key the most. field = self @@ -88,8 +107,17 @@ impl PickerQuery { .filter(|col| col.starts_with(&text)) // select "fittest" column .min_by_key(|col| col.len()); - text.clear(); - in_field = false; + + // Update the column range for this column. + if let Some((_range, current_field)) = self + .column_ranges + .last_mut() + .filter(|(range, _)| range.end == usize::MAX) + { + *current_field = field.cloned(); + } else { + self.column_ranges.push((idx..usize::MAX, field.cloned())); + } } _ => text.push(ch), } @@ -106,6 +134,23 @@ impl PickerQuery { mem::replace(&mut self.inner, new_inner) } + + /// Finds the column which the cursor is 'within' in the last parse. + /// + /// The cursor is considered to be within a column when it is placed within any + /// of a column's text. See the `active_column_test` unit test below for examples. + /// + /// `cursor` is a byte index that represents the location of the prompt's cursor. + pub fn active_column(&self, cursor: usize) -> Option<&Arc<str>> { + let point = self + .column_ranges + .partition_point(|(range, _field)| cursor > range.end); + + self.column_ranges + .get(point) + .filter(|(range, _field)| cursor >= range.start && cursor <= range.end) + .and_then(|(_range, field)| field.as_ref()) + } } #[cfg(test)] @@ -279,4 +324,45 @@ mod test { ) ); } + + #[test] + fn active_column_test() { + fn active_column<'a>(query: &'a mut PickerQuery, input: &str) -> Option<&'a str> { + let cursor = input.find('|').expect("cursor must be indicated with '|'"); + let input = input.replace('|', ""); + query.parse(&input); + query.active_column(cursor).map(AsRef::as_ref) + } + + let mut query = PickerQuery::new( + ["primary".into(), "foo".into(), "bar".into()].into_iter(), + 0, + ); + + assert_eq!(active_column(&mut query, "|"), Some("primary")); + assert_eq!(active_column(&mut query, "hello| world"), Some("primary")); + assert_eq!(active_column(&mut query, "|%foo hello"), Some("primary")); + assert_eq!(active_column(&mut query, "%foo|"), Some("foo")); + assert_eq!(active_column(&mut query, "%|"), None); + assert_eq!(active_column(&mut query, "%baz|"), None); + assert_eq!(active_column(&mut query, "%quiz%|"), None); + assert_eq!(active_column(&mut query, "%foo hello| world"), Some("foo")); + assert_eq!(active_column(&mut query, "%foo hello world|"), Some("foo")); + assert_eq!(active_column(&mut query, "%foo| hello world"), Some("foo")); + assert_eq!(active_column(&mut query, "%|foo hello world"), Some("foo")); + assert_eq!(active_column(&mut query, "%f|oo hello world"), Some("foo")); + assert_eq!(active_column(&mut query, "hello %f|oo world"), Some("foo")); + assert_eq!( + active_column(&mut query, "hello %f|oo world %bar !"), + Some("foo") + ); + assert_eq!( + active_column(&mut query, "hello %foo wo|rld %bar !"), + Some("foo") + ); + assert_eq!( + active_column(&mut query, "hello %foo world %bar !|"), + Some("bar") + ); + } } |