shannon_nu_cli/
bash_highlight.rs1use 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
7pub 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}