Skip to main content

shannon_nu_cli/
bash_highlight.rs

1use nu_ansi_term::{Color, Style};
2use nu_color_config::get_shape_color;
3use nu_protocol::Config;
4use reedline::{Highlighter, StyledText};
5use tree_sitter::{Node, Parser};
6
7/// Syntax highlighter for bash using tree-sitter-bash.
8/// Colors are read from nushell's color_config so bash highlighting
9/// matches the user's nushell theme.
10pub struct BashHighlighter {
11    keyword: Style,
12    command: Style,
13    string: Style,
14    number: Style,
15    variable: Style,
16    operator: Style,
17    comment: Style,
18    foreground: Style,
19}
20
21impl BashHighlighter {
22    pub fn new(config: &Config) -> Self {
23        BashHighlighter {
24            keyword: get_shape_color("shape_keyword", config),
25            command: get_shape_color("shape_external", config),
26            string: get_shape_color("shape_string", config),
27            number: get_shape_color("shape_int", config),
28            variable: get_shape_color("shape_variable", config),
29            operator: get_shape_color("shape_operator", config),
30            comment: Style::new().fg(Color::DarkGray),
31            foreground: Style::default(),
32        }
33    }
34
35    fn bash_style(&self, node: &Node) -> Style {
36        match node.kind() {
37            "if" | "then" | "else" | "elif" | "fi" | "for" | "in" | "do" | "done" | "while"
38            | "until" | "case" | "esac" | "function" | "export" | "declare" | "local"
39            | "return" | "select" => self.keyword,
40
41            "command_name" => self.command,
42
43            "string" | "raw_string" | "heredoc_body" | "string_content" | "ansii_c_string" => {
44                self.string
45            }
46
47            "number" => self.number,
48
49            "variable_name" | "special_variable_name" => self.variable,
50            "simple_expansion" | "expansion" => self.variable,
51            "$" => self.variable,
52
53            "|" | ">" | ">>" | "<" | "<<" | "&&" | "||" | ";" | ";;" | "&" => self.operator,
54            "test_operator" => self.operator,
55
56            "comment" => self.comment,
57
58            _ => self.foreground,
59        }
60    }
61}
62
63impl Highlighter for BashHighlighter {
64    fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
65        let mut styled = StyledText::new();
66
67        if line.is_empty() {
68            return styled;
69        }
70
71        let language: tree_sitter::Language = tree_sitter_bash::LANGUAGE.into();
72        let mut parser = Parser::new();
73        if parser.set_language(&language).is_err() {
74            styled.push((self.foreground, line.to_string()));
75            return styled;
76        }
77
78        let tree = match parser.parse(line, None) {
79            Some(tree) => tree,
80            None => {
81                styled.push((self.foreground, line.to_string()));
82                return styled;
83            }
84        };
85
86        let mut segments: Vec<(usize, usize, Style)> = Vec::new();
87        collect_leaf_styles(&tree.root_node(), self, &mut segments);
88
89        segments.sort_by_key(|s| s.0);
90
91        let mut pos = 0;
92        for (start, end, style) in &segments {
93            let start = *start;
94            let end = (*end).min(line.len());
95            if start > pos {
96                styled.push((self.foreground, line[pos..start].to_string()));
97            }
98            if start >= pos && end > start {
99                styled.push((*style, line[start..end].to_string()));
100                pos = end;
101            }
102        }
103        if pos < line.len() {
104            styled.push((self.foreground, line[pos..].to_string()));
105        }
106
107        styled
108    }
109}
110
111fn collect_leaf_styles(
112    node: &Node,
113    highlighter: &BashHighlighter,
114    segments: &mut Vec<(usize, usize, Style)>,
115) {
116    if node.child_count() == 0 {
117        let start = node.start_byte();
118        let end = node.end_byte();
119        let style = highlighter.bash_style(node);
120        segments.push((start, end, style));
121    } else {
122        let parent_style = match node.kind() {
123            "command_name" => Some(highlighter.command),
124            "simple_expansion" | "expansion" => Some(highlighter.variable),
125            "string" => Some(highlighter.string),
126            _ => None,
127        };
128
129        if let Some(style) = parent_style {
130            segments.push((node.start_byte(), node.end_byte(), style));
131        } else {
132            for child in node.children(&mut node.walk()) {
133                collect_leaf_styles(&child, highlighter, segments);
134            }
135        }
136    }
137}