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