alacritty/alacritty/src/display/hint.rs

263 lines
8.4 KiB
Rust
Raw Normal View History

use alacritty_terminal::term::search::Match;
use alacritty_terminal::term::Term;
use crate::config::ui_config::{Hint, HintAction};
use crate::display::content::RegexMatches;
/// Percentage of characters in the hints alphabet used for the last character.
const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
/// Keyboard regex hint state.
pub struct HintState {
/// Hint currently in use.
hint: Option<Hint>,
/// Alphabet for hint labels.
alphabet: String,
/// Visible matches.
matches: RegexMatches,
/// Key label for each visible match.
labels: Vec<Vec<char>>,
/// Keys pressed for hint selection.
keys: Vec<char>,
}
impl HintState {
/// Initialize an inactive hint state.
pub fn new<S: Into<String>>(alphabet: S) -> Self {
Self {
alphabet: alphabet.into(),
hint: Default::default(),
matches: Default::default(),
labels: Default::default(),
keys: Default::default(),
}
}
/// Check if a hint selection is in progress.
pub fn active(&self) -> bool {
self.hint.is_some()
}
/// Start the hint selection process.
pub fn start(&mut self, hint: Hint) {
self.hint = Some(hint);
}
/// Cancel the hint highlighting process.
fn stop(&mut self) {
self.matches.clear();
self.labels.clear();
self.keys.clear();
self.hint = None;
}
/// Update the visible hint matches and key labels.
pub fn update_matches<T>(&mut self, term: &Term<T>) {
let hint = match self.hint.as_mut() {
Some(hint) => hint,
None => return,
};
// Find visible matches.
self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex));
// Cancel highlight with no visible matches.
if self.matches.is_empty() {
self.stop();
return;
}
let mut generator = HintLabels::new(&self.alphabet, HINT_SPLIT_PERCENTAGE);
let match_count = self.matches.len();
let keys_len = self.keys.len();
// Get the label for each match.
self.labels.resize(match_count, Vec::new());
for i in (0..match_count).rev() {
let mut label = generator.next();
if label.len() >= keys_len && label[..keys_len] == self.keys[..] {
self.labels[i] = label.split_off(keys_len);
} else {
self.labels[i] = Vec::new();
}
}
}
/// Handle keyboard input during hint selection.
pub fn keyboard_input<T>(&mut self, term: &Term<T>, c: char) -> Option<HintMatch> {
match c {
// Use backspace to remove the last character pressed.
'\x08' | '\x1f' => {
self.keys.pop();
},
// Cancel hint highlighting on ESC.
'\x1b' => self.stop(),
_ => (),
}
// Update the visible matches.
self.update_matches(term);
let hint = self.hint.as_ref()?;
// Find the last label starting with the input character.
let mut labels = self.labels.iter().enumerate().rev();
let (index, label) = labels.find(|(_, label)| !label.is_empty() && label[0] == c)?;
// Check if the selected label is fully matched.
if label.len() == 1 {
let bounds = self.matches[index].clone();
let action = hint.action.clone();
self.stop();
Some(HintMatch { action, bounds })
} else {
// Store character to preserve the selection.
self.keys.push(c);
None
}
}
/// Hint key labels.
pub fn labels(&self) -> &Vec<Vec<char>> {
&self.labels
}
/// Visible hint regex matches.
pub fn matches(&self) -> &RegexMatches {
&self.matches
}
/// Update the alphabet used for hint labels.
pub fn update_alphabet(&mut self, alphabet: &str) {
if self.alphabet != alphabet {
self.alphabet = alphabet.to_owned();
self.keys.clear();
}
}
}
/// Hint match which was selected by the user.
pub struct HintMatch {
/// Action for handling the text.
pub action: HintAction,
/// Terminal range matching the hint.
pub bounds: Match,
}
/// Generator for creating new hint labels.
struct HintLabels {
/// Full character set available.
alphabet: Vec<char>,
/// Alphabet indices for the next label.
indices: Vec<usize>,
/// Point separating the alphabet's head and tail characters.
///
/// To make identification of the tail character easy, part of the alphabet cannot be used for
/// any other position.
///
/// All characters in the alphabet before this index will be used for the last character, while
/// the rest will be used for everything else.
split_point: usize,
}
impl HintLabels {
/// Create a new label generator.
///
/// The `split_ratio` should be a number between 0.0 and 1.0 representing the percentage of
/// elements in the alphabet which are reserved for the tail of the hint label.
fn new(alphabet: impl Into<String>, split_ratio: f32) -> Self {
let alphabet: Vec<char> = alphabet.into().chars().collect();
let split_point = ((alphabet.len() - 1) as f32 * split_ratio.min(1.)) as usize;
Self { indices: vec![0], split_point, alphabet }
}
/// Get the characters for the next label.
fn next(&mut self) -> Vec<char> {
let characters = self.indices.iter().rev().map(|index| self.alphabet[*index]).collect();
self.increment();
characters
}
/// Increment the character sequence.
fn increment(&mut self) {
// Increment the last character; if it's not at the split point we're done.
let tail = &mut self.indices[0];
if *tail < self.split_point {
*tail += 1;
return;
}
*tail = 0;
// Increment all other characters in reverse order.
let alphabet_len = self.alphabet.len();
for index in self.indices.iter_mut().skip(1) {
if *index + 1 == alphabet_len {
// Reset character and move to the next if it's already at the limit.
*index = self.split_point + 1;
} else {
// If the character can be incremented, we're done.
*index += 1;
return;
}
}
// Extend the sequence with another character when nothing could be incremented.
self.indices.push(self.split_point + 1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hint_label_generation() {
let mut generator = HintLabels::new("0123", 0.5);
assert_eq!(generator.next(), vec!['0']);
assert_eq!(generator.next(), vec!['1']);
assert_eq!(generator.next(), vec!['2', '0']);
assert_eq!(generator.next(), vec!['2', '1']);
assert_eq!(generator.next(), vec!['3', '0']);
assert_eq!(generator.next(), vec!['3', '1']);
assert_eq!(generator.next(), vec!['2', '2', '0']);
assert_eq!(generator.next(), vec!['2', '2', '1']);
assert_eq!(generator.next(), vec!['2', '3', '0']);
assert_eq!(generator.next(), vec!['2', '3', '1']);
assert_eq!(generator.next(), vec!['3', '2', '0']);
assert_eq!(generator.next(), vec!['3', '2', '1']);
assert_eq!(generator.next(), vec!['3', '3', '0']);
assert_eq!(generator.next(), vec!['3', '3', '1']);
assert_eq!(generator.next(), vec!['2', '2', '2', '0']);
assert_eq!(generator.next(), vec!['2', '2', '2', '1']);
assert_eq!(generator.next(), vec!['2', '2', '3', '0']);
assert_eq!(generator.next(), vec!['2', '2', '3', '1']);
assert_eq!(generator.next(), vec!['2', '3', '2', '0']);
assert_eq!(generator.next(), vec!['2', '3', '2', '1']);
assert_eq!(generator.next(), vec!['2', '3', '3', '0']);
assert_eq!(generator.next(), vec!['2', '3', '3', '1']);
assert_eq!(generator.next(), vec!['3', '2', '2', '0']);
assert_eq!(generator.next(), vec!['3', '2', '2', '1']);
assert_eq!(generator.next(), vec!['3', '2', '3', '0']);
assert_eq!(generator.next(), vec!['3', '2', '3', '1']);
assert_eq!(generator.next(), vec!['3', '3', '2', '0']);
assert_eq!(generator.next(), vec!['3', '3', '2', '1']);
assert_eq!(generator.next(), vec!['3', '3', '3', '0']);
assert_eq!(generator.next(), vec!['3', '3', '3', '1']);
}
}