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(&regex_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