use rowan::{NodeOrToken, SyntaxElement, SyntaxToken};
use super::super::context::FormatContext;
use super::super::core::{ir_expr_segment, is_trivia};
use super::super::ir::Ir;
use super::super::printer::Printer;
use crate::syntax::{RLanguage, SyntaxKind, SyntaxNode};
const DOT_WIDTH: usize = 1;
pub(crate) fn ir_tribble_table(
callee: &[SyntaxElement<RLanguage>],
arg_list: &SyntaxNode,
indent: usize,
ctx: FormatContext,
) -> Option<Ir> {
if !callee_is_tribble(callee) {
return None;
}
let (args, trailing_comma) = collect_args(arg_list)?;
if args.iter().any(is_dynamic_dots) {
return None;
}
let columns = args
.iter()
.take_while(|arg| is_one_sided_formula(arg))
.count();
if columns == 0 || (args.len() - columns) % columns != 0 {
return None;
}
let printer = Printer::new(ctx.style());
let mut cells: Vec<Cell> = Vec::with_capacity(args.len());
for arg in &args {
cells.push(build_cell(arg, &printer, indent, ctx)?);
}
let widths = measure_columns(&cells, columns);
Some(render(&cells, &widths, columns, trailing_comma))
}
fn callee_is_tribble(elements: &[SyntaxElement<RLanguage>]) -> bool {
match significant(elements.iter().cloned()).as_slice() {
[NodeOrToken::Token(tok)] => tok.kind() == SyntaxKind::IDENT && tok.text() == "tribble",
[NodeOrToken::Node(node)] if node.kind() == SyntaxKind::BINARY_EXPR => {
let parts = significant(node.children_with_tokens());
matches!(
parts.as_slice(),
[_, NodeOrToken::Token(op), NodeOrToken::Token(name)]
if matches!(op.kind(), SyntaxKind::COLON2 | SyntaxKind::COLON3)
&& name.kind() == SyntaxKind::IDENT
&& name.text() == "tribble"
)
}
_ => false,
}
}
fn collect_args(arg_list: &SyntaxNode) -> Option<(Vec<SyntaxNode>, bool)> {
let mut slots: Vec<Option<SyntaxNode>> = Vec::new();
let mut current: Option<SyntaxNode> = None;
for element in arg_list.children_with_tokens() {
match element {
NodeOrToken::Node(arg) if arg.kind() == SyntaxKind::ARG => {
if arg.children_with_tokens().all(|el| is_trivia(el.kind())) {
continue;
}
current = Some(arg);
}
NodeOrToken::Token(tok) if tok.kind() == SyntaxKind::COMMA => {
slots.push(current.take());
}
_ => {}
}
}
slots.push(current.take());
let trailing_comma = slots.len() > 1 && slots.last().is_some_and(Option::is_none);
if trailing_comma {
slots.pop();
}
let mut args = Vec::with_capacity(slots.len());
for slot in slots {
let arg = slot?;
if arg
.children_with_tokens()
.any(|el| el.kind() == SyntaxKind::ASSIGN_EQ)
{
return None;
}
args.push(arg);
}
if args.is_empty() {
return None;
}
Some((args, trailing_comma))
}
fn is_one_sided_formula(arg: &SyntaxNode) -> bool {
let Some(NodeOrToken::Node(node)) = single_significant(arg) else {
return false;
};
unary_parts(&node).is_some_and(|(op, _)| op.kind() == SyntaxKind::TILDE)
}
fn is_dynamic_dots(arg: &SyntaxNode) -> bool {
match single_significant(arg) {
Some(NodeOrToken::Token(tok)) => tok.kind() == SyntaxKind::IDENT && tok.text() == "...",
Some(NodeOrToken::Node(node)) => unary_parts(&node).is_some_and(|(op, operand)| {
op.kind() == SyntaxKind::BANG
&& matches!(&operand, NodeOrToken::Node(inner)
if unary_parts(inner).is_some_and(|(op, _)| op.kind() == SyntaxKind::BANG))
}),
None => false,
}
}
struct Cell {
text: String,
kind: CellKind,
}
enum CellKind {
Numeric {
integer_width: usize,
fractional_width: Option<usize>,
},
Other,
}
impl Cell {
fn width(&self) -> usize {
self.text.chars().count()
}
}
fn build_cell(
arg: &SyntaxNode,
printer: &Printer,
indent: usize,
ctx: FormatContext,
) -> Option<Cell> {
let element = single_significant(arg)?;
let elements: Vec<_> = arg.children_with_tokens().collect();
let ir = ir_expr_segment(&elements, "tribble cell", indent, ctx).ok()?;
if ir.contains_forced_break() {
return None;
}
let text = printer.render_flat(&ir)?;
let kind = if is_numeric_atom(&element) {
numeric_kind(&text)
} else {
CellKind::Other
};
Some(Cell { text, kind })
}
fn is_numeric_atom(element: &SyntaxElement<RLanguage>) -> bool {
match element {
NodeOrToken::Token(tok) => is_numeric_literal(tok),
NodeOrToken::Node(node) => unary_parts(node).is_some_and(|(op, operand)| {
matches!(op.kind(), SyntaxKind::PLUS | SyntaxKind::MINUS)
&& matches!(&operand, NodeOrToken::Token(tok) if is_numeric_literal(tok))
}),
}
}
fn is_numeric_literal(token: &SyntaxToken<RLanguage>) -> bool {
matches!(token.kind(), SyntaxKind::INT | SyntaxKind::FLOAT)
}
fn numeric_kind(text: &str) -> CellKind {
let width = text.chars().count();
let integer_width = text.chars().take_while(|c| *c != '.').count();
let (integer_width, fractional_width) = if integer_width < width {
(integer_width, Some(width - integer_width - DOT_WIDTH))
} else if text.ends_with('L') {
(width - 1, Some(0))
} else {
(width, None)
};
CellKind::Numeric {
integer_width,
fractional_width,
}
}
#[derive(Default)]
struct ColumnInfo {
has_decimal: bool,
max_width: usize,
max_integer_part: usize,
max_fractional_part: usize,
}
fn measure_columns(cells: &[Cell], columns: usize) -> Vec<ColumnInfo> {
let mut infos: Vec<ColumnInfo> = (0..columns).map(|_| ColumnInfo::default()).collect();
for (index, cell) in cells.iter().enumerate() {
let info = &mut infos[index % columns];
info.max_width = info.max_width.max(cell.width());
if let CellKind::Numeric {
integer_width,
fractional_width,
} = cell.kind
{
info.max_integer_part = info.max_integer_part.max(integer_width);
if let Some(fractional_width) = fractional_width {
info.has_decimal = true;
info.max_fractional_part = info.max_fractional_part.max(fractional_width);
}
}
}
infos
}
impl ColumnInfo {
fn padding(&self, cell: &Cell) -> (usize, usize) {
if self.has_decimal {
self.decimal_padding(cell)
} else {
self.simple_padding(cell)
}
}
fn simple_padding(&self, cell: &Cell) -> (usize, usize) {
let padding = self.max_width - cell.width();
match cell.kind {
CellKind::Numeric { .. } => (padding, 0),
CellKind::Other => (0, padding),
}
}
fn decimal_padding(&self, cell: &Cell) -> (usize, usize) {
let decimal_width = self.max_integer_part + DOT_WIDTH + self.max_fractional_part;
let target = self.max_width.max(decimal_width);
match cell.kind {
CellKind::Numeric {
integer_width,
fractional_width,
} => {
let left = self.max_integer_part - integer_width;
let right = match fractional_width {
Some(width) => self.max_fractional_part - width,
None => DOT_WIDTH + self.max_fractional_part,
};
(left, right + (target - decimal_width))
}
CellKind::Other => (0, target - cell.width()),
}
}
}
fn render(cells: &[Cell], widths: &[ColumnInfo], columns: usize, trailing_comma: bool) -> Ir {
let mut lines: Vec<Ir> = Vec::new();
for (row_index, row) in cells.chunks(columns).enumerate() {
let mut parts: Vec<Ir> = Vec::new();
for (column_index, cell) in row.iter().enumerate() {
let column = &widths[column_index];
let (left, right) = column.padding(cell);
let is_final = row_index * columns + column_index + 1 == cells.len();
parts.push(spaces(left));
parts.push(Ir::text(cell.text.clone()));
if !is_final || trailing_comma {
parts.push(spaces(right));
parts.push(Ir::text(" ,"));
if column_index + 1 < row.len() {
parts.push(Ir::text(" "));
}
}
}
lines.push(Ir::concat(parts));
}
let body = Ir::concat(
lines
.into_iter()
.map(|line| Ir::concat([Ir::hard_line(), line])),
);
Ir::concat([
Ir::text("("),
Ir::indent(body),
Ir::hard_line(),
Ir::text(")"),
])
}
fn spaces(count: usize) -> Ir {
if count == 0 {
Ir::nil()
} else {
Ir::text(" ".repeat(count))
}
}
fn significant(
iter: impl Iterator<Item = SyntaxElement<RLanguage>>,
) -> Vec<SyntaxElement<RLanguage>> {
iter.filter(|el| !is_trivia(el.kind())).collect()
}
fn single_significant(node: &SyntaxNode) -> Option<SyntaxElement<RLanguage>> {
match significant(node.children_with_tokens()).as_slice() {
[element] => Some(element.clone()),
_ => None,
}
}
fn unary_parts(node: &SyntaxNode) -> Option<(SyntaxToken<RLanguage>, SyntaxElement<RLanguage>)> {
if node.kind() != SyntaxKind::UNARY_EXPR {
return None;
}
match significant(node.children_with_tokens()).as_slice() {
[NodeOrToken::Token(op), operand] => Some((op.clone(), operand.clone())),
_ => None,
}
}