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// <<<<<<< HEAD
27// =======
28//         let config = self.stack.get_config(&self.engine_state);
29//         let highlight_resolved_externals = config.highlight_resolved_externals;
30//         let mut working_set = StateWorkingSet::new(&self.engine_state);
31//         let block = parse(&mut working_set, None, line.as_bytes(), false);
32//         let (shapes, global_span_offset) = {
33//             let mut shapes = flatten_block(&working_set, &block);
34//             // Highlighting externals has a config point because of concerns that using which to resolve
35//             // externals may slow down things too much.
36//             if highlight_resolved_externals {
37//                 for (span, shape) in shapes.iter_mut() {
38//                     if let FlatShape::External(aliased_command_span) = *shape {
39//                         let str_contents = working_set.get_span_contents(aliased_command_span);
40// >>>>>>> df798b657 (Fix highlighting of aliases to external commands)
41
42/// Result of a syntax highlight operation
43#[derive(Default)]
44pub(crate) struct HighlightResult {
45    /// The highlighted text
46    pub(crate) text: StyledText,
47    /// The span of any garbage that was highlighted
48    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        // Highlighting externals has a config point because of concerns that using which to resolve
66        // externals may slow down things too much.
67        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            // We've already output something for this span
107            // so just skip this one
108            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    // calculate first bracket position
242    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            // cursor is at the end of a non-empty string -- find block end at the previous position
246            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            // cursor is in the middle of a string -- find block end at the current position
253            global_cursor_offset
254        };
255
256    // check that position contains bracket
257    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    // find matching bracket by finding matching block end
265    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                && 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            {
303                return Some(pos);
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            // TODO: Can't these be handled with an `_ => None` branch? Refactor
353            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                    // cursor is at table end
404                    Some(expr_first)
405                } else if expr_first == global_cursor_offset {
406                    // cursor is at table start
407                    Some(expr_last)
408                } else {
409                    // cursor is inside table
410                    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                    // cursor is at record end
429                    Some(expr_first)
430                } else if expr_first == global_cursor_offset {
431                    // cursor is at record start
432                    Some(expr_last)
433                } else {
434                    // cursor is inside record
435                    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                    // cursor is at block end
507                    Some(expr_first)
508                } else if expr_first == global_cursor_offset {
509                    // cursor is at block start
510                    Some(expr_last)
511                } else {
512                    // cursor is inside block
513                    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                    // cursor is at list end
539                    Some(expr_first)
540                } else if expr_first == global_cursor_offset {
541                    // cursor is at list start
542                    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}