rustyline/
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
53    /// vs final highlight).
54    ///
55    /// Used to optimize refresh when a character is inserted or the cursor is
56    /// moved.
57    fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
58        let _ = (line, pos, forced);
59        false
60    }
61}
62
63impl Highlighter for () {}
64
65impl<'r, H: ?Sized + Highlighter> Highlighter for &'r H {
66    fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
67        (**self).highlight(line, pos)
68    }
69
70    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
71        &'s self,
72        prompt: &'p str,
73        default: bool,
74    ) -> Cow<'b, str> {
75        (**self).highlight_prompt(prompt, default)
76    }
77
78    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
79        (**self).highlight_hint(hint)
80    }
81
82    fn highlight_candidate<'c>(
83        &self,
84        candidate: &'c str,
85        completion: CompletionType,
86    ) -> Cow<'c, str> {
87        (**self).highlight_candidate(candidate, completion)
88    }
89
90    fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
91        (**self).highlight_char(line, pos, forced)
92    }
93}
94
95// TODO versus https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html?highlight=HighlightMatchingBracketProcessor#prompt_toolkit.layout.processors.HighlightMatchingBracketProcessor
96
97/// Highlight matching bracket when typed or cursor moved on.
98#[derive(Default)]
99pub struct MatchingBracketHighlighter {
100    bracket: Cell<Option<(u8, usize)>>, // memorize the character to search...
101}
102
103impl MatchingBracketHighlighter {
104    /// Constructor
105    #[must_use]
106    pub fn new() -> Self {
107        Self {
108            bracket: Cell::new(None),
109        }
110    }
111}
112
113impl Highlighter for MatchingBracketHighlighter {
114    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
115        if line.len() <= 1 {
116            return Borrowed(line);
117        }
118        // highlight matching brace/bracket/parenthesis if it exists
119        if let Some((bracket, pos)) = self.bracket.get() {
120            if let Some((matching, idx)) = find_matching_bracket(line, pos, bracket) {
121                let mut copy = line.to_owned();
122                copy.replace_range(idx..=idx, &format!("\x1b[1;34m{}\x1b[0m", matching as char));
123                return Owned(copy);
124            }
125        }
126        Borrowed(line)
127    }
128
129    fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
130        if forced {
131            self.bracket.set(None);
132            return false;
133        }
134        // will highlight matching brace/bracket/parenthesis if it exists
135        self.bracket.set(check_bracket(line, pos));
136        self.bracket.get().is_some()
137    }
138}
139
140fn find_matching_bracket(line: &str, pos: usize, bracket: u8) -> Option<(u8, usize)> {
141    let matching = matching_bracket(bracket);
142    let mut idx;
143    let mut unmatched = 1;
144    if is_open_bracket(bracket) {
145        // forward search
146        idx = pos + 1;
147        let bytes = &line.as_bytes()[idx..];
148        for b in bytes {
149            if *b == matching {
150                unmatched -= 1;
151                if unmatched == 0 {
152                    debug_assert_eq!(matching, line.as_bytes()[idx]);
153                    return Some((matching, idx));
154                }
155            } else if *b == bracket {
156                unmatched += 1;
157            }
158            idx += 1;
159        }
160        debug_assert_eq!(idx, line.len());
161    } else {
162        // backward search
163        idx = pos;
164        let bytes = &line.as_bytes()[..idx];
165        for b in bytes.iter().rev() {
166            if *b == matching {
167                unmatched -= 1;
168                if unmatched == 0 {
169                    debug_assert_eq!(matching, line.as_bytes()[idx - 1]);
170                    return Some((matching, idx - 1));
171                }
172            } else if *b == bracket {
173                unmatched += 1;
174            }
175            idx -= 1;
176        }
177        debug_assert_eq!(idx, 0);
178    }
179    None
180}
181
182// check under or before the cursor
183fn check_bracket(line: &str, pos: usize) -> Option<(u8, usize)> {
184    if line.is_empty() {
185        return None;
186    }
187    let mut pos = pos;
188    if pos >= line.len() {
189        pos = line.len() - 1; // before cursor
190        let b = line.as_bytes()[pos]; // previous byte
191        if is_close_bracket(b) {
192            Some((b, pos))
193        } else {
194            None
195        }
196    } else {
197        let mut under_cursor = true;
198        loop {
199            let b = line.as_bytes()[pos];
200            if is_close_bracket(b) {
201                return if pos == 0 { None } else { Some((b, pos)) };
202            } else if is_open_bracket(b) {
203                return if pos + 1 == line.len() {
204                    None
205                } else {
206                    Some((b, pos))
207                };
208            } else if under_cursor && pos > 0 {
209                under_cursor = false;
210                pos -= 1; // or before cursor
211            } else {
212                return None;
213            }
214        }
215    }
216}
217
218const fn matching_bracket(bracket: u8) -> u8 {
219    match bracket {
220        b'{' => b'}',
221        b'}' => b'{',
222        b'[' => b']',
223        b']' => b'[',
224        b'(' => b')',
225        b')' => b'(',
226        b => b,
227    }
228}
229const fn is_open_bracket(bracket: u8) -> bool {
230    matches!(bracket, b'{' | b'[' | b'(')
231}
232const fn is_close_bracket(bracket: u8) -> bool {
233    matches!(bracket, b'}' | b']' | b')')
234}
235
236#[cfg(test)]
237mod tests {
238    #[test]
239    pub fn find_matching_bracket() {
240        use super::find_matching_bracket;
241        assert_eq!(find_matching_bracket("(...", 0, b'('), None);
242        assert_eq!(find_matching_bracket("...)", 3, b')'), None);
243
244        assert_eq!(find_matching_bracket("()..", 0, b'('), Some((b')', 1)));
245        assert_eq!(find_matching_bracket("(..)", 0, b'('), Some((b')', 3)));
246
247        assert_eq!(find_matching_bracket("..()", 3, b')'), Some((b'(', 2)));
248        assert_eq!(find_matching_bracket("(..)", 3, b')'), Some((b'(', 0)));
249
250        assert_eq!(find_matching_bracket("(())", 0, b'('), Some((b')', 3)));
251        assert_eq!(find_matching_bracket("(())", 3, b')'), Some((b'(', 0)));
252    }
253    #[test]
254    pub fn check_bracket() {
255        use super::check_bracket;
256        assert_eq!(check_bracket(")...", 0), None);
257        assert_eq!(check_bracket("(...", 2), None);
258        assert_eq!(check_bracket("...(", 3), None);
259        assert_eq!(check_bracket("...(", 4), None);
260        assert_eq!(check_bracket("..).", 4), None);
261
262        assert_eq!(check_bracket("(...", 0), Some((b'(', 0)));
263        assert_eq!(check_bracket("(...", 1), Some((b'(', 0)));
264        assert_eq!(check_bracket("...)", 3), Some((b')', 3)));
265        assert_eq!(check_bracket("...)", 4), Some((b')', 3)));
266    }
267    #[test]
268    pub fn matching_bracket() {
269        use super::matching_bracket;
270        assert_eq!(matching_bracket(b'('), b')');
271        assert_eq!(matching_bracket(b')'), b'(');
272    }
273
274    #[test]
275    pub fn is_open_bracket() {
276        use super::is_close_bracket;
277        use super::is_open_bracket;
278        assert!(is_open_bracket(b'('));
279        assert!(is_close_bracket(b')'));
280    }
281}