1use log::trace;
2use nu_ansi_term::Style;
3use nu_color_config::{get_matching_brackets_style, get_shape_color};
4use nu_engine::env;
5use nu_parser::{flatten_block, parse, FlatShape};
6use nu_protocol::{
7 ast::{Block, Expr, Expression, PipelineRedirection, RecordItem},
8 engine::{EngineState, Stack, StateWorkingSet},
9 Span,
10};
11use reedline::{Highlighter, StyledText};
12use std::sync::Arc;
13
14pub struct NuHighlighter {
15 pub engine_state: Arc<EngineState>,
16 pub stack: Arc<Stack>,
17}
18
19impl Highlighter for NuHighlighter {
20 fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
21 trace!("highlighting: {}", line);
22
23 let config = self.stack.get_config(&self.engine_state);
24 let highlight_resolved_externals = config.highlight_resolved_externals;
25 let mut working_set = StateWorkingSet::new(&self.engine_state);
26 let block = parse(&mut working_set, None, line.as_bytes(), false);
27 let (shapes, global_span_offset) = {
28 let mut shapes = flatten_block(&working_set, &block);
29 if highlight_resolved_externals {
32 for (span, shape) in shapes.iter_mut() {
33 if *shape == FlatShape::External {
34 let str_contents =
35 working_set.get_span_contents(Span::new(span.start, span.end));
36
37 let str_word = String::from_utf8_lossy(str_contents).to_string();
38 let paths = env::path_str(&self.engine_state, &self.stack, *span).ok();
39 #[allow(deprecated)]
40 let res = if let Ok(cwd) =
41 env::current_dir_str(&self.engine_state, &self.stack)
42 {
43 which::which_in(str_word, paths.as_ref(), cwd).ok()
44 } else {
45 which::which_in_global(str_word, paths.as_ref())
46 .ok()
47 .and_then(|mut i| i.next())
48 };
49 if res.is_some() {
50 *shape = FlatShape::ExternalResolved;
51 }
52 }
53 }
54 }
55 (shapes, self.engine_state.next_span_start())
56 };
57
58 let mut output = StyledText::default();
59 let mut last_seen_span = global_span_offset;
60
61 let global_cursor_offset = _cursor + global_span_offset;
62 let matching_brackets_pos = find_matching_brackets(
63 line,
64 &working_set,
65 &block,
66 global_span_offset,
67 global_cursor_offset,
68 );
69
70 for shape in &shapes {
71 if shape.0.end <= last_seen_span
72 || last_seen_span < global_span_offset
73 || shape.0.start < global_span_offset
74 {
75 continue;
78 }
79 if shape.0.start > last_seen_span {
80 let gap = line
81 [(last_seen_span - global_span_offset)..(shape.0.start - global_span_offset)]
82 .to_string();
83 output.push((Style::new(), gap));
84 }
85 let next_token = line
86 [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)]
87 .to_string();
88
89 let mut add_colored_token = |shape: &FlatShape, text: String| {
90 output.push((get_shape_color(shape.as_str(), &config), text));
91 };
92
93 match shape.1 {
94 FlatShape::Garbage => add_colored_token(&shape.1, next_token),
95 FlatShape::Nothing => add_colored_token(&shape.1, next_token),
96 FlatShape::Binary => add_colored_token(&shape.1, next_token),
97 FlatShape::Bool => add_colored_token(&shape.1, next_token),
98 FlatShape::Int => add_colored_token(&shape.1, next_token),
99 FlatShape::Float => add_colored_token(&shape.1, next_token),
100 FlatShape::Range => add_colored_token(&shape.1, next_token),
101 FlatShape::InternalCall(_) => add_colored_token(&shape.1, next_token),
102 FlatShape::External => add_colored_token(&shape.1, next_token),
103 FlatShape::ExternalArg => add_colored_token(&shape.1, next_token),
104 FlatShape::ExternalResolved => add_colored_token(&shape.1, next_token),
105 FlatShape::Keyword => add_colored_token(&shape.1, next_token),
106 FlatShape::Literal => add_colored_token(&shape.1, next_token),
107 FlatShape::Operator => add_colored_token(&shape.1, next_token),
108 FlatShape::Signature => add_colored_token(&shape.1, next_token),
109 FlatShape::String => add_colored_token(&shape.1, next_token),
110 FlatShape::RawString => add_colored_token(&shape.1, next_token),
111 FlatShape::StringInterpolation => add_colored_token(&shape.1, next_token),
112 FlatShape::DateTime => add_colored_token(&shape.1, next_token),
113 FlatShape::List
114 | FlatShape::Table
115 | FlatShape::Record
116 | FlatShape::Block
117 | FlatShape::Closure => {
118 let span = shape.0;
119 let shape = &shape.1;
120 let spans = split_span_by_highlight_positions(
121 line,
122 span,
123 &matching_brackets_pos,
124 global_span_offset,
125 );
126 for (part, highlight) in spans {
127 let start = part.start - span.start;
128 let end = part.end - span.start;
129 let text = next_token[start..end].to_string();
130 let mut style = get_shape_color(shape.as_str(), &config);
131 if highlight {
132 style = get_matching_brackets_style(style, &config);
133 }
134 output.push((style, text));
135 }
136 }
137
138 FlatShape::Filepath => add_colored_token(&shape.1, next_token),
139 FlatShape::Directory => add_colored_token(&shape.1, next_token),
140 FlatShape::GlobInterpolation => add_colored_token(&shape.1, next_token),
141 FlatShape::GlobPattern => add_colored_token(&shape.1, next_token),
142 FlatShape::Variable(_) | FlatShape::VarDecl(_) => {
143 add_colored_token(&shape.1, next_token)
144 }
145 FlatShape::Flag => add_colored_token(&shape.1, next_token),
146 FlatShape::Pipe => add_colored_token(&shape.1, next_token),
147 FlatShape::Redirection => add_colored_token(&shape.1, next_token),
148 FlatShape::Custom(..) => add_colored_token(&shape.1, next_token),
149 FlatShape::MatchPattern => add_colored_token(&shape.1, next_token),
150 }
151 last_seen_span = shape.0.end;
152 }
153
154 let remainder = line[(last_seen_span - global_span_offset)..].to_string();
155 if !remainder.is_empty() {
156 output.push((Style::new(), remainder));
157 }
158
159 output
160 }
161}
162
163fn split_span_by_highlight_positions(
164 line: &str,
165 span: Span,
166 highlight_positions: &[usize],
167 global_span_offset: usize,
168) -> Vec<(Span, bool)> {
169 let mut start = span.start;
170 let mut result: Vec<(Span, bool)> = Vec::new();
171 for pos in highlight_positions {
172 if start <= *pos && pos < &span.end {
173 if start < *pos {
174 result.push((Span::new(start, *pos), false));
175 }
176 let span_str = &line[pos - global_span_offset..span.end - global_span_offset];
177 let end = span_str
178 .chars()
179 .next()
180 .map(|c| pos + get_char_length(c))
181 .unwrap_or(pos + 1);
182 result.push((Span::new(*pos, end), true));
183 start = end;
184 }
185 }
186 if start < span.end {
187 result.push((Span::new(start, span.end), false));
188 }
189 result
190}
191
192fn find_matching_brackets(
193 line: &str,
194 working_set: &StateWorkingSet,
195 block: &Block,
196 global_span_offset: usize,
197 global_cursor_offset: usize,
198) -> Vec<usize> {
199 const BRACKETS: &str = "{}[]()";
200
201 let global_end_offset = line.len() + global_span_offset;
203 let global_bracket_pos =
204 if global_cursor_offset == global_end_offset && global_end_offset > global_span_offset {
205 if let Some(last_char) = line.chars().last() {
207 global_cursor_offset - get_char_length(last_char)
208 } else {
209 global_cursor_offset
210 }
211 } else {
212 global_cursor_offset
214 };
215
216 let match_idx = global_bracket_pos - global_span_offset;
218 if match_idx >= line.len()
219 || !BRACKETS.contains(get_char_at_index(line, match_idx).unwrap_or_default())
220 {
221 return Vec::new();
222 }
223
224 let matching_block_end = find_matching_block_end_in_block(
226 line,
227 working_set,
228 block,
229 global_span_offset,
230 global_bracket_pos,
231 );
232 if let Some(pos) = matching_block_end {
233 let matching_idx = pos - global_span_offset;
234 if BRACKETS.contains(get_char_at_index(line, matching_idx).unwrap_or_default()) {
235 return if global_bracket_pos < pos {
236 vec![global_bracket_pos, pos]
237 } else {
238 vec![pos, global_bracket_pos]
239 };
240 }
241 }
242 Vec::new()
243}
244
245fn find_matching_block_end_in_block(
246 line: &str,
247 working_set: &StateWorkingSet,
248 block: &Block,
249 global_span_offset: usize,
250 global_cursor_offset: usize,
251) -> Option<usize> {
252 for p in &block.pipelines {
253 for e in &p.elements {
254 if e.expr.span.contains(global_cursor_offset) {
255 if let Some(pos) = find_matching_block_end_in_expr(
256 line,
257 working_set,
258 &e.expr,
259 global_span_offset,
260 global_cursor_offset,
261 ) {
262 return Some(pos);
263 }
264 }
265
266 if let Some(redirection) = e.redirection.as_ref() {
267 match redirection {
268 PipelineRedirection::Single { target, .. }
269 | PipelineRedirection::Separate { out: target, .. }
270 | PipelineRedirection::Separate { err: target, .. }
271 if target.span().contains(global_cursor_offset) =>
272 {
273 if let Some(pos) = target.expr().and_then(|expr| {
274 find_matching_block_end_in_expr(
275 line,
276 working_set,
277 expr,
278 global_span_offset,
279 global_cursor_offset,
280 )
281 }) {
282 return Some(pos);
283 }
284 }
285 _ => {}
286 }
287 }
288 }
289 }
290 None
291}
292
293fn find_matching_block_end_in_expr(
294 line: &str,
295 working_set: &StateWorkingSet,
296 expression: &Expression,
297 global_span_offset: usize,
298 global_cursor_offset: usize,
299) -> Option<usize> {
300 if expression.span.contains(global_cursor_offset) && expression.span.start >= global_span_offset
301 {
302 let expr_first = expression.span.start;
303 let span_str = &line
304 [expression.span.start - global_span_offset..expression.span.end - global_span_offset];
305 let expr_last = span_str
306 .chars()
307 .last()
308 .map(|c| expression.span.end - get_char_length(c))
309 .unwrap_or(expression.span.start);
310
311 return match &expression.expr {
312 Expr::Bool(_) => None,
314 Expr::Int(_) => None,
315 Expr::Float(_) => None,
316 Expr::Binary(_) => None,
317 Expr::Range(..) => None,
318 Expr::Var(_) => None,
319 Expr::VarDecl(_) => None,
320 Expr::ExternalCall(..) => None,
321 Expr::Operator(_) => None,
322 Expr::UnaryNot(_) => None,
323 Expr::Keyword(..) => None,
324 Expr::ValueWithUnit(..) => None,
325 Expr::DateTime(_) => None,
326 Expr::Filepath(_, _) => None,
327 Expr::Directory(_, _) => None,
328 Expr::GlobPattern(_, _) => None,
329 Expr::String(_) => None,
330 Expr::RawString(_) => None,
331 Expr::CellPath(_) => None,
332 Expr::ImportPattern(_) => None,
333 Expr::Overlay(_) => None,
334 Expr::Signature(_) => None,
335 Expr::MatchBlock(_) => None,
336 Expr::Nothing => None,
337 Expr::Garbage => None,
338
339 Expr::AttributeBlock(ab) => ab
340 .attributes
341 .iter()
342 .find_map(|attr| {
343 find_matching_block_end_in_expr(
344 line,
345 working_set,
346 &attr.expr,
347 global_span_offset,
348 global_cursor_offset,
349 )
350 })
351 .or_else(|| {
352 find_matching_block_end_in_expr(
353 line,
354 working_set,
355 &ab.item,
356 global_span_offset,
357 global_cursor_offset,
358 )
359 }),
360
361 Expr::Table(table) => {
362 if expr_last == global_cursor_offset {
363 Some(expr_first)
365 } else if expr_first == global_cursor_offset {
366 Some(expr_last)
368 } else {
369 table
371 .columns
372 .iter()
373 .chain(table.rows.iter().flat_map(AsRef::as_ref))
374 .find_map(|expr| {
375 find_matching_block_end_in_expr(
376 line,
377 working_set,
378 expr,
379 global_span_offset,
380 global_cursor_offset,
381 )
382 })
383 }
384 }
385
386 Expr::Record(exprs) => {
387 if expr_last == global_cursor_offset {
388 Some(expr_first)
390 } else if expr_first == global_cursor_offset {
391 Some(expr_last)
393 } else {
394 exprs.iter().find_map(|expr| match expr {
396 RecordItem::Pair(k, v) => find_matching_block_end_in_expr(
397 line,
398 working_set,
399 k,
400 global_span_offset,
401 global_cursor_offset,
402 )
403 .or_else(|| {
404 find_matching_block_end_in_expr(
405 line,
406 working_set,
407 v,
408 global_span_offset,
409 global_cursor_offset,
410 )
411 }),
412 RecordItem::Spread(_, record) => find_matching_block_end_in_expr(
413 line,
414 working_set,
415 record,
416 global_span_offset,
417 global_cursor_offset,
418 ),
419 })
420 }
421 }
422
423 Expr::Call(call) => call.arguments.iter().find_map(|arg| {
424 arg.expr().and_then(|expr| {
425 find_matching_block_end_in_expr(
426 line,
427 working_set,
428 expr,
429 global_span_offset,
430 global_cursor_offset,
431 )
432 })
433 }),
434
435 Expr::FullCellPath(b) => find_matching_block_end_in_expr(
436 line,
437 working_set,
438 &b.head,
439 global_span_offset,
440 global_cursor_offset,
441 ),
442
443 Expr::BinaryOp(lhs, op, rhs) => [lhs, op, rhs].into_iter().find_map(|expr| {
444 find_matching_block_end_in_expr(
445 line,
446 working_set,
447 expr,
448 global_span_offset,
449 global_cursor_offset,
450 )
451 }),
452
453 Expr::Collect(_, expr) => find_matching_block_end_in_expr(
454 line,
455 working_set,
456 expr,
457 global_span_offset,
458 global_cursor_offset,
459 ),
460
461 Expr::Block(block_id)
462 | Expr::Closure(block_id)
463 | Expr::RowCondition(block_id)
464 | Expr::Subexpression(block_id) => {
465 if expr_last == global_cursor_offset {
466 Some(expr_first)
468 } else if expr_first == global_cursor_offset {
469 Some(expr_last)
471 } else {
472 let nested_block = working_set.get_block(*block_id);
474 find_matching_block_end_in_block(
475 line,
476 working_set,
477 nested_block,
478 global_span_offset,
479 global_cursor_offset,
480 )
481 }
482 }
483
484 Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
485 exprs.iter().find_map(|expr| {
486 find_matching_block_end_in_expr(
487 line,
488 working_set,
489 expr,
490 global_span_offset,
491 global_cursor_offset,
492 )
493 })
494 }
495
496 Expr::List(list) => {
497 if expr_last == global_cursor_offset {
498 Some(expr_first)
500 } else if expr_first == global_cursor_offset {
501 Some(expr_last)
503 } else {
504 list.iter().find_map(|item| {
505 find_matching_block_end_in_expr(
506 line,
507 working_set,
508 item.expr(),
509 global_span_offset,
510 global_cursor_offset,
511 )
512 })
513 }
514 }
515 };
516 }
517 None
518}
519
520fn get_char_at_index(s: &str, index: usize) -> Option<char> {
521 s[index..].chars().next()
522}
523
524fn get_char_length(c: char) -> usize {
525 c.to_string().len()
526}