rustyline/
highlight.rs

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