Add a special query syntax for Pickers to select columns

Now that the picker is defined as a table, we need a way to provide
input for each field in the picker. We introduce a small query syntax
that supports multiple columns without being too verbose. Fields are
specified as `%field:pattern`. The default column for a picker doesn't
need the `%field:` prefix. The field name may be selected by a prefix
of the field, for example `%p:foo.rs` rather than `%path:foo.rs`.

Co-authored-by: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Michael Davis 4 months ago
parent 4908438de0
commit 8a07f357e5
No known key found for this signature in database

@ -1,4 +1,5 @@
mod handlers;
mod query;
use crate::{
alt,
@ -227,7 +228,7 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
cursor: u32,
prompt: Prompt,
previous_pattern: String,
query: query::PickerQuery,
/// Whether to show the preview panel (default true)
show_preview: bool,
@ -345,7 +346,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
shutdown,
cursor: 0,
prompt,
previous_pattern: String::new(),
query: query::PickerQuery::default(),
truncate_start: true,
show_preview: true,
callback_fn: Box::new(callback_fn),
@ -447,6 +448,13 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
.map(|item| item.data)
}
fn primary_query(&self) -> Arc<str> {
self.query
.get(&self.column_names[self.primary_column])
.cloned()
.unwrap_or_else(|| "".into())
}
fn header_height(&self) -> u16 {
if self.columns.len() > 1 {
1
@ -467,16 +475,31 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
}
fn handle_prompt_change(&mut self) {
let pattern = self.prompt.line();
// TODO: better track how the pattern has changed
if pattern != &self.previous_pattern {
self.matcher.pattern.reparse(
0,
pattern,
CaseMatching::Smart,
pattern.starts_with(&self.previous_pattern),
);
self.previous_pattern = pattern.clone();
let line = self.prompt.line();
let new_query = query::parse(&self.column_names, self.primary_column, line);
if new_query != self.query {
for (i, column) in self
.columns
.iter()
.filter(|column| column.filter)
.enumerate()
{
let pattern: &str = new_query
.get(column.name.as_str())
.map(|f| &**f)
.unwrap_or("");
let append = self
.query
.get(column.name.as_str())
.map(|old_pattern| pattern.starts_with(&**old_pattern))
.unwrap_or(false);
self.matcher
.pattern
.reparse(i, pattern, CaseMatching::Smart, append);
}
self.query = new_query;
}
}

@ -0,0 +1,210 @@
use std::{collections::HashMap, sync::Arc};
pub(super) type PickerQuery = HashMap<Arc<str>, Arc<str>>;
pub(super) fn parse(column_names: &[Arc<str>], primary_column: usize, input: &str) -> PickerQuery {
let mut fields: HashMap<Arc<str>, String> = HashMap::new();
let primary_field = &column_names[primary_column];
let mut escaped = false;
let mut quoted = false;
let mut in_field = false;
let mut field = None;
let mut text = String::new();
macro_rules! finish_field {
() => {
let key = field.take().unwrap_or(primary_field);
if let Some(pattern) = fields.get_mut(key) {
pattern.push(' ');
pattern.push_str(&text);
text.clear();
} else {
fields.insert(key.clone(), std::mem::take(&mut text));
}
};
}
for ch in input.chars() {
match ch {
// Backslash escaping
'\\' => escaped = !escaped,
_ if escaped => {
// Allow escaping '%' and '"'
if !matches!(ch, '%' | '"') {
text.push('\\');
}
text.push(ch);
escaped = false;
}
// Double quoting
'"' => quoted = !quoted,
'%' | ':' | ' ' if quoted => text.push(ch),
// Space either completes the current word if no field is specified
// or field if one is specified.
'%' | ' ' if !text.is_empty() => {
finish_field!();
in_field = ch == '%';
}
'%' => in_field = true,
':' if in_field => {
// Go over all columns and their indices, find all that starts with field key,
// select a column that fits key the most.
field = column_names
.iter()
.filter(|col| col.starts_with(&text))
// select "fittest" column
.min_by_key(|col| col.len());
text.clear();
in_field = false;
}
_ => text.push(ch),
}
}
if !in_field && !text.is_empty() {
finish_field!();
}
fields
.into_iter()
.map(|(field, query)| (field, query.as_str().into()))
.collect()
}
#[cfg(test)]
mod test {
use helix_core::hashmap;
use super::*;
#[test]
fn parse_query_test() {
let columns = &[
"primary".into(),
"field1".into(),
"field2".into(),
"another".into(),
"anode".into(),
];
let primary_column = 0;
// Basic field splitting
assert_eq!(
parse(columns, primary_column, "hello world"),
hashmap!(
"primary".into() => "hello world".into(),
)
);
assert_eq!(
parse(columns, primary_column, "hello %field1:world %field2:!"),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "world".into(),
"field2".into() => "!".into(),
)
);
assert_eq!(
parse(columns, primary_column, "%field1:abc %field2:def xyz"),
hashmap!(
"primary".into() => "xyz".into(),
"field1".into() => "abc".into(),
"field2".into() => "def".into(),
)
);
// Trailing space is trimmed
assert_eq!(
parse(columns, primary_column, "hello "),
hashmap!(
"primary".into() => "hello".into(),
)
);
// Trailing fields are trimmed.
assert_eq!(
parse(columns, primary_column, "hello %foo"),
hashmap!(
"primary".into() => "hello".into(),
)
);
// Quoting
assert_eq!(
parse(columns, primary_column, r#"hello %field1:"a b c""#),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "a b c".into(),
)
);
// Escaping
assert_eq!(
parse(columns, primary_column, r#"hello\ world"#),
hashmap!(
"primary".into() => r#"hello\ world"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"hello \%field1:world"#),
hashmap!(
"primary".into() => "hello %field1:world".into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"hello %field1:"a\"b""#),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => r#"a"b"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"%field1:hello\ world"#),
hashmap!(
"field1".into() => r#"hello\ world"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"%field1:"hello\ world""#),
hashmap!(
"field1".into() => r#"hello\ world"#.into(),
)
);
assert_eq!(
parse(columns, primary_column, r#"\bfoo\b"#),
hashmap!(
"primary".into() => r#"\bfoo\b"#.into(),
)
);
// Prefix
assert_eq!(
parse(columns, primary_column, "hello %anot:abc"),
hashmap!(
"primary".into() => "hello".into(),
"another".into() => "abc".into(),
)
);
assert_eq!(
parse(columns, primary_column, "hello %ano:abc"),
hashmap!(
"primary".into() => "hello".into(),
"anode".into() => "abc".into()
)
);
assert_eq!(
parse(columns, primary_column, "hello %field1:xyz %fie:abc"),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "xyz abc".into()
)
);
assert_eq!(
parse(columns, primary_column, "hello %fie:abc"),
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "abc".into()
)
);
}
}
Loading…
Cancel
Save