rho-coding-agent 0.21.1

A lightweight agent harness inspired by Pi
Documentation
use ratatui::{
    style::Modifier,
    text::{Line, Span},
};

use super::{display_width, markdown_inline_segments, wrap_styled_segments, StyledSegment, Theme};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TableAlignment {
    Left,
    Center,
    Right,
}

#[derive(Debug, PartialEq, Eq)]
struct MarkdownTable {
    alignments: Vec<TableAlignment>,
    rows: Vec<Vec<String>>,
}

pub(super) fn markdown_table_lines(
    lines: &[&str],
    width: usize,
) -> Option<(Vec<Line<'static>>, usize)> {
    let (table, consumed_lines) = parse_markdown_table(lines)?;
    render_markdown_table(&table, width).map(|lines| (lines, consumed_lines))
}

fn parse_markdown_table(lines: &[&str]) -> Option<(MarkdownTable, usize)> {
    let [header, separator, ..] = lines else {
        return None;
    };
    if !has_markdown_table_delimiter(header) || !has_markdown_table_delimiter(separator) {
        return None;
    }

    let header = markdown_table_cells(header);
    let separator = markdown_table_cells(separator);
    if header.len() < 2 || header.len() != separator.len() {
        return None;
    }
    let alignments = separator
        .iter()
        .map(|cell| markdown_table_alignment(cell))
        .collect::<Option<Vec<_>>>()?;

    let column_count = header.len();
    let mut rows = vec![header];
    let mut consumed_lines = 2;
    for line in &lines[2..] {
        if !has_markdown_table_delimiter(line) || line.trim().is_empty() {
            break;
        }
        let mut cells = markdown_table_cells(line);
        cells.resize(column_count, String::new());
        cells.truncate(column_count);
        rows.push(cells);
        consumed_lines += 1;
    }

    Some((MarkdownTable { alignments, rows }, consumed_lines))
}

fn has_markdown_table_delimiter(line: &str) -> bool {
    !markdown_table_delimiters(line).is_empty()
}

pub(super) fn markdown_table_cells(line: &str) -> Vec<String> {
    let trimmed = line.trim();
    let delimiters = markdown_table_delimiters(trimmed);
    let start = usize::from(delimiters.first().is_some_and(|delimiter| *delimiter == 0));
    let end = trimmed.len()
        - usize::from(
            delimiters
                .last()
                .is_some_and(|delimiter| *delimiter + '|'.len_utf8() == trimmed.len()),
        );
    let delimiters = delimiters
        .into_iter()
        .filter(|delimiter| *delimiter >= start && *delimiter < end);

    let mut cells = Vec::new();
    let mut cell_start = start;
    for delimiter in delimiters {
        cells.push(markdown_table_cell(&trimmed[cell_start..delimiter]));
        cell_start = delimiter + '|'.len_utf8();
    }
    cells.push(markdown_table_cell(&trimmed[cell_start..end]));
    cells
}

fn markdown_table_delimiters(line: &str) -> Vec<usize> {
    let mut delimiters = Vec::new();
    let mut index = 0;
    let mut code_fence_len = None;

    while index < line.len() {
        let ch = line[index..]
            .chars()
            .next()
            .expect("index is within the line");
        if ch == '`' {
            let fence_len = line[index..]
                .bytes()
                .take_while(|byte| *byte == b'`')
                .count();
            match code_fence_len {
                Some(opening_fence_len) if opening_fence_len == fence_len => code_fence_len = None,
                None if has_matching_code_fence(line, index + fence_len, fence_len) => {
                    code_fence_len = Some(fence_len)
                }
                Some(_) | None => {}
            }
            index += fence_len;
        } else if ch == '\\' && code_fence_len.is_none() {
            index += ch.len_utf8();
            if let Some(next) = line[index..].chars().next() {
                index += next.len_utf8();
            }
        } else {
            if ch == '|' && code_fence_len.is_none() {
                delimiters.push(index);
            }
            index += ch.len_utf8();
        }
    }

    delimiters
}

fn has_matching_code_fence(line: &str, mut index: usize, fence_len: usize) -> bool {
    while index < line.len() {
        if line[index..].starts_with('`') {
            let candidate_len = line[index..]
                .bytes()
                .take_while(|byte| *byte == b'`')
                .count();
            if candidate_len == fence_len {
                return true;
            }
            index += candidate_len;
        } else {
            index += line[index..]
                .chars()
                .next()
                .expect("index is within the line")
                .len_utf8();
        }
    }
    false
}

