Reverts nushell/nushell#7448 Some surprising behavior in how we do this. For example: ``` 〉if (true || false) { print "yes!" } else { print "no!" } no! 〉if (true or false) { print "yes!" } else { print "no!" } yes! ``` This means for folks who are using the old `||`, they possibly get the wrong answer once they upgrade. I don't think we can ship with that as it will catch too many people by surprise and just make it easier to write buggy code.
436 lines
16 KiB
Rust
436 lines
16 KiB
Rust
use log::trace;
|
|
use nu_ansi_term::Style;
|
|
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, PipelineElement};
|
|
use nu_protocol::engine::{EngineState, StateWorkingSet};
|
|
use nu_protocol::{Config, Span};
|
|
use reedline::{Highlighter, StyledText};
|
|
|
|
pub struct NuHighlighter {
|
|
pub engine_state: EngineState,
|
|
pub config: Config,
|
|
}
|
|
|
|
impl Highlighter for NuHighlighter {
|
|
fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
|
|
trace!("highlighting: {}", line);
|
|
|
|
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())
|
|
};
|
|
|
|
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
|
|
|| shape.0.start < global_span_offset
|
|
{
|
|
// We've already output something for this span
|
|
// so just skip this one
|
|
continue;
|
|
}
|
|
if shape.0.start > last_seen_span {
|
|
let gap = line
|
|
[(last_seen_span - global_span_offset)..(shape.0.start - global_span_offset)]
|
|
.to_string();
|
|
output.push((Style::new(), gap));
|
|
}
|
|
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 => 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 => {
|
|
add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
|
|
}
|
|
FlatShape::Table => {
|
|
add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
|
|
}
|
|
FlatShape::Record => {
|
|
add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
|
|
}
|
|
|
|
FlatShape::Block => {
|
|
add_colored_token_with_bracket_highlight!(shape.1, shape.0, 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::Pipe => add_colored_token!(shape.1, next_token),
|
|
FlatShape::And => add_colored_token!(shape.1, next_token),
|
|
FlatShape::Or => add_colored_token!(shape.1, next_token),
|
|
FlatShape::Redirection => add_colored_token!(shape.1, next_token),
|
|
FlatShape::Custom(..) => add_colored_token!(shape.1, next_token),
|
|
}
|
|
last_seen_span = shape.0.end;
|
|
}
|
|
|
|
let remainder = line[(last_seen_span - global_span_offset)..].to_string();
|
|
if !remainder.is_empty() {
|
|
output.push((Style::new(), remainder));
|
|
}
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
fn split_span_by_highlight_positions(
|
|
line: &str,
|
|
span: &Span,
|
|
highlight_positions: &Vec<usize>,
|
|
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::new(start, *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::new(*pos, end), true));
|
|
start = end;
|
|
}
|
|
}
|
|
if start < span.end {
|
|
result.push((Span::new(start, span.end), false));
|
|
}
|
|
result
|
|
}
|
|
|
|
fn find_matching_brackets(
|
|
line: &str,
|
|
working_set: &StateWorkingSet,
|
|
block: &Block,
|
|
global_span_offset: usize,
|
|
global_cursor_offset: usize,
|
|
) -> Vec<usize> {
|
|
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<usize> {
|
|
for p in &block.pipelines {
|
|
for e in &p.elements {
|
|
match e {
|
|
PipelineElement::Expression(_, e)
|
|
| PipelineElement::Redirection(_, _, e)
|
|
| PipelineElement::And(_, e)
|
|
| PipelineElement::Or(_, e) => {
|
|
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<usize> {
|
|
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) && expression.span.start >= global_span_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::Closure(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<char> {
|
|
s[index..].chars().next()
|
|
}
|
|
|
|
fn get_char_length(c: char) -> usize {
|
|
c.to_string().len()
|
|
}
|