nu_cli/
syntax_highlight.rs

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/// Result of a syntax highlight operation
27#[derive(Default)]
28pub(crate) struct HighlightResult {
29    /// The highlighted text
30    pub(crate) text: StyledText,
31    /// The span of any garbage that was highlighted
32    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        // Highlighting externals has a config point because of concerns that using which to resolve
50        // externals may slow down things too much.
51        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            // We've already output something for this span
93            // so just skip this one
94            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    // calculate first bracket position
228    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            // cursor is at the end of a non-empty string -- find block end at the previous position
232            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            // cursor is in the middle of a string -- find block end at the current position
239            global_cursor_offset
240        };
241
242    // check that position contains bracket
243    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    // find matching bracket by finding matching block end
251    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            // TODO: Can't these be handled with an `_ => None` branch? Refactor
339            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                    // cursor is at table end
390                    Some(expr_first)
391                } else if expr_first == global_cursor_offset {
392                    // cursor is at table start
393                    Some(expr_last)
394                } else {
395                    // cursor is inside table
396                    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                    // cursor is at record end
415                    Some(expr_first)
416                } else if expr_first == global_cursor_offset {
417                    // cursor is at record start
418                    Some(expr_last)
419                } else {
420                    // cursor is inside record
421                    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                    // cursor is at block end
493                    Some(expr_first)
494                } else if expr_first == global_cursor_offset {
495                    // cursor is at block start
496                    Some(expr_last)
497                } else {
498                    // cursor is inside block
499                    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                    // cursor is at list end
525                    Some(expr_first)
526                } else if expr_first == global_cursor_offset {
527                    // cursor is at list start
528                    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}