char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
5.6 KiB177 linesraw
1use std::borrow::Cow;
2use std::cell::RefCell;
3
4use nu_ansi_term::{Color as AnsiColor, Style};
5use reedline::{
6    Color, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, StyledText,
7    ValidationResult,
8};
9use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent};
10
11pub(super) fn create() -> Reedline {
12    Reedline::create()
13        .with_highlighter(Box::new(Highlighter::new()))
14        .with_validator(Box::new(Validator))
15}
16
17pub(super) struct Prompt;
18
19impl reedline::Prompt for Prompt {
20    fn render_prompt_left(&self) -> Cow<'_, str> {
21        Cow::Borrowed("")
22    }
23
24    fn render_prompt_right(&self) -> Cow<'_, str> {
25        Cow::Borrowed("")
26    }
27
28    fn render_prompt_indicator(&self, _mode: PromptEditMode) -> Cow<'_, str> {
29        Cow::Borrowed("> ")
30    }
31
32    fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
33        Cow::Borrowed("… ")
34    }
35
36    fn render_prompt_history_search_indicator(&self, search: PromptHistorySearch) -> Cow<'_, str> {
37        let failing = match search.status {
38            PromptHistorySearchStatus::Passing => "",
39            PromptHistorySearchStatus::Failing => "failing ",
40        };
41        Cow::Owned(format!("({failing}reverse-search: {}) ", search.term))
42    }
43
44    fn get_indicator_color(&self) -> Color {
45        Color::Green
46    }
47
48    fn get_prompt_multiline_color(&self) -> AnsiColor {
49        AnsiColor::DarkGray
50    }
51}
52
53struct Validator;
54
55impl reedline::Validator for Validator {
56    fn validate(&self, line: &str) -> ValidationResult {
57        if super::is_incomplete(&format!("{line}\n")) {
58            ValidationResult::Incomplete
59        } else {
60            ValidationResult::Complete
61        }
62    }
63}
64
65struct Highlighter {
66    config: HighlightConfiguration,
67    styles: Vec<Style>,
68    highlighter: RefCell<tree_sitter_highlight::Highlighter>,
69}
70
71impl Highlighter {
72    fn new() -> Self {
73        // Plain ANSI colors so the terminal's own palette decides how they look.
74        let theme = [
75            ("comment", Style::new().fg(AnsiColor::DarkGray)),
76            ("keyword", Style::new().fg(AnsiColor::Purple)),
77            ("string", Style::new().fg(AnsiColor::Green)),
78            ("string.special", Style::new().fg(AnsiColor::Red)),
79            ("number", Style::new().fg(AnsiColor::Yellow)),
80            ("constant.builtin", Style::new().fg(AnsiColor::Yellow)),
81            ("function", Style::new().fg(AnsiColor::Blue)),
82            ("constructor", Style::new().fg(AnsiColor::Cyan)),
83            ("variable.builtin", Style::new().fg(AnsiColor::Red)),
84            ("embedded", Style::new()),
85        ];
86        let mut config = HighlightConfiguration::new(
87            tree_sitter_javascript::LANGUAGE.into(),
88            "javascript",
89            tree_sitter_javascript::HIGHLIGHT_QUERY,
90            "",
91            tree_sitter_javascript::LOCALS_QUERY,
92        )
93        .expect("bundled JavaScript highlight queries are valid");
94        config.configure(&theme.map(|(name, _)| name));
95        Self {
96            config,
97            styles: theme.map(|(_, style)| style).to_vec(),
98            highlighter: RefCell::new(tree_sitter_highlight::Highlighter::new()),
99        }
100    }
101}
102
103impl reedline::Highlighter for Highlighter {
104    fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
105        let mut text = StyledText::new();
106        let mut highlighter = self.highlighter.borrow_mut();
107        let events = highlighter
108            .highlight(&self.config, line.as_bytes(), None, None, |_| None)
109            .and_then(|events| events.collect::<Result<Vec<_>, _>>());
110        let Ok(events) = events else {
111            text.push((Style::new(), line.to_owned()));
112            return text;
113        };
114        let mut styles = Vec::new();
115        for event in events {
116            match event {
117                HighlightEvent::HighlightStart(Highlight(index)) => {
118                    styles.push(self.styles[index]);
119                }
120                HighlightEvent::HighlightEnd => {
121                    styles.pop();
122                }
123                HighlightEvent::Source { start, end } => {
124                    let style = styles.last().copied().unwrap_or_default();
125                    text.push((style, line[start..end].to_owned()));
126                }
127            }
128        }
129        text
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::Highlighter;
136    use nu_ansi_term::Style;
137    use reedline::Highlighter as _;
138
139    fn style_of(line: &str, fragment: &str) -> Style {
140        let text = Highlighter::new().highlight(line, 0);
141        text.buffer
142            .iter()
143            .find(|(_, text)| text.contains(fragment))
144            .unwrap_or_else(|| panic!("{fragment:?} is not in {line:?}"))
145            .0
146    }
147
148    #[test]
149    fn highlighting_preserves_partial_and_unicode_input() {
150        let highlighter = Highlighter::new();
151        for line in [
152            "",
153            "const café = \"☕\" +",
154            "function greet() {\n  return `hi ${name",
155            "/* unterminated",
156            "import * as TID from \"jsr:@atcute/tid\"",
157            ")}]",
158        ] {
159            assert_eq!(highlighter.highlight(line, 0).raw_string(), line);
160        }
161    }
162
163    #[test]
164    fn literals_and_keywords_are_distinguished_from_identifiers() {
165        let line = "const answer = \"forty\" + 2";
166        let plain = style_of(line, "answer");
167        assert_ne!(style_of(line, "const"), plain);
168        assert_ne!(style_of(line, "\"forty\""), plain);
169        assert_ne!(style_of(line, "2"), plain);
170    }
171
172    #[test]
173    fn template_substitutions_are_not_styled_as_strings() {
174        let line = "`total: ${count}`";
175        assert_ne!(style_of(line, "count"), style_of(line, "`total: "));
176    }
177}