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