fn markdown_table_cell(cell: &str) -> String {
    let mut result = String::new();
    let mut index = 0;
    let mut code_fence_len = None;

    while index < cell.len() {
        let ch = cell[index..]
            .chars()
            .next()
            .expect("index is within the cell");
        if ch == '`' {
            let fence_len = cell[index..]
                .bytes()
                .take_while(|byte| *byte == b'`')
                .count();
            match code_fence_len {
                Some(opening_fence_len) if opening_fence_len == fence_len => code_fence_len = None,
                None if has_matching_code_fence(cell, index + fence_len, fence_len) => {
                    code_fence_len = Some(fence_len)
                }
                Some(_) | None => {}
            }
            result.push_str(&cell[index..index + fence_len]);
            index += fence_len;
        } else if ch == '\\' && code_fence_len.is_none() {
            let next_index = index + ch.len_utf8();
            if cell[next_index..].starts_with('|') {
                result.push('|');
                index = next_index + '|'.len_utf8();
            } else {
                result.push(ch);
                index = next_index;
            }
        } else {
            result.push(ch);
            index += ch.len_utf8();
        }
    }

    result.trim().to_string()
}

fn markdown_table_alignment(cell: &str) -> Option<TableAlignment> {
    let cell = cell.trim();
    let left = cell.starts_with(':');
    let right = cell.ends_with(':');
    let marker = cell.trim_matches(':');
    (marker.len() >= 3 && marker.chars().all(|ch| ch == '-')).then_some(match (left, right) {
        (true, true) => TableAlignment::Center,
        (false, true) => TableAlignment::Right,
        (true, false) | (false, false) => TableAlignment::Left,
    })
}

fn render_markdown_table(table: &MarkdownTable, width: usize) -> Option<Vec<Line<'static>>> {
    let column_count = table.alignments.len();
    let border_width = column_count + 1;
    let padding_width = column_count * 2;
    let available_content_width = width.checked_sub(border_width + padding_width)?;
    if available_content_width < column_count {
        return None;
    }

    let styled_rows = table
        .rows
        .iter()
        .map(|row| {
            row.iter()
                .map(|cell| markdown_inline_segments(cell))
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut column_widths = (0..column_count)
        .map(|column| {
            styled_rows
                .iter()
                .map(|row| styled_segments_width(&row[column]))
                .max()
                .unwrap_or(1)
                .max(1)
        })
        .collect::<Vec<_>>();
    while column_widths.iter().sum::<usize>() > available_content_width {
        let (column, _) = column_widths
            .iter()
            .enumerate()
            .filter(|(_, column_width)| **column_width > 1)
            .max_by_key(|(_, column_width)| **column_width)?;
        column_widths[column] -= 1;
    }

    let mut lines = vec![table_border(&column_widths, '', '', '')];
    for (row_index, row) in styled_rows.iter().enumerate() {
        let wrapped_cells = row
            .iter()
            .zip(&column_widths)
            .map(|(cell, column_width)| wrap_styled_segments(cell, *column_width))
            .collect::<Vec<_>>();
        let row_height = wrapped_cells.iter().map(Vec::len).max().unwrap_or(1);
        for visual_line in 0..row_height {
            let mut spans = vec![Span::styled("", Theme::dim())];
            for (column, cell_lines) in wrapped_cells.iter().enumerate() {
                spans.push(Span::styled(" ", Theme::text()));
                let cell_spans = cell_lines
                    .get(visual_line)
                    .map(|line| line.spans.clone())
                    .unwrap_or_else(|| vec![Span::raw(String::new())]);
                let cell_width = spans_display_width(&cell_spans);
                let remaining = column_widths[column].saturating_sub(cell_width);
                let (left_padding, right_padding) =
                    table_cell_padding(table.alignments[column], remaining);
                spans.push(Span::styled(" ".repeat(left_padding), Theme::text()));
                spans.extend(cell_spans.into_iter().map(|span| {
                    if row_index == 0 {
                        Span::styled(
                            span.content.into_owned(),
                            span.style.add_modifier(Modifier::BOLD),
                        )
                    } else {
                        span
                    }
                }));
                spans.push(Span::styled(" ".repeat(right_padding + 1), Theme::text()));
                spans.push(Span::styled("", Theme::dim()));
            }
            lines.push(Line::from(spans));
        }
        if row_index == 0 {
            lines.push(table_border(&column_widths, '', '', ''));
        }
    }
    lines.push(table_border(&column_widths, '', '', ''));
    Some(lines)
}

fn styled_segments_width(segments: &[StyledSegment]) -> usize {
    segments
        .iter()
        .map(|segment| display_width(&segment.text))
        .sum()
}

fn spans_display_width(spans: &[Span<'_>]) -> usize {
    spans
        .iter()
        .map(|span| display_width(span.content.as_ref()))
        .sum()
}

fn table_cell_padding(alignment: TableAlignment, remaining: usize) -> (usize, usize) {
    match alignment {
        TableAlignment::Left => (0, remaining),
        TableAlignment::Center => (remaining / 2, remaining - remaining / 2),
        TableAlignment::Right => (remaining, 0),
    }
}

fn table_border(column_widths: &[usize], left: char, junction: char, right: char) -> Line<'static> {
    let mut border = left.to_string();
    for (index, column_width) in column_widths.iter().enumerate() {
        border.push_str(&"".repeat(column_width + 2));
        border.push(if index + 1 == column_widths.len() {
            right
        } else {
            junction
        });
    }
    Line::from(Span::styled(border, Theme::dim()))
}