Files
Ely-GPUI-Components/src/documents/document/cells.rs
T
ZacharyZhang-NYandClaude Opus 5.5 6a3f3486fd
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
CI / crate (push) Failing after 13m33s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Documents: open cells keep presses; finds open their cell
A press inside the open cell stays with its field. Find opens the
cell holding a match. A closing backslash no longer escapes the
next pipe. A right press after typed pipes reads the right byte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 01:16:08 -04:00

263 lines
8.0 KiB
Rust

//! A table cell opens alone, as Typora edits tables.
use std::ops::Range;
use markdown::mdast::Node;
use crate::documents::{
markdown::{file_tree, span, walk},
render::CellAt,
};
/// A cell: which, its source, its caret.
#[derive(Debug, PartialEq, Eq)]
pub(super) struct Cell {
pub at: CellAt,
pub range: Range<usize>,
pub caret: usize,
}
/// What names a cell in a block.
#[derive(Clone, Copy, Debug)]
pub(super) enum Aim {
/// The cell holding this byte of the text.
Byte(usize),
/// This cell, its caret at the end.
At(CellAt),
First,
Last,
}
/// A way out of a cell.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Move {
Next,
Previous,
Up,
Down,
}
/// The rows of each table in `root`, in drawing order.
fn tables(root: &Node) -> Vec<Vec<&Node>> {
let mut nodes = Vec::new();
walk(root, &mut nodes);
nodes
.into_iter()
.filter(|node| matches!(node, Node::Table(_)))
.map(|table| {
table
.children()
.expect("a table holds rows")
.iter()
.collect()
})
.collect()
}
fn cells(row: &Node) -> &[Node] {
row.children().expect("a row holds cells")
}
/// The cell `aim` names in `block` of `text`.
pub(super) fn find(text: &str, block: Range<usize>, aim: Aim) -> Option<Cell> {
let root = file_tree(&text[block.clone()]);
let tables = tables(&root);
let at = match aim {
Aim::At(at) => at,
Aim::First if !tables.is_empty() => CellAt {
table: 0,
row: 0,
column: 0,
},
Aim::Last => {
let table = tables.len().checked_sub(1)?;
let row = tables[table].len() - 1;
let column = cells(tables[table][row]).len().checked_sub(1)?;
CellAt { table, row, column }
}
Aim::First => return None,
Aim::Byte(byte) => {
let byte = byte.checked_sub(block.start)?;
let table = (tables.iter()).position(|rows| {
let (first, last) = (rows[0], rows[rows.len() - 1]);
(span(first).start..=span(last).end).contains(&byte)
})?;
let rows = &tables[table];
let row = rows.iter().rposition(|row| span(row).start <= byte)?;
let column = (cells(rows[row]).iter()).rposition(|cell| span(cell).start <= byte)?;
CellAt { table, row, column }
}
};
let cell = cells(tables.get(at.table)?.get(at.row)?).get(at.column)?;
let range = written(&text[block.clone()], cell);
let caret = match aim {
Aim::Byte(byte) => (byte - block.start).clamp(range.start, range.end) - range.start,
_ => range.len(),
};
Some(Cell {
at,
range: block.start + range.start..block.start + range.end,
caret,
})
}
/// The cell `by` reaches from `at`; none past the edge.
pub(super) fn beside(text: &str, block: Range<usize>, at: CellAt, by: Move) -> Option<CellAt> {
let root = file_tree(&text[block]);
let tables = tables(&root);
let rows = tables.get(at.table)?;
let width = |row: usize| rows.get(row).map_or(0, |row| cells(row).len());
let (row, column) = match by {
Move::Next if at.column + 1 < width(at.row) => (at.row, at.column + 1),
Move::Next => (at.row + 1, 0),
Move::Previous if at.column > 0 => (at.row, at.column - 1),
Move::Previous => {
let row = at.row.checked_sub(1)?;
(row, width(row).checked_sub(1)?)
}
Move::Up => (at.row.checked_sub(1)?, at.column),
Move::Down => (at.row + 1, at.column),
};
let column = column.min(width(row).checked_sub(1)?);
Some(CellAt {
table: at.table,
row,
column,
})
}
/// A cell's source; empty, just past its opening pipe.
fn written(text: &str, cell: &Node) -> Range<usize> {
let inside = cell.children().expect("a cell holds inlines");
if let (Some(first), Some(last)) = (inside.first(), inside.last()) {
return span(first).start..span(last).end;
}
let span = span(cell);
let mut at = span.start;
if text[at..span.end].starts_with('|') {
at += 1;
}
if text[at..span.end].starts_with([' ', '\t']) {
at += 1;
}
at..at
}
/// `typed` with each bare `|` escaped.
fn guarded(typed: &str) -> String {
let mut out = String::with_capacity(typed.len());
let mut slashes = 0;
for ch in typed.chars() {
if ch == '|' && slashes % 2 == 0 {
out.push('\\');
}
slashes = if ch == '\\' { slashes + 1 } else { 0 };
out.push(ch);
}
out
}
/// `typed` as a cell keeps it, its closing pipe intact.
pub(super) fn kept(typed: &str) -> String {
let mut out = guarded(typed);
let slashes = out.len() - out.trim_end_matches('\\').len();
if slashes % 2 == 1 {
out.push('\\');
}
out
}
/// Byte `at` of `typed` within `kept(typed)`.
pub(super) fn kept_at(typed: &str, at: usize) -> usize {
match at == typed.len() {
true => kept(typed).len(),
false => guarded(&typed[..at]).len(),
}
}
#[cfg(test)]
mod tests {
use super::{Aim, CellAt, Move, beside, find, guarded, kept, kept_at};
const TABLE: &str = "intro\n\n| a | **b** |\n| --- | --- |\n| c | |\n| d | e |\n";
fn block() -> std::ops::Range<usize> {
7..TABLE.len() - 1
}
fn at(row: usize, column: usize) -> CellAt {
CellAt {
table: 0,
row,
column,
}
}
#[test]
fn a_byte_opens_its_cell_with_the_caret_there() {
let byte = TABLE.find("**b**").expect("in the table") + 2;
let cell = find(TABLE, block(), Aim::Byte(byte)).expect("a cell");
assert_eq!(cell.at, at(0, 1));
assert_eq!(&TABLE[cell.range.clone()], "**b**");
assert_eq!(cell.caret, 2);
}
#[test]
fn an_empty_cell_opens_between_its_pipes() {
let cell = find(TABLE, block(), Aim::At(at(1, 1))).expect("a cell");
assert!(cell.range.is_empty());
let written = [&TABLE[..cell.range.start], "x", &TABLE[cell.range.end..]].concat();
assert!(written.contains("| c | x |"), "{written:?}");
}
#[test]
fn ends_open_the_first_and_last_cells() {
let first = find(TABLE, block(), Aim::First).expect("a cell");
assert_eq!((first.at, &TABLE[first.range]), (at(0, 0), "a"));
let last = find(TABLE, block(), Aim::Last).expect("a cell");
assert_eq!((last.at, &TABLE[last.range]), (at(2, 1), "e"));
assert_eq!(find("plain\n", 0..5, Aim::First), None);
}
#[test]
fn moves_wrap_rows_and_stop_at_edges() {
let to = |from, by| beside(TABLE, block(), from, by);
assert_eq!(to(at(0, 1), Move::Next), Some(at(1, 0)));
assert_eq!(to(at(1, 0), Move::Previous), Some(at(0, 1)));
assert_eq!(to(at(0, 0), Move::Previous), None);
assert_eq!(to(at(2, 1), Move::Next), None);
assert_eq!(to(at(1, 1), Move::Up), Some(at(0, 1)));
assert_eq!(to(at(2, 0), Move::Down), None);
}
#[test]
fn bare_pipes_escape_and_escaped_stay() {
assert_eq!(guarded("a|b"), "a\\|b");
assert_eq!(guarded("a\\|b"), "a\\|b");
assert_eq!(guarded("a\\\\|b"), "a\\\\\\|b");
}
#[test]
fn a_closing_backslash_keeps_the_next_pipe() {
assert_eq!(kept("a\\"), "a\\\\");
assert_eq!(kept("a\\\\"), "a\\\\");
assert_eq!(kept_at("a\\", 2), 3);
assert_eq!(kept_at("a\\b", 2), 2);
let text = "|a|b|\n|---|---|\n|c|d|\n";
let cell = find(text, 0..text.len() - 1, Aim::First).expect("a cell");
let written = [
&text[..cell.range.start],
&kept("a\\"),
&text[cell.range.end..],
]
.concat();
let b = find(&written, 0..written.len() - 1, Aim::At(at(0, 1))).expect("still a table");
assert_eq!(&written[b.range], "b");
}
#[test]
fn typed_pipes_shift_later_bytes() {
assert_eq!(kept_at("||x", 2), 4);
}
}