Picker: Highlight the currently active column
We can track the ranges in the input text that correspond to each column and use this information during rendering to apply a new theme key that makes the "active column" stand out. This makes it easier to tell at a glance which column you're entering.
This commit is contained in:
parent
a7777b3c11
commit
9de5f5cefa
4 changed files with 106 additions and 5 deletions
|
@ -298,6 +298,7 @@ These scopes are used for theming the editor interface:
|
|||
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
||||
| `ui.popup.info` | Prompt for multiple key options |
|
||||
| `ui.picker.header` | Column names in pickers with multiple columns |
|
||||
| `ui.picker.header.active` | The column name in pickers with multiple columns where the cursor is entering into. |
|
||||
| `ui.window` | Borderlines separating splits |
|
||||
| `ui.help` | Description box for commands |
|
||||
| `ui.text` | Default text style, command prompts, popup text, etc. |
|
||||
|
|
|
@ -787,13 +787,21 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
|||
|
||||
// -- Header
|
||||
if self.columns.len() > 1 {
|
||||
let active_column = self.query.active_column(self.prompt.position());
|
||||
let header_style = cx.editor.theme.get("ui.picker.header");
|
||||
|
||||
table = table.header(Row::new(self.columns.iter().map(|column| {
|
||||
if column.hidden {
|
||||
Cell::default()
|
||||
} else {
|
||||
Cell::from(Span::styled(Cow::from(&*column.name), header_style))
|
||||
let style = if active_column.is_some_and(|name| Arc::ptr_eq(name, &column.name))
|
||||
{
|
||||
cx.editor.theme.get("ui.picker.header.active")
|
||||
} else {
|
||||
header_style
|
||||
};
|
||||
|
||||
Cell::from(Span::styled(Cow::from(&*column.name), style))
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,12 @@ impl Prompt {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the byte index in the input representing the current cursor location.
|
||||
#[inline]
|
||||
pub(crate) fn position(&self) -> usize {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
|
||||
self.set_line(line, editor);
|
||||
self
|
||||
|
|
Loading…
Add table
Reference in a new issue