From 8224ec49bc29acf8f4a4e716723209cfbd9c8c63 Mon Sep 17 00:00:00 2001 From: Max Zhuravsky Date: Sat, 22 Oct 2022 19:55:45 +0300 Subject: [PATCH] Highlight matching brackets / parentheses (#6655) * [4325] - wip * [4325] - hightlight only matched symbol * [4325] - cleanup * [4325] - match bracket while typing * [4325] - fix clippy * [4325] - add bracket highlight configuration * [4325] - fix working with non-ascii --- crates/nu-cli/src/syntax_highlight.rs | 522 ++++++++++++------ crates/nu-color-config/src/lib.rs | 2 + .../src/matching_brackets_style.rs | 30 + .../src/sample_config/default_config.nu | 2 + 4 files changed, 400 insertions(+), 156 deletions(-) create mode 100644 crates/nu-color-config/src/matching_brackets_style.rs diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index cd9612f19..79a5a82a4 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -1,9 +1,10 @@ use log::trace; use nu_ansi_term::Style; -use nu_color_config::get_shape_color; +use nu_color_config::{get_matching_brackets_style, get_shape_color}; use nu_parser::{flatten_block, parse, FlatShape}; +use nu_protocol::ast::{Argument, Block, Expr, Expression}; use nu_protocol::engine::{EngineState, StateWorkingSet}; -use nu_protocol::Config; +use nu_protocol::{Config, Span}; use reedline::{Highlighter, StyledText}; pub struct NuHighlighter { @@ -15,10 +16,12 @@ impl Highlighter for NuHighlighter { fn highlight(&self, line: &str, _cursor: usize) -> StyledText { trace!("highlighting: {}", line); - let (shapes, global_span_offset) = { - let mut working_set = StateWorkingSet::new(&self.engine_state); + let mut working_set = StateWorkingSet::new(&self.engine_state); + let block = { let (block, _) = parse(&mut working_set, None, line.as_bytes(), false, &[]); - + block + }; + let (shapes, global_span_offset) = { let shapes = flatten_block(&working_set, &block); (shapes, self.engine_state.next_span_start()) }; @@ -26,6 +29,15 @@ impl Highlighter for NuHighlighter { let mut output = StyledText::default(); let mut last_seen_span = global_span_offset; + let global_cursor_offset = _cursor + global_span_offset; + let matching_brackets_pos = find_matching_brackets( + line, + &working_set, + &block, + global_span_offset, + global_cursor_offset, + ); + for shape in &shapes { if shape.0.end <= last_seen_span || last_seen_span < global_span_offset @@ -44,166 +56,71 @@ impl Highlighter for NuHighlighter { let next_token = line [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)] .to_string(); + + macro_rules! add_colored_token_with_bracket_highlight { + ($shape:expr, $span:expr, $text:expr) => {{ + let spans = split_span_by_highlight_positions( + line, + &$span, + &matching_brackets_pos, + global_span_offset, + ); + spans.iter().for_each(|(part, highlight)| { + let start = part.start - $span.start; + let end = part.end - $span.start; + let text = (&next_token[start..end]).to_string(); + let mut style = get_shape_color($shape.to_string(), &self.config); + if *highlight { + style = get_matching_brackets_style(style, &self.config); + } + output.push((style, text)); + }); + }}; + } + + macro_rules! add_colored_token { + ($shape:expr, $text:expr) => { + output.push((get_shape_color($shape.to_string(), &self.config), $text)) + }; + } + match shape.1 { - FlatShape::Garbage => output.push(( - // nushell Garbage - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::Nothing => output.push(( - // nushell Nothing - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::Binary => { - // nushell ? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Bool => { - // nushell ? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Int => { - // nushell Int - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Float => { - // nushell Decimal - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Range => output.push(( - // nushell DotDot ? - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::InternalCall => output.push(( - // nushell InternalCommand - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::External => { - // nushell ExternalCommand - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::ExternalArg => { - // nushell ExternalWord - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Literal => { - // nushell ? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Operator => output.push(( - // nushell Operator - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::Signature => output.push(( - // nushell ? - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::String => { - // nushell String - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::StringInterpolation => { - // nushell ??? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::DateTime => { - // nushell ??? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } + FlatShape::Garbage => add_colored_token!(shape.1, next_token), + FlatShape::Nothing => add_colored_token!(shape.1, next_token), + FlatShape::Binary => add_colored_token!(shape.1, next_token), + FlatShape::Bool => add_colored_token!(shape.1, next_token), + FlatShape::Int => add_colored_token!(shape.1, next_token), + FlatShape::Float => add_colored_token!(shape.1, next_token), + FlatShape::Range => add_colored_token!(shape.1, next_token), + FlatShape::InternalCall => add_colored_token!(shape.1, next_token), + FlatShape::External => add_colored_token!(shape.1, next_token), + FlatShape::ExternalArg => add_colored_token!(shape.1, next_token), + FlatShape::Literal => add_colored_token!(shape.1, next_token), + FlatShape::Operator => add_colored_token!(shape.1, next_token), + FlatShape::Signature => add_colored_token!(shape.1, next_token), + FlatShape::String => add_colored_token!(shape.1, next_token), + FlatShape::StringInterpolation => add_colored_token!(shape.1, next_token), + FlatShape::DateTime => add_colored_token!(shape.1, next_token), FlatShape::List => { - // nushell ??? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) + add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token) } FlatShape::Table => { - // nushell ??? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) + add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token) } FlatShape::Record => { - // nushell ??? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) + add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token) } + FlatShape::Block => { - // nushell ??? - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) + add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token) } - FlatShape::Filepath => output.push(( - // nushell Path - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::Directory => output.push(( - // nushell Directory - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::GlobPattern => output.push(( - // nushell GlobPattern - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::Variable => output.push(( - // nushell Variable - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), - FlatShape::Flag => { - // nushell Flag - output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )) - } - FlatShape::Custom(..) => output.push(( - get_shape_color(shape.1.to_string(), &self.config), - next_token, - )), + + FlatShape::Filepath => add_colored_token!(shape.1, next_token), + FlatShape::Directory => add_colored_token!(shape.1, next_token), + FlatShape::GlobPattern => add_colored_token!(shape.1, next_token), + FlatShape::Variable => add_colored_token!(shape.1, next_token), + FlatShape::Flag => add_colored_token!(shape.1, next_token), + FlatShape::Custom(..) => add_colored_token!(shape.1, next_token), } last_seen_span = shape.0.end; } @@ -216,3 +133,296 @@ impl Highlighter for NuHighlighter { output } } + +fn split_span_by_highlight_positions( + line: &str, + span: &Span, + highlight_positions: &Vec, + global_span_offset: usize, +) -> Vec<(Span, bool)> { + let mut start = span.start; + let mut result: Vec<(Span, bool)> = Vec::new(); + for pos in highlight_positions { + if start <= *pos && pos < &span.end { + if start < *pos { + result.push((Span { start, end: *pos }, false)); + } + let span_str = &line[pos - global_span_offset..span.end - global_span_offset]; + let end = span_str + .chars() + .next() + .map(|c| pos + get_char_length(c)) + .unwrap_or(pos + 1); + result.push((Span { start: *pos, end }, true)); + start = end; + } + } + if start < span.end { + result.push(( + Span { + start, + end: span.end, + }, + false, + )); + } + result +} + +fn find_matching_brackets( + line: &str, + working_set: &StateWorkingSet, + block: &Block, + global_span_offset: usize, + global_cursor_offset: usize, +) -> Vec { + const BRACKETS: &str = "{}[]()"; + + // calculate first bracket position + let global_end_offset = line.len() + global_span_offset; + let global_bracket_pos = + if global_cursor_offset == global_end_offset && global_end_offset > global_span_offset { + // cursor is at the end of a non-empty string -- find block end at the previous position + if let Some(last_char) = line.chars().last() { + global_cursor_offset - get_char_length(last_char) + } else { + global_cursor_offset + } + } else { + // cursor is in the middle of a string -- find block end at the current position + global_cursor_offset + }; + + // check that position contains bracket + let match_idx = global_bracket_pos - global_span_offset; + if match_idx >= line.len() + || !BRACKETS.contains(get_char_at_index(line, match_idx).unwrap_or_default()) + { + return Vec::new(); + } + + // find matching bracket by finding matching block end + let matching_block_end = find_matching_block_end_in_block( + line, + working_set, + block, + global_span_offset, + global_bracket_pos, + ); + if let Some(pos) = matching_block_end { + let matching_idx = pos - global_span_offset; + if BRACKETS.contains(get_char_at_index(line, matching_idx).unwrap_or_default()) { + return if global_bracket_pos < pos { + vec![global_bracket_pos, pos] + } else { + vec![pos, global_bracket_pos] + }; + } + } + Vec::new() +} + +fn find_matching_block_end_in_block( + line: &str, + working_set: &StateWorkingSet, + block: &Block, + global_span_offset: usize, + global_cursor_offset: usize, +) -> Option { + for p in &block.pipelines { + for e in &p.expressions { + if e.span.contains(global_cursor_offset) { + if let Some(pos) = find_matching_block_end_in_expr( + line, + working_set, + e, + global_span_offset, + global_cursor_offset, + ) { + return Some(pos); + } + } + } + } + None +} + +fn find_matching_block_end_in_expr( + line: &str, + working_set: &StateWorkingSet, + expression: &Expression, + global_span_offset: usize, + global_cursor_offset: usize, +) -> Option { + macro_rules! find_in_expr_or_continue { + ($inner_expr:ident) => { + if let Some(pos) = find_matching_block_end_in_expr( + line, + working_set, + $inner_expr, + global_span_offset, + global_cursor_offset, + ) { + return Some(pos); + } + }; + } + + if expression.span.contains(global_cursor_offset) { + let expr_first = expression.span.start; + let span_str = &line + [expression.span.start - global_span_offset..expression.span.end - global_span_offset]; + let expr_last = span_str + .chars() + .last() + .map(|c| expression.span.end - get_char_length(c)) + .unwrap_or(expression.span.start); + + return match &expression.expr { + Expr::Bool(_) => None, + Expr::Int(_) => None, + Expr::Float(_) => None, + Expr::Binary(_) => None, + Expr::Range(..) => None, + Expr::Var(_) => None, + Expr::VarDecl(_) => None, + Expr::ExternalCall(..) => None, + Expr::Operator(_) => None, + Expr::UnaryNot(_) => None, + Expr::Keyword(..) => None, + Expr::ValueWithUnit(..) => None, + Expr::DateTime(_) => None, + Expr::Filepath(_) => None, + Expr::Directory(_) => None, + Expr::GlobPattern(_) => None, + Expr::String(_) => None, + Expr::CellPath(_) => None, + Expr::ImportPattern(_) => None, + Expr::Overlay(_) => None, + Expr::Signature(_) => None, + Expr::Nothing => None, + Expr::Garbage => None, + + Expr::Table(hdr, rows) => { + if expr_last == global_cursor_offset { + // cursor is at table end + Some(expr_first) + } else if expr_first == global_cursor_offset { + // cursor is at table start + Some(expr_last) + } else { + // cursor is inside table + for inner_expr in hdr { + find_in_expr_or_continue!(inner_expr); + } + for row in rows { + for inner_expr in row { + find_in_expr_or_continue!(inner_expr); + } + } + None + } + } + + Expr::Record(exprs) => { + if expr_last == global_cursor_offset { + // cursor is at record end + Some(expr_first) + } else if expr_first == global_cursor_offset { + // cursor is at record start + Some(expr_last) + } else { + // cursor is inside record + for (k, v) in exprs { + find_in_expr_or_continue!(k); + find_in_expr_or_continue!(v); + } + None + } + } + + Expr::Call(call) => { + for arg in &call.arguments { + let opt_expr = match arg { + Argument::Named((_, _, opt_expr)) => opt_expr.as_ref(), + Argument::Positional(inner_expr) => Some(inner_expr), + }; + + if let Some(inner_expr) = opt_expr { + find_in_expr_or_continue!(inner_expr); + } + } + None + } + + Expr::FullCellPath(b) => find_matching_block_end_in_expr( + line, + working_set, + &b.head, + global_span_offset, + global_cursor_offset, + ), + + Expr::BinaryOp(lhs, op, rhs) => { + find_in_expr_or_continue!(lhs); + find_in_expr_or_continue!(op); + find_in_expr_or_continue!(rhs); + None + } + + Expr::Block(block_id) + | Expr::RowCondition(block_id) + | Expr::Subexpression(block_id) => { + if expr_last == global_cursor_offset { + // cursor is at block end + Some(expr_first) + } else if expr_first == global_cursor_offset { + // cursor is at block start + Some(expr_last) + } else { + // cursor is inside block + let nested_block = working_set.get_block(*block_id); + find_matching_block_end_in_block( + line, + working_set, + nested_block, + global_span_offset, + global_cursor_offset, + ) + } + } + + Expr::StringInterpolation(inner_expr) => { + for inner_expr in inner_expr { + find_in_expr_or_continue!(inner_expr); + } + None + } + + Expr::List(inner_expr) => { + if expr_last == global_cursor_offset { + // cursor is at list end + Some(expr_first) + } else if expr_first == global_cursor_offset { + // cursor is at list start + Some(expr_last) + } else { + // cursor is inside list + for inner_expr in inner_expr { + find_in_expr_or_continue!(inner_expr); + } + None + } + } + }; + } + None +} + +fn get_char_at_index(s: &str, index: usize) -> Option { + s[index..].chars().next() +} + +fn get_char_length(c: char) -> usize { + c.to_string().len() +} diff --git a/crates/nu-color-config/src/lib.rs b/crates/nu-color-config/src/lib.rs index f1ad25ac4..b5441979e 100644 --- a/crates/nu-color-config/src/lib.rs +++ b/crates/nu-color-config/src/lib.rs @@ -1,7 +1,9 @@ mod color_config; +mod matching_brackets_style; mod nu_style; mod shape_color; pub use color_config::*; +pub use matching_brackets_style::*; pub use nu_style::*; pub use shape_color::*; diff --git a/crates/nu-color-config/src/matching_brackets_style.rs b/crates/nu-color-config/src/matching_brackets_style.rs new file mode 100644 index 000000000..5967d73f6 --- /dev/null +++ b/crates/nu-color-config/src/matching_brackets_style.rs @@ -0,0 +1,30 @@ +use crate::color_config::lookup_ansi_color_style; +use nu_ansi_term::Style; +use nu_protocol::Config; + +pub fn get_matching_brackets_style(default_style: Style, conf: &Config) -> Style { + const MATCHING_BRACKETS_CONFIG_KEY: &str = "shape_matching_brackets"; + + match conf.color_config.get(MATCHING_BRACKETS_CONFIG_KEY) { + Some(int_color) => match int_color.as_string() { + Ok(int_color) => merge_styles(default_style, lookup_ansi_color_style(&int_color)), + Err(_) => default_style, + }, + None => default_style, + } +} + +fn merge_styles(base: Style, extra: Style) -> Style { + Style { + foreground: extra.foreground.or(base.foreground), + background: extra.background.or(base.background), + is_bold: extra.is_bold || base.is_bold, + is_dimmed: extra.is_dimmed || base.is_dimmed, + is_italic: extra.is_italic || base.is_italic, + is_underline: extra.is_underline || base.is_underline, + is_blink: extra.is_blink || base.is_blink, + is_reverse: extra.is_reverse || base.is_reverse, + is_hidden: extra.is_hidden || base.is_hidden, + is_strikethrough: extra.is_strikethrough || base.is_strikethrough, + } +} diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index f16270bbe..b8e0ec896 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -179,6 +179,7 @@ let dark_theme = { shape_flag: blue_bold shape_custom: green shape_nothing: light_cyan + shape_matching_brackets: { attr: u } } let light_theme = { @@ -231,6 +232,7 @@ let light_theme = { shape_flag: blue_bold shape_custom: green shape_nothing: light_cyan + shape_matching_brackets: { attr: u } } # External completer example