1 use std::cmp::{max, min};
2
3 use glutin::event::ModifiersState;
4
5 use alacritty_terminal::grid::BidirectionalIterator;
6 use alacritty_terminal::index::{Boundary, Direction, Point};
7 use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
8 use alacritty_terminal::term::{Term, TermMode};
9
10 use crate::config::ui_config::{Hint, HintAction};
11 use crate::config::Config;
12 use crate::display::content::RegexMatches;
13 use crate::display::MAX_SEARCH_LINES;
14
15 /// Percentage of characters in the hints alphabet used for the last character.
16 const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
17
18 /// Keyboard regex hint state.
19 pub struct HintState {
20 /// Hint currently in use.
21 hint: Option<Hint>,
22
23 /// Alphabet for hint labels.
24 alphabet: String,
25
26 /// Visible matches.
27 matches: RegexMatches,
28
29 /// Key label for each visible match.
30 labels: Vec<Vec<char>>,
31
32 /// Keys pressed for hint selection.
33 keys: Vec<char>,
34 }
35
36 impl HintState {
37 /// Initialize an inactive hint state.
new<S: Into<String>>(alphabet: S) -> Self38 pub fn new<S: Into<String>>(alphabet: S) -> Self {
39 Self {
40 alphabet: alphabet.into(),
41 hint: Default::default(),
42 matches: Default::default(),
43 labels: Default::default(),
44 keys: Default::default(),
45 }
46 }
47
48 /// Check if a hint selection is in progress.
active(&self) -> bool49 pub fn active(&self) -> bool {
50 self.hint.is_some()
51 }
52
53 /// Start the hint selection process.
start(&mut self, hint: Hint)54 pub fn start(&mut self, hint: Hint) {
55 self.hint = Some(hint);
56 }
57
58 /// Cancel the hint highlighting process.
stop(&mut self)59 fn stop(&mut self) {
60 self.matches.clear();
61 self.labels.clear();
62 self.keys.clear();
63 self.hint = None;
64 }
65
66 /// Update the visible hint matches and key labels.
update_matches<T>(&mut self, term: &Term<T>)67 pub fn update_matches<T>(&mut self, term: &Term<T>) {
68 let hint = match self.hint.as_mut() {
69 Some(hint) => hint,
70 None => return,
71 };
72
73 // Find visible matches.
74 self.matches.0 = hint.regex.with_compiled(|regex| {
75 let mut matches = RegexMatches::new(term, regex);
76
77 // Apply post-processing and search for sub-matches if necessary.
78 if hint.post_processing {
79 matches
80 .drain(..)
81 .map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
82 .flatten()
83 .collect()
84 } else {
85 matches.0
86 }
87 });
88
89 // Cancel highlight with no visible matches.
90 if self.matches.is_empty() {
91 self.stop();
92 return;
93 }
94
95 let mut generator = HintLabels::new(&self.alphabet, HINT_SPLIT_PERCENTAGE);
96 let match_count = self.matches.len();
97 let keys_len = self.keys.len();
98
99 // Get the label for each match.
100 self.labels.resize(match_count, Vec::new());
101 for i in (0..match_count).rev() {
102 let mut label = generator.next();
103 if label.len() >= keys_len && label[..keys_len] == self.keys[..] {
104 self.labels[i] = label.split_off(keys_len);
105 } else {
106 self.labels[i] = Vec::new();
107 }
108 }
109 }
110
111 /// Handle keyboard input during hint selection.
keyboard_input<T>(&mut self, term: &Term<T>, c: char) -> Option<HintMatch>112 pub fn keyboard_input<T>(&mut self, term: &Term<T>, c: char) -> Option<HintMatch> {
113 match c {
114 // Use backspace to remove the last character pressed.
115 '\x08' | '\x1f' => {
116 self.keys.pop();
117 },
118 // Cancel hint highlighting on ESC/Ctrl+c.
119 '\x1b' | '\x03' => self.stop(),
120 _ => (),
121 }
122
123 // Update the visible matches.
124 self.update_matches(term);
125
126 let hint = self.hint.as_ref()?;
127
128 // Find the last label starting with the input character.
129 let mut labels = self.labels.iter().enumerate().rev();
130 let (index, label) = labels.find(|(_, label)| !label.is_empty() && label[0] == c)?;
131
132 // Check if the selected label is fully matched.
133 if label.len() == 1 {
134 let bounds = self.matches[index].clone();
135 let action = hint.action.clone();
136
137 self.stop();
138
139 Some(HintMatch { action, bounds })
140 } else {
141 // Store character to preserve the selection.
142 self.keys.push(c);
143
144 None
145 }
146 }
147
148 /// Hint key labels.
labels(&self) -> &Vec<Vec<char>>149 pub fn labels(&self) -> &Vec<Vec<char>> {
150 &self.labels
151 }
152
153 /// Visible hint regex matches.
matches(&self) -> &RegexMatches154 pub fn matches(&self) -> &RegexMatches {
155 &self.matches
156 }
157
158 /// Update the alphabet used for hint labels.
update_alphabet(&mut self, alphabet: &str)159 pub fn update_alphabet(&mut self, alphabet: &str) {
160 if self.alphabet != alphabet {
161 self.alphabet = alphabet.to_owned();
162 self.keys.clear();
163 }
164 }
165 }
166
167 /// Hint match which was selected by the user.
168 #[derive(PartialEq, Debug, Clone)]
169 pub struct HintMatch {
170 /// Action for handling the text.
171 pub action: HintAction,
172
173 /// Terminal range matching the hint.
174 pub bounds: Match,
175 }
176
177 /// Generator for creating new hint labels.
178 struct HintLabels {
179 /// Full character set available.
180 alphabet: Vec<char>,
181
182 /// Alphabet indices for the next label.
183 indices: Vec<usize>,
184
185 /// Point separating the alphabet's head and tail characters.
186 ///
187 /// To make identification of the tail character easy, part of the alphabet cannot be used for
188 /// any other position.
189 ///
190 /// All characters in the alphabet before this index will be used for the last character, while
191 /// the rest will be used for everything else.
192 split_point: usize,
193 }
194
195 impl HintLabels {
196 /// Create a new label generator.
197 ///
198 /// The `split_ratio` should be a number between 0.0 and 1.0 representing the percentage of
199 /// elements in the alphabet which are reserved for the tail of the hint label.
new(alphabet: impl Into<String>, split_ratio: f32) -> Self200 fn new(alphabet: impl Into<String>, split_ratio: f32) -> Self {
201 let alphabet: Vec<char> = alphabet.into().chars().collect();
202 let split_point = ((alphabet.len() - 1) as f32 * split_ratio.min(1.)) as usize;
203
204 Self { indices: vec![0], split_point, alphabet }
205 }
206
207 /// Get the characters for the next label.
next(&mut self) -> Vec<char>208 fn next(&mut self) -> Vec<char> {
209 let characters = self.indices.iter().rev().map(|index| self.alphabet[*index]).collect();
210 self.increment();
211 characters
212 }
213
214 /// Increment the character sequence.
increment(&mut self)215 fn increment(&mut self) {
216 // Increment the last character; if it's not at the split point we're done.
217 let tail = &mut self.indices[0];
218 if *tail < self.split_point {
219 *tail += 1;
220 return;
221 }
222 *tail = 0;
223
224 // Increment all other characters in reverse order.
225 let alphabet_len = self.alphabet.len();
226 for index in self.indices.iter_mut().skip(1) {
227 if *index + 1 == alphabet_len {
228 // Reset character and move to the next if it's already at the limit.
229 *index = self.split_point + 1;
230 } else {
231 // If the character can be incremented, we're done.
232 *index += 1;
233 return;
234 }
235 }
236
237 // Extend the sequence with another character when nothing could be incremented.
238 self.indices.push(self.split_point + 1);
239 }
240 }
241
242 /// Check if there is a hint highlighted at the specified point.
highlighted_at<T>( term: &Term<T>, config: &Config, point: Point, mouse_mods: ModifiersState, ) -> Option<HintMatch>243 pub fn highlighted_at<T>(
244 term: &Term<T>,
245 config: &Config,
246 point: Point,
247 mouse_mods: ModifiersState,
248 ) -> Option<HintMatch> {
249 let mouse_mode = term.mode().intersects(TermMode::MOUSE_MODE);
250
251 config.ui_config.hints.enabled.iter().find_map(|hint| {
252 // Check if all required modifiers are pressed.
253 let highlight = hint.mouse.map_or(false, |mouse| {
254 mouse.enabled
255 && mouse_mods.contains(mouse.mods.0)
256 && (!mouse_mode || mouse_mods.contains(ModifiersState::SHIFT))
257 });
258 if !highlight {
259 return None;
260 }
261
262 hint.regex.with_compiled(|regex| {
263 // Setup search boundaries.
264 let mut start = term.line_search_left(point);
265 start.line = max(start.line, point.line - MAX_SEARCH_LINES);
266 let mut end = term.line_search_right(point);
267 end.line = min(end.line, point.line + MAX_SEARCH_LINES);
268
269 // Function to verify that the specified point is inside the match.
270 let at_point = |rm: &Match| *rm.end() >= point && *rm.start() <= point;
271
272 // Check if there's any match at the specified point.
273 let mut iter = RegexIter::new(start, end, Direction::Right, term, regex);
274 let regex_match = iter.find(at_point)?;
275
276 // Apply post-processing and search for sub-matches if necessary.
277 let regex_match = if hint.post_processing {
278 HintPostProcessor::new(term, regex, regex_match).find(at_point)
279 } else {
280 Some(regex_match)
281 };
282
283 regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds })
284 })
285 })
286 }
287
288 /// Iterator over all post-processed matches inside an existing hint match.
289 struct HintPostProcessor<'a, T> {
290 /// Regex search DFAs.
291 regex: &'a RegexSearch,
292
293 /// Terminal reference.
294 term: &'a Term<T>,
295
296 /// Next hint match in the iterator.
297 next_match: Option<Match>,
298
299 /// Start point for the next search.
300 start: Point,
301
302 /// End point for the hint match iterator.
303 end: Point,
304 }
305
306 impl<'a, T> HintPostProcessor<'a, T> {
307 /// Create a new iterator for an unprocessed match.
new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self308 fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self {
309 let end = *regex_match.end();
310 let mut post_processor = Self { next_match: None, start: end, end, term, regex };
311
312 // Post-process the first hint match.
313 let next_match = post_processor.hint_post_processing(®ex_match);
314 post_processor.start = next_match.end().add(term, Boundary::Grid, 1);
315 post_processor.next_match = Some(next_match);
316
317 post_processor
318 }
319
320 /// Apply some hint post processing heuristics.
321 ///
322 /// This will check the end of the hint and make it shorter if certain characters are determined
323 /// to be unlikely to be intentionally part of the hint.
324 ///
325 /// This is most useful for identifying URLs appropriately.
hint_post_processing(&self, regex_match: &Match) -> Match326 fn hint_post_processing(&self, regex_match: &Match) -> Match {
327 let mut iter = self.term.grid().iter_from(*regex_match.start());
328
329 let mut c = iter.cell().c;
330
331 // Truncate uneven number of brackets.
332 let end = *regex_match.end();
333 let mut open_parents = 0;
334 let mut open_brackets = 0;
335 loop {
336 match c {
337 '(' => open_parents += 1,
338 '[' => open_brackets += 1,
339 ')' => {
340 if open_parents == 0 {
341 iter.prev();
342 break;
343 } else {
344 open_parents -= 1;
345 }
346 },
347 ']' => {
348 if open_brackets == 0 {
349 iter.prev();
350 break;
351 } else {
352 open_brackets -= 1;
353 }
354 },
355 _ => (),
356 }
357
358 if iter.point() == end {
359 break;
360 }
361
362 match iter.next() {
363 Some(indexed) => c = indexed.cell.c,
364 None => break,
365 }
366 }
367
368 // Truncate trailing characters which are likely to be delimiters.
369 let start = *regex_match.start();
370 while iter.point() != start {
371 if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') {
372 break;
373 }
374
375 match iter.prev() {
376 Some(indexed) => c = indexed.cell.c,
377 None => break,
378 }
379 }
380
381 start..=iter.point()
382 }
383 }
384
385 impl<'a, T> Iterator for HintPostProcessor<'a, T> {
386 type Item = Match;
387
next(&mut self) -> Option<Self::Item>388 fn next(&mut self) -> Option<Self::Item> {
389 let next_match = self.next_match.take()?;
390
391 if self.start <= self.end {
392 if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) {
393 let regex_match = self.hint_post_processing(&rm);
394 self.start = regex_match.end().add(self.term, Boundary::Grid, 1);
395 self.next_match = Some(regex_match);
396 }
397 }
398
399 Some(next_match)
400 }
401 }
402
403 #[cfg(test)]
404 mod tests {
405 use super::*;
406
407 #[test]
hint_label_generation()408 fn hint_label_generation() {
409 let mut generator = HintLabels::new("0123", 0.5);
410
411 assert_eq!(generator.next(), vec!['0']);
412 assert_eq!(generator.next(), vec!['1']);
413
414 assert_eq!(generator.next(), vec!['2', '0']);
415 assert_eq!(generator.next(), vec!['2', '1']);
416 assert_eq!(generator.next(), vec!['3', '0']);
417 assert_eq!(generator.next(), vec!['3', '1']);
418
419 assert_eq!(generator.next(), vec!['2', '2', '0']);
420 assert_eq!(generator.next(), vec!['2', '2', '1']);
421 assert_eq!(generator.next(), vec!['2', '3', '0']);
422 assert_eq!(generator.next(), vec!['2', '3', '1']);
423 assert_eq!(generator.next(), vec!['3', '2', '0']);
424 assert_eq!(generator.next(), vec!['3', '2', '1']);
425 assert_eq!(generator.next(), vec!['3', '3', '0']);
426 assert_eq!(generator.next(), vec!['3', '3', '1']);
427
428 assert_eq!(generator.next(), vec!['2', '2', '2', '0']);
429 assert_eq!(generator.next(), vec!['2', '2', '2', '1']);
430 assert_eq!(generator.next(), vec!['2', '2', '3', '0']);
431 assert_eq!(generator.next(), vec!['2', '2', '3', '1']);
432 assert_eq!(generator.next(), vec!['2', '3', '2', '0']);
433 assert_eq!(generator.next(), vec!['2', '3', '2', '1']);
434 assert_eq!(generator.next(), vec!['2', '3', '3', '0']);
435 assert_eq!(generator.next(), vec!['2', '3', '3', '1']);
436 assert_eq!(generator.next(), vec!['3', '2', '2', '0']);
437 assert_eq!(generator.next(), vec!['3', '2', '2', '1']);
438 assert_eq!(generator.next(), vec!['3', '2', '3', '0']);
439 assert_eq!(generator.next(), vec!['3', '2', '3', '1']);
440 assert_eq!(generator.next(), vec!['3', '3', '2', '0']);
441 assert_eq!(generator.next(), vec!['3', '3', '2', '1']);
442 assert_eq!(generator.next(), vec!['3', '3', '3', '0']);
443 assert_eq!(generator.next(), vec!['3', '3', '3', '1']);
444 }
445 }
446