use crate::ast::Pattern;
use crate::engine::{Match, Span};
use crate::explain::{Explainer, Explanation};
use crate::files::LineIndex;
use crate::paint::{Painter, Role};
use crate::records::RecordUnit;
use crate::token::{Token, TokenKind};
#[derive(Clone, Copy)]
pub struct Members<'a> {
pub set: &'a crate::PatternSet,
pub of: &'a [usize],
}
impl Members<'_> {
#[must_use]
pub fn name(&self, k: usize) -> String {
self.set.name(self.of[k])
}
}
pub struct Explaining<'a> {
explainers: Vec<Option<Explainer<'a>>>,
route: &'a str,
}
impl<'a> Explaining<'a> {
#[must_use]
pub fn over<'p>(
count: usize,
pattern: impl Fn(usize) -> &'p Pattern,
input: &'a [u8],
shapes: &crate::ShapeSet,
members: Option<Members<'_>>,
route: &'a str,
) -> Self {
let mut explainers: Vec<Option<Explainer<'a>>> = (0..count).map(|_| None).collect();
let mut present: Vec<usize> = members.map_or_else(|| vec![0], |ms| ms.of.to_vec());
present.sort_unstable();
present.dedup();
for i in present {
explainers[i] = Some(Explainer::new(pattern(i), input, shapes));
}
Explaining { explainers, route }
}
#[must_use]
pub fn explain(&self, m: &Match, members: Option<Members<'_>>, k: usize) -> Explanation {
let i = members.map_or(0, |ms| ms.of[k]);
self.explainers[i].as_ref().expect("an explainer for every pattern with a match").explain(m, self.route)
}
}
#[must_use]
pub fn json_extras(
m: &Match,
members: Option<Members<'_>>,
k: usize,
explaining: Option<&Explaining<'_>>,
) -> Option<String> {
let mut parts = Vec::new();
if let Some(ms) = members {
parts.push(format!("\"pattern\":\"{}\"", json_escape(&ms.name(k))));
}
if let Some(ex) = explaining {
parts.push(explanation_json(&ex.explain(m, members, k)));
}
if parts.is_empty() { None } else { Some(parts.join(",")) }
}
#[must_use]
pub fn explanation_lines(e: &Explanation) -> Vec<String> {
let tokens: Vec<String> = e.tokens.iter().map(|(kind, text)| format!("{kind} {text:?}")).collect();
let mut out = vec![format!(" tokens: {}", tokens.join(", "))];
for guard in &e.guards {
out.push(format!(" guard: {guard}"));
}
for r in &e.readings {
out.push(format!(" {}: {} {:?}", r.axis, r.value, r.text));
}
out.push(format!(" route: {}", e.route));
out
}
pub fn human_report(
input: &[u8],
matches: &[Match],
index: &LineIndex,
painter: &Painter,
members: Option<Members<'_>>,
explaining: Option<&Explaining<'_>>,
out: &mut dyn FnMut(&str),
) {
if matches.is_empty() {
out("no match");
return;
}
for (k, m) in matches.iter().enumerate() {
let name = members.map(|ms| ms.name(k));
out(&format!(
"[{}..{}] {}",
index.offset(m.start),
index.offset(m.end),
match_text(input, m, painter, name.as_deref())
));
if let Some(ex) = explaining {
for line in explanation_lines(&ex.explain(m, members, k)) {
out(&line);
}
}
}
}
#[must_use]
pub fn json_report(
input: &[u8],
matches: &[Match],
index: &LineIndex,
members: Option<Members<'_>>,
explaining: Option<&Explaining<'_>>,
values: Option<&ValueView<'_>>,
) -> String {
let mut out = String::from("[");
for (k, m) in matches.iter().enumerate() {
if k > 0 {
out.push(',');
}
let extra = json_extras(m, members, k, explaining);
out.push_str(&match_json(input, m, None, index.offset(0), extra.as_deref(), values));
}
out.push(']');
out
}
#[allow(clippy::too_many_arguments)]
pub fn lines_report(
prefix: Option<&str>,
input: &[u8],
matches: &[Match],
index: &LineIndex,
invert: bool,
max_count: Option<usize>,
painter: &Painter,
out: &mut dyn FnMut(&str),
) {
let tokens = if painter.paints_kinds() { crate::lexer::lex(input) } else { Vec::new() };
let spans: Vec<(usize, usize)> = matches
.iter()
.map(|m| (index.line_of(m.start), index.line_of(m.end.saturating_sub(1).max(m.start))))
.collect();
let mut from = 0usize;
let mut selected_so_far = 0usize;
for line in 0..index.lines() {
while from < matches.len() && spans[from].1 < line {
from += 1;
}
let mut to = from;
while to < matches.len() && spans[to].0 <= line {
to += 1;
}
let here = &matches[from..to];
let touched = !here.is_empty();
if invert && touched {
continue;
}
let capped = max_count.is_some_and(|n| selected_so_far >= n);
if invert && capped {
break;
}
let selected = invert || (touched && !capped);
if selected {
selected_so_far += 1;
}
out(&painted_line(prefix, input, index, line, selected, if selected { here } else { &[] }, painter, &tokens));
}
}
#[must_use]
pub fn listing_header(name: &str, painter: &Painter) -> String {
format!("==> {} <==", painter.paint(Role::Path, name))
}
pub fn numbered_lines(input: &[u8], index: &LineIndex, painter: &Painter, out: &mut dyn FnMut(&str)) {
if input.is_empty() {
return;
}
let tokens = if painter.paints_kinds() { crate::lexer::lex(input) } else { Vec::new() };
for line in 0..index.lines() {
let (s, e) = index.line_span(line);
out(&format!(
"{}{}{}",
painter.paint(Role::Line, &index.number(line).to_string()),
painter.paint(Role::Separator, ":"),
painter.paint_kinds(input, s..e, &tokens)
));
}
}
pub struct Context<'a> {
pub before: usize,
pub after: usize,
pub record: Option<(&'a RecordUnit, bool, bool)>,
}
#[allow(clippy::too_many_arguments)]
pub fn context_report(
prefix: Option<&str>,
input: &[u8],
matches: &[Match],
index: &LineIndex,
context: &Context<'_>,
painter: &Painter,
members: Option<Members<'_>>,
explaining: Option<&Explaining<'_>>,
out: &mut dyn FnMut(&str),
) {
let (before, after) = (context.before, context.after);
let lines = index.lines();
let match_lines: Vec<usize> = matches.iter().map(|m| index.line_of(m.start)).collect();
let records = context.record.map(|(unit, _, _)| unit.records(input));
let tokens = if painter.paints_kinds() { crate::lexer::lex(input) } else { Vec::new() };
let mut last_printed: Option<usize> = None;
let sep = |s: &str| painter.paint(Role::Separator, s);
let context_line = |line: usize, out: &mut dyn FnMut(&str)| {
let (s, e) = index.line_span(line);
let text = painter.paint_kinds(input, s..e, &tokens);
let number = painter.paint(Role::Line, &index.number(line).to_string());
match prefix {
Some(p) => out(&format!("{}{}{number}{}{text}", painter.paint(Role::Path, p), sep("-"), sep("-"))),
None => out(&format!("{number}{}{text}", sep("-"))),
}
};
for (k, (m, &line)) in matches.iter().zip(&match_lines).enumerate() {
let (line1, col) = index.line_col(input, m.start);
let (from, to) = match (context.record, &records) {
(Some((_, ahead, behind)), Some(records)) => {
let holding = records
.iter()
.filter(|&&(s, e)| s <= m.start && m.end <= e)
.max_by_key(|&&(s, e)| (s, std::cmp::Reverse(e)));
match holding {
Some(&(s, e)) => (
if ahead { index.line_of(s) } else { line },
if behind { (index.line_of(e.saturating_sub(1)) + 1).min(lines) } else { line + 1 },
),
None => (line, line + 1),
}
}
_ => (line.saturating_sub(before), (line + after + 1).min(lines)),
};
if (before > 0 || after > 0 || context.record.is_some())
&& let Some(last) = last_printed
&& from > last + 1
{
out("--");
}
for l in from..line {
if last_printed.is_some_and(|last| l <= last) || match_lines.binary_search(&l).is_ok() {
continue;
}
context_line(l, &mut *out);
last_printed = Some(l);
}
let place = format!(
"{}{}{}{} ",
painter.paint(Role::Line, &line1.to_string()),
sep(":"),
painter.paint(Role::Column, &col.to_string()),
sep(":")
);
let name = members.map(|ms| ms.name(k));
let text = match_text(input, m, painter, name.as_deref());
match prefix {
Some(p) => out(&format!("{}{}{place}{text}", painter.paint(Role::Path, p), sep(":"))),
None => out(&format!("{place}{text}")),
}
if let Some(ex) = explaining {
for said in explanation_lines(&ex.explain(m, members, k)) {
out(&said);
}
}
last_printed = Some(line);
for l in line + 1..to {
if match_lines.binary_search(&l).is_ok() {
continue;
}
context_line(l, &mut *out);
last_printed = Some(l);
}
}
}
pub struct ValueView<'a> {
pub kinds: &'a [(String, Option<TokenKind>)],
pub style: crate::typed::ValueStyle,
pub clock: crate::Clock,
}
impl ValueView<'_> {
fn kind_of(&self, name: &str) -> Option<TokenKind> {
self.kinds.iter().find(|(n, _)| n == name).and_then(|(_, k)| *k)
}
}
#[must_use]
pub fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
#[must_use]
pub fn match_json(
input: &[u8],
m: &Match,
at: Option<(&str, usize, usize)>,
base: usize,
extra: Option<&str>,
values: Option<&ValueView<'_>>,
) -> String {
let span = String::from_utf8_lossy(&input[m.start..m.end]);
let mut out = String::from("{");
if let Some((path, line, col)) = at {
out.push_str(&format!("\"path\":\"{}\",\"line\":{line},\"col\":{col},", json_escape(path)));
}
out.push_str(&format!(
"\"start\":{},\"end\":{},\"text\":\"{}\",\"captures\":{}",
m.start + base,
m.end + base,
json_escape(&span),
captures_json(input, m, "", None, values)
));
if let Some(members) = extra {
out.push(',');
out.push_str(members);
}
out.push('}');
out
}
#[must_use]
pub fn captures_json(
input: &[u8],
m: &Match,
prefix: &str,
within: Option<Span>,
values: Option<&ValueView<'_>>,
) -> String {
let names = m.names();
let inside = |s: &Span| within.is_none_or(|w| s.start() >= w.start() && s.end() <= w.end());
let text = |s: &Span| json_escape(&String::from_utf8_lossy(&input[s.range()]));
let mut members: Vec<String> = Vec::new();
for (name, span) in names.iter().zip(m.captures()) {
let Some(short) = name.strip_prefix(prefix) else { continue };
if short.is_empty() || short.contains('.') || (!prefix.is_empty() && !name.starts_with(prefix)) {
continue;
}
let nested = format!("{name}.");
let has_children = names.iter().any(|n| n.starts_with(&nested));
let valued = |s: &Span| {
let v = values?;
let kind = v.kind_of(name)?;
let txt = String::from_utf8_lossy(&input[s.range()]);
let read = crate::typed::value_of(kind, &txt)?;
Some(read.json(kind, v.style, v.clock))
};
let one = |s: &Span| match (has_children, valued(s)) {
(true, Some(v)) => format!(
"{{\"text\":\"{}\",\"value\":{v},\"captures\":{}}}",
text(s),
captures_json(input, m, &nested, Some(*s), values)
),
(true, None) => format!(
"{{\"text\":\"{}\",\"captures\":{}}}",
text(s),
captures_json(input, m, &nested, Some(*s), values)
),
(false, Some(v)) => format!("{{\"text\":\"{}\",\"value\":{v}}}", text(s)),
(false, None) => format!("\"{}\"", text(s)),
};
let value = match m.list(name) {
Some(all) => {
let mut items: Vec<String> = all.iter().filter(|s| inside(s)).map(one).collect();
match (within, items.len()) {
(Some(_), 0) => "\"\"".to_string(),
(Some(_), 1) => items.remove(0),
_ => format!("[{}]", items.join(",")),
}
}
None if inside(span) => one(span),
None => "\"\"".to_string(),
};
members.push(format!("\"{}\":{value}", json_escape(short)));
}
format!("{{{}}}", members.join(","))
}
#[must_use]
pub fn explanation_json(e: &Explanation) -> String {
let tokens: Vec<String> = e
.tokens
.iter()
.map(|(kind, text)| format!("{{\"kind\":\"{}\",\"text\":\"{}\"}}", json_escape(kind), json_escape(text)))
.collect();
let guards: Vec<String> = e.guards.iter().map(|g| format!("\"{}\"", json_escape(g))).collect();
let readings: Vec<String> = e
.readings
.iter()
.map(|r| {
format!(
"{{\"axis\":\"{}\",\"text\":\"{}\",\"value\":\"{}\"}}",
r.axis,
json_escape(&r.text),
json_escape(&r.value)
)
})
.collect();
format!(
"\"explain\":{{\"tokens\":[{}],\"guards\":[{}],\"readings\":[{}],\"route\":\"{}\"}}",
tokens.join(","),
guards.join(","),
readings.join(","),
json_escape(&e.route)
)
}
#[must_use]
pub fn match_text(input: &[u8], m: &Match, painter: &Painter, pattern: Option<&str>) -> String {
let quoted = format!("{:?}", String::from_utf8_lossy(&input[m.start..m.end]));
let mut out = painter.paint(Role::Match, "ed);
if !m.captures().is_empty() {
let caps: Vec<String> = m
.names()
.iter()
.zip(m.captures())
.map(|(k, s)| format!("{k}={:?}", String::from_utf8_lossy(&input[s.range()])))
.collect();
out.push_str(" captures: ");
out.push_str(&caps.join(", "));
}
if let Some(name) = pattern {
out.push_str(" pattern: ");
out.push_str(name);
}
out
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn painted_line(
prefix: Option<&str>,
input: &[u8],
index: &LineIndex,
line: usize,
selected: bool,
matches: &[Match],
painter: &Painter,
tokens: &[Token],
) -> String {
let (s, e) = index.line_span(line);
let mut text = String::new();
let mut at = s;
for m in matches {
let (ms, me) = (m.start.max(s), m.end.min(e));
if me <= ms || ms < at {
continue;
}
text.push_str(&painter.paint_kinds(input, at..ms, tokens));
let mut within = ms;
if painter.palette().paints_captures() {
for (name, span) in m.names().iter().zip(m.captures()) {
let Some(style) = painter.palette().capture(name) else { continue };
let sgr = style.sgr(painter.depth());
let (cs, ce) = (span.start().max(within), span.end().min(me));
if sgr.is_empty() || ce <= cs {
continue;
}
if within < cs {
let before = String::from_utf8_lossy(&input[within..cs]).into_owned();
text.push_str(&painter.paint(Role::Match, &before));
}
text.push_str(&format!("\x1b[{sgr}m{}\x1b[0m", String::from_utf8_lossy(&input[cs..ce])));
within = ce;
}
}
if within < me {
text.push_str(&painter.paint(Role::Match, &String::from_utf8_lossy(&input[within..me])));
}
at = me;
}
text.push_str(&painter.paint_kinds(input, at..e, tokens));
let Some(p) = prefix else {
return text;
};
let sep = if selected { ":" } else { "-" };
format!(
"{}{}{}{}{text}",
painter.paint(Role::Path, p),
painter.paint(Role::Separator, sep),
painter.paint(Role::Line, &index.number(line).to_string()),
painter.paint(Role::Separator, sep)
)
}