rustyline/
highlight.rs

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