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