rustyline_with_hint_fix/
highlight.rs

1//! Syntax highlighting
2
3use crate::config::CompletionType;
4use std::borrow::Cow::{self, Borrowed, Owned};
5use std::cell::Cell;
6
7/// Syntax highlighter with [ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters).
8/// Rustyline will try to handle escape sequence for ANSI color on windows
9/// when not supported natively (windows <10).
10///
11/// Currently, the highlighted version *must* have the same display width as
12/// the original input.
13pub trait Highlighter {
14    /// Takes the currently edited `line` with the cursor `pos`ition and
15    /// returns the highlighted version (with ANSI color).
16    ///
17    /// For example, you can implement
18    /// [blink-matching-paren](https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html).
19    fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
20        let _ = pos;
21        Borrowed(line)
22    }
23    /// Takes the `prompt` and
24    /// returns the highlighted version (with ANSI color).
25    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
26        &'s self,
27        prompt: &'p str,
28        default: bool,
29    ) -> Cow<'b, str> {
30        let _ = default;
31        Borrowed(prompt)
32    }
33    /// Takes the `hint` and
34    /// returns the highlighted version (with ANSI color).
35    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
36        Borrowed(hint)
37    }
38    /// Takes the completion `candidate` and
39    /// returns the highlighted version (with ANSI color).
40    ///
41    /// Currently, used only with `CompletionType::List`.
42    fn highlight_candidate<'c>(
43        &self,
44        candidate: &'c str, // FIXME should be Completer::Candidate
45        completion: CompletionType,
46    ) -> Cow<'c, str> {
47        let _ = completion;
48        Borrowed(candidate)
49    }
50    /// Tells if `line` needs to be highlighted when a specific char is typed or
51    /// when cursor is moved under a specific char.
52    /// `forced` flag is `true` mainly when user presses Enter (i.e. transient vs final highlight).
53    ///
54    /// Used to optimize refresh when a character is inserted or the cursor is
55    /// moved.
56    fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
57        let _ = (line, pos, forced);
58        false
59    }
60}
61
62impl Highlighter for () {}
63
64impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H {
65    fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
66        (**self).highlight(line, pos)
67    }
68
69    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
70        &'s self,
71        prompt: &'p str,
72        default: bool,
73    ) -> Cow<'b, str> {
74        (**self).highlight_prompt(prompt, default)
75    }
76
77    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
78        (**self).highlight_hint(hint)
79    }
80
81    fn highlight_candidate<'c>(
82        &self,
83        candidate: &'c str,
84        completion: CompletionType,
85    ) -> Cow<'c, str> {
86        (**self).highlight_candidate(candidate, completion)
87    }
88
89    fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
90        (**self).highlight_char(line, pos, forced)
91    }
92}
93
94// TODO versus https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html?highlight=HighlightMatchingBracketProcessor#prompt_toolkit.layout.processors.HighlightMatchingBracketProcessor
95
96/// Highlight matching bracket when typed or cursor moved on.
97#[derive(Default)]
98pub struct MatchingBracketHighlighter {
99    bracket: Cell<Option<(u8, usize)>>, // memorize the character to search...
100}
101
102impl MatchingBracketHighlighter {
103    /// Constructor
104    #[must_use]
105    pub fn new() -> Self {
106        Self {
107            bracket: Cell::new(None),
108        }
109    }
110}
111
112impl Highlighter for MatchingBracketHighlighter {
113    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
114        if line.len() <= 1 {
115            return Borrowed(line);
116        }
117        // highlight matching brace/bracket/parenthesis if it exists
118        if let Some((bracket, pos)) = self.bracket.get() {
119            if let Some((matching, idx)) = find_matching_bracket(line, pos, bracket) {
120                let mut copy = line.to_owned();
121                copy.replace_range(idx..=idx, &format!("\x1b[1;34m{}\x1b[0m", matching as char));
122                return Owned(copy);
123            }
124        }
125        Borrowed(line)
126    }
127
128    fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
129        if forced {
130            self.bracket.set(None);
131            return false;
132        }
133        // will highlight matching brace/bracket/parenthesis if it exists
134        self.bracket.set(check_bracket(line, pos));
135        self.bracket.get().is_some()
136    }
137}
138
139fn find_matching_bracket(line: &str, pos: usize, bracket: u8) -> Option<(u8, usize)> {
140    let matching = matching_bracket(bracket);
141    let mut idx;
142    let mut unmatched = 1;
143    if is_open_bracket(bracket) {
144        // forward search
145        idx = pos + 1;
146        let bytes = &line.as_bytes()[idx..];
147        for b in bytes {
148            if *b == matching {
149                unmatched -= 1;
150                if unmatched == 0 {
151                    debug_assert_eq!(matching, line.as_bytes()[idx]);
152                    return Some((matching, idx));
153                }
154            } else if *b == bracket {
155                unmatched += 1;
156            }
157            idx += 1;
158        }
159        debug_assert_eq!(idx, line.len());
160    } else {
161        // backward search
162        idx = pos;
163        let bytes = &line.as_bytes()[..idx];
164        for b in bytes.iter().rev() {
165            if *b == matching {
166                unmatched -= 1;
167                if unmatched == 0 {
168                    debug_assert_eq!(matching, line.as_bytes()[idx - 1]);
169                    return Some((matching, idx - 1));
170                }
171            } else if *b == bracket {
172                unmatched += 1;
173            }
174            idx -= 1;
175        }
176        debug_assert_eq!(idx, 0);
177    }
178    None
179}
180
181// check under or before the cursor
182fn check_bracket(line: &str, pos: usize) -> Option<(u8, usize)> {
183    if line.is_empty() {
184        return None;
185    }
186    let mut pos = pos;
187    if pos >= line.len() {
188        pos = line.len() - 1; // before cursor
189        let b = line.as_bytes()[pos]; // previous byte
190        if is_close_bracket(b) {
191            Some((b, pos))
192        } else {
193            None
194        }
195    } else {
196        let mut under_cursor = true;
197        loop {
198            let b = line.as_bytes()[pos];
199            if is_close_bracket(b) {
200                return if pos == 0 { None } else { Some((b, pos)) };
201            } else if is_open_bracket(b) {
202                return if pos + 1 == line.len() {
203                    None
204                } else {
205                    Some((b, pos))
206                };
207            } else if under_cursor && pos > 0 {
208                under_cursor = false;
209                pos -= 1; // or before cursor
210            } else {
211                return None;
212            }
213        }
214    }
215}
216
217const fn matching_bracket(bracket: u8) -> u8 {
218    match bracket {
219        b'{' => b'}',
220        b'}' => b'{',
221        b'[' => b']',
222        b']' => b'[',
223        b'(' => b')',
224        b')' => b'(',
225        b => b,
226    }
227}
228const fn is_open_bracket(bracket: u8) -> bool {
229    matches!(bracket, b'{' | b'[' | b'(')
230}
231const fn is_close_bracket(bracket: u8) -> bool {
232    matches!(bracket, b'}' | b']' | b')')
233}
234
235#[cfg(test)]
236mod tests {
237    #[test]
238    pub fn find_matching_bracket() {
239        use super::find_matching_bracket;
240        assert_eq!(find_matching_bracket("(...", 0, b'('), None);
241        assert_eq!(find_matching_bracket("...)", 3, b')'), None);
242
243        assert_eq!(find_matching_bracket("()..", 0, b'('), Some((b')', 1)));
244        assert_eq!(find_matching_bracket("(..)", 0, b'('), Some((b')', 3)));
245
246        assert_eq!(find_matching_bracket("..()", 3, b')'), Some((b'(', 2)));
247        assert_eq!(find_matching_bracket("(..)", 3, b')'), Some((b'(', 0)));
248
249        assert_eq!(find_matching_bracket("(())", 0, b'('), Some((b')', 3)));
250        assert_eq!(find_matching_bracket("(())", 3, b')'), Some((b'(', 0)));
251    }
252    #[test]
253    pub fn check_bracket() {
254        use super::check_bracket;
255        assert_eq!(check_bracket(")...", 0), None);
256        assert_eq!(check_bracket("(...", 2), None);
257        assert_eq!(check_bracket("...(", 3), None);
258        assert_eq!(check_bracket("...(", 4), None);
259        assert_eq!(check_bracket("..).", 4), None);
260
261        assert_eq!(check_bracket("(...", 0), Some((b'(', 0)));
262        assert_eq!(check_bracket("(...", 1), Some((b'(', 0)));
263        assert_eq!(check_bracket("...)", 3), Some((b')', 3)));
264        assert_eq!(check_bracket("...)", 4), Some((b')', 3)));
265    }
266    #[test]
267    pub fn matching_bracket() {
268        use super::matching_bracket;
269        assert_eq!(matching_bracket(b'('), b')');
270        assert_eq!(matching_bracket(b')'), b'(');
271    }
272
273    #[test]
274    pub fn is_open_bracket() {
275        use super::is_close_bracket;
276        use super::is_open_bracket;
277        assert!(is_open_bracket(b'('));
278        assert!(is_close_bracket(b')'));
279    }
280}