1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
use std::collections::HashSet;
use std::env;
use std::fs;
use std::ops::Range;
use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use std::sync::Mutex;
use lineread::highlighting::{Highlighter, Style};
use crate::libs::prefix;
use crate::parsers::parser_line;
use crate::shell;
use crate::tools;
#[derive(Clone)]
pub struct CicadaHighlighter;
const GREEN: &str = "\x1b[0;32m";
lazy_static! {
static ref AVAILABLE_COMMANDS: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
static ref ALIASES: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
}
/// Initialize the available commands cache by scanning PATH directories
pub fn init_command_cache() {
let commands = scan_available_commands();
if let Ok(mut cache) = AVAILABLE_COMMANDS.lock() {
*cache = commands;
}
}
/// Update aliases in the highlighter's cache
pub fn update_aliases(sh: &shell::Shell) {
if let Ok(mut aliases) = ALIASES.lock() {
aliases.clear();
for alias_name in sh.aliases.keys() {
aliases.insert(alias_name.clone());
}
}
}
fn scan_available_commands() -> HashSet<String> {
let mut commands = HashSet::new();
if let Ok(path_var) = env::var("PATH") {
for dir_path in env::split_paths(&path_var) {
if !dir_path.is_dir() {
continue;
}
if let Ok(entries) = fs::read_dir(dir_path) {
for entry in entries.filter_map(Result::ok) {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() || file_type.is_symlink() {
if let Ok(metadata) = entry.metadata() {
// Check if file is executable
if metadata.permissions().mode() & 0o111 != 0 {
if let Some(name) = entry.file_name().to_str() {
commands.insert(name.to_string());
}
}
}
}
}
}
}
}
}
commands
}
fn is_command(word: &str) -> bool {
if tools::is_builtin(word) {
return true;
}
if let Ok(aliases) = ALIASES.lock() {
if aliases.contains(word) {
return true;
}
}
if let Ok(commands) = AVAILABLE_COMMANDS.lock() {
if commands.contains(word) {
return true;
}
}
false
}
fn find_token_range_heuristic(
line: &str,
start_byte: usize,
token: &(String, String),
) -> Option<Range<usize>> {
let (sep, word) = token;
// Find the start of the token, skipping leading whitespace from the search start position
let mut search_area = &line[start_byte..];
let token_start_byte =
if let Some(non_ws_offset) = search_area.find(|c: char| !c.is_whitespace()) {
// Calculate the actual byte index of the first non-whitespace character
start_byte
+ search_area
.char_indices()
.nth(non_ws_offset)
.map_or(0, |(idx, _)| idx)
} else {
return None; // Only whitespace left
};
search_area = &line[token_start_byte..];
// Estimate the end byte based on the token structure
let mut estimated_len = 0;
let mut current_search_offset = 0;
// Match separator prefix if needed (e.g., `"` or `'`)
if !sep.is_empty() && search_area.starts_with(sep) {
estimated_len += sep.len();
current_search_offset += sep.len();
}
// Match the word content
// Use starts_with for a basic check, assuming the word appears next
if search_area[current_search_offset..].starts_with(word) {
estimated_len += word.len();
current_search_offset += word.len();
// Match separator suffix if needed
if !sep.is_empty() && search_area[current_search_offset..].starts_with(sep) {
estimated_len += sep.len();
}
Some(token_start_byte..(token_start_byte + estimated_len))
} else if word.is_empty()
&& !sep.is_empty()
&& search_area.starts_with(sep)
&& search_area[sep.len()..].starts_with(sep)
{
// Handle empty quoted string like "" or ''
estimated_len += sep.len() * 2;
Some(token_start_byte..(token_start_byte + estimated_len))
} else {
// Fallback: Maybe it's just the word without quotes, or a separator like `|`
if search_area.starts_with(word) {
Some(token_start_byte..(token_start_byte + word.len()))
} else {
// Could not reliably map the token back to the original string segment
// This might happen with complex escapes or parser ambiguities
// As a basic fallback, consume up to the next space or end of line? Unsafe.
// Return None to signal failure for this token.
None
}
}
}
impl Highlighter for CicadaHighlighter {
fn highlight(&self, line: &str) -> Vec<(Range<usize>, Style)> {
let mut styles = Vec::new();
if line.is_empty() {
return styles;
}
let line_info = parser_line::parse_line(line);
if line_info.tokens.is_empty() {
// If parser returns no tokens, style whole line as default
styles.push((0..line.len(), Style::Default));
return styles;
}
let mut current_byte_idx = 0;
let mut expect_command = true;
let mut after_wrapper = false;
for token in &line_info.tokens {
// Find the range in the original line for this token
match find_token_range_heuristic(line, current_byte_idx, token) {
Some(token_range) => {
// Style potential whitespace before the token
if token_range.start > current_byte_idx {
styles.push((current_byte_idx..token_range.start, Style::Default));
}
let (_sep, word) = token;
let mut current_token_style = Style::Default;
if expect_command && !word.is_empty() {
if prefix::is_env_assignment(word) {
// Environment variable assignment like FOO=bar
// Keep expecting command, don't change state
} else if is_command(word) {
current_token_style = Style::AnsiColor(GREEN.to_string());
if prefix::is_wrapper_command(word) {
after_wrapper = true;
} else {
expect_command = false;
after_wrapper = false;
}
} else if !after_wrapper {
// First token wasn't a command or assignment — stop expecting
expect_command = false;
}
// else: after wrapper, not a command yet → keep looking
}
styles.push((token_range.clone(), current_token_style));
// Check if this token marks the end of a command segment
if ["|", "&&", "||", ";"].contains(&word.as_str()) {
expect_command = true;
after_wrapper = false;
}
current_byte_idx = token_range.end;
}
None => {
// If we can't map a token, style the rest of the line as default and stop.
if current_byte_idx < line.len() {
styles.push((current_byte_idx..line.len(), Style::Default));
}
current_byte_idx = line.len(); // Mark as done
break; // Stop processing further tokens
}
}
}
// Style any remaining characters after the last processed token
if current_byte_idx < line.len() {
styles.push((current_byte_idx..line.len(), Style::Default));
}
styles
}
}
pub fn create_highlighter() -> Arc<CicadaHighlighter> {
Arc::new(CicadaHighlighter)
}