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