use ropey::Rope;
use std::ops::Range;
use tree_sitter::Node;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Align {
Left,
Right,
Center,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableCell {
pub content: Range<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRow {
pub line: Range<usize>,
pub cells: Vec<TableCell>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableInfo {
pub block: Range<usize>,
pub header: TableRow,
pub delimiter_line: Range<usize>,
pub aligns: Vec<Align>,
pub body: Vec<TableRow>,
pub ncols: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowKind {
Header,
Delimiter,
Body(usize),
}
fn trim_cell_range(rope: &Rope, mut start: usize, mut end: usize) -> Range<usize> {
while start < end && matches!(rope.get_byte(start), Some(b' ') | Some(b'\t')) {
start += 1;
}
while end > start && matches!(rope.get_byte(end - 1), Some(b' ') | Some(b'\t')) {
end -= 1;
}
start..end
}
fn row_from_node(node: &Node, rope: &Rope) -> TableRow {
let mut cursor = node.walk();
let cells = node
.children(&mut cursor)
.filter(|c| c.kind() == "pipe_table_cell")
.map(|c| TableCell {
content: trim_cell_range(rope, c.start_byte(), c.end_byte()),
})
.collect();
TableRow {
line: node.start_byte()..node.end_byte(),
cells,
}
}
fn align_from_delimiter_cell(cell: &Node) -> Align {
let mut cursor = cell.walk();
let mut left = false;
let mut right = false;
for mark in cell.children(&mut cursor) {
match mark.kind() {
"pipe_table_align_left" => left = true,
"pipe_table_align_right" => right = true,
_ => {}
}
}
match (left, right) {
(true, true) => Align::Center,
(false, true) => Align::Right,
_ => Align::Left,
}
}
pub fn extract_table(node: &Node, rope: &Rope) -> Option<TableInfo> {
if node.kind() != "pipe_table" {
return None;
}
let mut header: Option<TableRow> = None;
let mut delimiter_line: Option<Range<usize>> = None;
let mut aligns: Vec<Align> = Vec::new();
let mut body: Vec<TableRow> = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"pipe_table_header" => header = Some(row_from_node(&child, rope)),
"pipe_table_delimiter_row" => {
delimiter_line = Some(child.start_byte()..child.end_byte());
let mut dc = child.walk();
aligns = child
.children(&mut dc)
.filter(|c| c.kind() == "pipe_table_delimiter_cell")
.map(|c| align_from_delimiter_cell(&c))
.collect();
}
"pipe_table_row" => body.push(row_from_node(&child, rope)),
_ => {}
}
}
let header = header?;
let delimiter_line = delimiter_line?;
let ncols = std::iter::once(header.cells.len())
.chain(body.iter().map(|r| r.cells.len()))
.max()
.unwrap_or(0);
Some(TableInfo {
block: node.start_byte()..node.end_byte(),
header,
delimiter_line,
aligns,
body,
ncols,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::Buffer;
fn cell_text<'a>(text: &'a str, cell: &TableCell) -> &'a str {
text[cell.content.clone()].trim()
}
fn only_table(buf: &Buffer) -> TableInfo {
let tables = &buf.parsed().tables;
assert_eq!(tables.len(), 1, "expected exactly one table");
tables[0].clone()
}
#[test]
fn basic_two_col() {
let buf: Buffer = "| a | b |\n|:--|--:|\n| 1 | 2 |\n".parse().unwrap();
let text = buf.text();
let t = only_table(&buf);
assert_eq!(t.ncols, 2);
assert_eq!(t.aligns, vec![Align::Left, Align::Right]);
assert_eq!(t.header.cells.len(), 2);
assert_eq!(cell_text(&text, &t.header.cells[0]), "a");
assert_eq!(cell_text(&text, &t.header.cells[1]), "b");
assert_eq!(t.body.len(), 1);
assert_eq!(cell_text(&text, &t.body[0].cells[0]), "1");
assert_eq!(cell_text(&text, &t.body[0].cells[1]), "2");
}
#[test]
fn center_alignment() {
let buf: Buffer = "| a | b |\n|:-:|:-:|\n| 1 | 2 |\n".parse().unwrap();
let t = only_table(&buf);
assert_eq!(t.aligns, vec![Align::Center, Align::Center]);
}
#[test]
fn default_alignment() {
let buf: Buffer = "| a | b |\n|---|---|\n| 1 | 2 |\n".parse().unwrap();
let t = only_table(&buf);
assert_eq!(t.aligns, vec![Align::Left, Align::Left]);
}
#[test]
fn multi_row_and_ragged() {
let buf: Buffer = "| a | b |\n|---|---|\n| 1 | 2 |\n| 3 |\n".parse().unwrap();
let text = buf.text();
let t = only_table(&buf);
assert_eq!(t.ncols, 2);
assert_eq!(t.body.len(), 2);
assert_eq!(t.body[0].cells.len(), 2);
assert_eq!(t.body[1].cells.len(), 1);
assert_eq!(cell_text(&text, &t.body[1].cells[0]), "3");
}
#[test]
fn spike_a_lone_header_is_not_a_table() {
let buf: Buffer = "| a | b |\n".parse().unwrap();
assert!(buf.parsed().tables.is_empty());
let buf2: Buffer = "| a | b |\n|---|---|\n".parse().unwrap();
assert_eq!(buf2.parsed().tables.len(), 1);
}
#[test]
fn snapshot_offset_and_row_lookup() {
let mut buf: Buffer = "| a | b |\n|:--|--:|\n| 1 | 2 |\n| 3 | 4 |\n"
.parse()
.unwrap();
let snap = buf.render_snapshot();
let inside = snap.table_containing_offset(15);
assert!(inside.is_some());
assert_eq!(inside.unwrap().ncols, 2);
assert!(snap.table_containing_offset(1000).is_none());
assert!(matches!(
snap.table_row_at_line(0),
Some((_, RowKind::Header))
));
assert!(matches!(
snap.table_row_at_line(1),
Some((_, RowKind::Delimiter))
));
assert!(matches!(
snap.table_row_at_line(2),
Some((_, RowKind::Body(0)))
));
assert!(matches!(
snap.table_row_at_line(3),
Some((_, RowKind::Body(1)))
));
}
}