Documents: cells keep their row's pipes; long reads finish cell moves
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

An emptied bare edge cell writes a pipe so its column stays. The caret
reads the escaped write-back. A right press beside an edited cell keeps
its place. A move that waits on a long read opens once the read lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-15 02:09:43 -04:00
1 parent 2b6bd1d369
commit f1771b3880
8 files changed
+224 -44

No files matched your search

+5 -5
View File
@@ -132,10 +132,10 @@ impl MarkdownDocument {
// A cell reads as it writes back.
let typed = match open.cell {
Some(_) => {
let closed = cells::closed(text, open.range.end);
let shift = |at: usize| cells::kept_at(typed, at, closed);
let edges = cells::edges(text, open.range.clone());
let shift = |at: usize| cells::kept_at(typed, at, edges);
picked = shift(picked.start)..shift(picked.end);
cells::kept(typed, closed)
cells::kept(typed, edges)
}
None => typed.to_string(),
};
@@ -157,8 +157,8 @@ impl MarkdownDocument {
if let Some(open) = &self.open {
let typed = open.input.read(cx).text();
// Offsets read the escaped cell; its field holds it raw.
let closed = cells::closed(self.editor.read(cx).text(), open.range.end);
let raw = open.cell.is_some() && cells::kept(typed, closed) != typed;
let edges = cells::edges(self.editor.read(cx).text(), open.range.clone());
let raw = open.cell.is_some() && cells::kept(typed, edges) != typed;
let end = offset + typed.len();
let grown = (edits.iter().map(|(_, with)| with.len()).sum::<usize>() + end)
.checked_sub(edits.iter().map(|(range, _)| range.len()).sum::<usize>());
+91 -30
View File
@@ -157,32 +157,55 @@ fn guarded(typed: &str) -> String {
out
}
/// Whether a pipe follows the cell source ending at `end`.
pub(super) fn closed(text: &str, end: usize) -> bool {
text[end..].starts_with('|')
/// What borders a cell's source in its row.
#[derive(Clone, Copy, Debug)]
pub(super) struct Edges {
/// A pipe follows at once.
closed: bool,
/// No pipe before it on its line.
first: bool,
/// No pipe after it on its line.
last: bool,
}
/// `typed` as a cell keeps it, a `closed` pipe intact.
pub(super) fn kept(typed: &str, closed: bool) -> String {
/// The borders of the cell source at `range` of `text`.
pub(super) fn edges(text: &str, range: Range<usize>) -> Edges {
let line = text[..range.start].rfind('\n').map_or(0, |at| at + 1);
let end = (text[range.end..].find('\n')).map_or(text.len(), |at| range.end + at);
let (before, after) = (&text[line..range.start], &text[range.end..end]);
Edges {
closed: after.starts_with('|'),
first: !before.contains('|'),
last: !after.contains('|'),
}
}
/// `typed` as a cell keeps it, its row's pipes intact.
pub(super) fn kept(typed: &str, edges: Edges) -> String {
let mut out = guarded(typed);
let slashes = out.len() - out.trim_end_matches('\\').len();
if closed && slashes % 2 == 1 {
if edges.closed && slashes % 2 == 1 {
out.push('\\');
}
// An emptied bare edge cell keeps its column.
if out.is_empty() && (edges.first || edges.last) {
out.push('|');
}
out
}
/// Byte `at` of `typed` within `kept(typed, closed)`.
pub(super) fn kept_at(typed: &str, at: usize, closed: bool) -> usize {
/// Byte `at` of `typed` within `kept(typed, edges)`.
pub(super) fn kept_at(typed: &str, at: usize, edges: Edges) -> usize {
match at == typed.len() {
true => kept(typed, closed).len(),
true if typed.is_empty() && !edges.first => 0,
true => kept(typed, edges).len(),
false => guarded(&typed[..at]).len(),
}
}
#[cfg(test)]
mod tests {
use super::{Aim, CellAt, Move, beside, closed, find, guarded, kept, kept_at};
use super::{Aim, CellAt, Edges, Move, beside, edges, find, guarded, kept, kept_at};
const TABLE: &str = "intro\n\n| a | **b** |\n| --- | --- |\n| c | |\n| d | e |\n";
@@ -242,36 +265,74 @@ mod tests {
assert_eq!(guarded("a\\\\|b"), "a\\\\\\|b");
}
/// Cell `(row, column)` of `text`'s one table, written as `typed`.
fn write(text: &str, row: usize, column: usize, typed: &str) -> String {
let block = 0..text.len() - 1;
let cell = find(text, block, Aim::At(at(row, column))).expect("a cell");
let kept = kept(typed, edges(text, cell.range.clone()));
[&text[..cell.range.start], &kept, &text[cell.range.end..]].concat()
}
/// The source of each cell of `text`'s one table.
fn grid(text: &str) -> Vec<Vec<String>> {
let block = 0..text.len() - 1;
(0..)
.map_while(|row| {
let cells: Vec<String> = (0..)
.map_while(|column| {
let cell = find(text, block.clone(), Aim::At(at(row, column)))?;
Some(text[cell.range].to_string())
})
.collect();
(!cells.is_empty()).then_some(cells)
})
.collect()
}
#[test]
fn a_closing_backslash_keeps_the_next_pipe() {
assert_eq!(kept("a\\", true), "a\\\\");
assert_eq!(kept("a\\\\", true), "a\\\\");
assert_eq!(kept_at("a\\", 2, true), 3);
assert_eq!(kept_at("a\\b", 2, true), 2);
let text = "|a|b|\n|---|---|\n|c|d|\n";
let cell = find(text, 0..text.len() - 1, Aim::First).expect("a cell");
let shut = closed(text, cell.range.end);
let written = [
&text[..cell.range.start],
&kept("a\\", shut),
&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");
let shut = Edges {
closed: true,
first: false,
last: false,
};
assert_eq!(kept("a\\", shut), "a\\\\");
assert_eq!(kept("a\\\\", shut), "a\\\\");
assert_eq!(kept_at("a\\", 2, shut), 3);
assert_eq!(kept_at("a\\b", 2, shut), 2);
let written = write("|a|b|\n|---|---|\n|c|d|\n", 0, 0, "a\\");
assert_eq!(grid(&written)[0], ["a\\\\", "b"]);
}
#[test]
fn a_backslash_before_padding_stays_single() {
let text = "| a\\ | b |\n| --- | --- |\n";
let cell = find(text, 0..text.len() - 1, Aim::First).expect("a cell");
assert_eq!(&text[cell.range.clone()], "a\\");
assert!(!closed(text, cell.range.end));
assert_eq!(kept("a\\", false), "a\\");
assert_eq!(write(text, 0, 0, "a\\"), text);
}
#[test]
fn an_emptied_bare_edge_cell_keeps_its_column() {
let text = "alpha | beta\n--- | ---\nc | d\n";
let rows = |written: &str| grid(written).iter().map(Vec::len).collect::<Vec<_>>();
for (row, column) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
let written = write(text, row, column, "");
assert_eq!(rows(&written), [2, 2], "{written:?}");
assert_eq!(grid(&written)[row][column], "", "{written:?}");
}
assert_eq!(grid(&write(text, 1, 0, ""))[1], ["", "d"]);
let empty = edges(text, 0..5);
assert_eq!(kept_at("", 0, empty), 1, "past its new pipe");
let last = edges(text, 8..12);
assert_eq!(kept_at("", 0, last), 0, "before its new pipe");
}
#[test]
fn typed_pipes_shift_later_bytes() {
assert_eq!(kept_at("||x", 2, false), 4);
let open = Edges {
closed: false,
first: false,
last: false,
};
assert_eq!(kept_at("||x", 2, open), 4);
}
}
+47 -3
View File
@@ -27,6 +27,17 @@ pub(super) enum Landing {
Cell(Aim),
}
impl Landing {
/// The same landing after `edit`, if it survives.
fn moved(self, edit: &Option<(Range<usize>, usize)>) -> Option<Self> {
Some(match self {
Self::At(at) => Self::At(moved(at, edit)?),
Self::Cell(Aim::Byte(at)) => Self::Cell(Aim::Byte(moved(at, edit)?)),
other => other,
})
}
}
/// Where byte `at` lies after `edit`, if it survives.
pub(super) fn moved(at: usize, edit: &Option<(Range<usize>, usize)>) -> Option<usize> {
match edit {
@@ -69,6 +80,7 @@ impl MarkdownDocument {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.waiting = None;
let pressed = match ix.map(|ix| self.parsed.blocks.get(ix)) {
Some(None) => return log::warn!("markdown document: a press on a block read away"),
pressed => pressed.flatten().map(|block| block.range.start),
@@ -77,6 +89,19 @@ impl MarkdownDocument {
if written.is_some() {
self.reparse(true, cx);
}
if !self.read_now(cx) && written.is_some() {
// A long write-back reads first; it opens after.
let start = match pressed {
None => Some(None),
Some(start) => moved(start, &written).map(Some),
};
let (Some(start), Some(landing)) = (start, landing.moved(&written)) else {
return log::warn!("markdown document: the pressed block was written over");
};
log::info!("markdown document: a block opens once its text is read");
self.waiting = Some((start, landing, self.edits + self.writes as u64));
return;
}
if !self.read_now(cx) {
return log::warn!("markdown document: pressed while the text is read again");
}
@@ -188,6 +213,25 @@ impl MarkdownDocument {
cx.notify();
}
/// A block asked open before its text was read, now opened.
pub(super) fn open_waiting(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.waiting.is_none() || !self.read_now(cx) {
return;
}
let (start, landing, expected) = self.waiting.take().expect("checked above");
if self.edits != expected {
return log::info!("markdown document: a waiting block lapsed with its text");
}
let ix = match start {
None => None,
Some(start) => match (self.parsed.blocks.iter()).position(|b| b.range.start == start) {
Some(ix) => Some(ix),
None => return log::warn!("markdown document: the waiting block was read away"),
},
};
self.open(ix, landing, window, cx);
}
/// Takes the focus a dropped block still holds.
pub(super) fn take_orphan(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(orphan) = self.orphan.take() else {
@@ -231,15 +275,15 @@ impl MarkdownDocument {
let typed = open.input.read(cx).text().to_string();
let caret = open.input.read(cx).cursor();
let text = self.editor.read(cx).text();
let closed = cells::closed(text, open.range.end);
let edges = cells::edges(text, open.range.clone());
let (lead, written) = match open.append {
true if typed.is_empty() => return None,
true => (joint(text).len(), format!("{}{typed}", joint(text))),
false if open.cell.is_some() => (0, cells::kept(&typed, closed)),
false if open.cell.is_some() => (0, cells::kept(&typed, edges)),
false => (0, typed.clone()),
};
let caret = match open.cell {
Some(_) => cells::kept_at(&typed, caret, closed),
Some(_) => cells::kept_at(&typed, caret, edges),
None => caret,
};
let at = open.range.start + lead + caret;
+6 -1
View File
@@ -109,6 +109,8 @@ pub struct MarkdownDocument {
writes: usize,
/// A match asked before its text was read; its version.
showing: Option<(Range<usize>, u64)>,
/// A block asked open before its text was read.
waiting: Option<(Option<usize>, edit::Landing, u64)>,
look: DocumentLook,
/// Typing in the open block, counted.
typed: u64,
@@ -181,6 +183,7 @@ impl MarkdownDocument {
asks: 0,
writes: 0,
showing: None,
waiting: None,
look: DocumentLook::default(),
typed: 0,
centered: None,
@@ -246,7 +249,9 @@ impl MarkdownDocument {
let written = match open.append {
true if typed.is_empty() => String::new(),
true => format!("{}{typed}", joint(text)),
false if open.cell.is_some() => cells::kept(typed, cells::closed(text, open.range.end)),
false if open.cell.is_some() => {
cells::kept(typed, cells::edges(text, open.range.clone()))
}
false => typed.to_string(),
};
[&text[..open.range.start], &written, &text[open.range.end..]].concat()
+10 -2
View File
@@ -1,6 +1,6 @@
use gpui::{App, Context, Focusable as _, ListOffset, ScrollStrategy, Window, px};
use super::{MarkdownDocument, Placement};
use super::{MarkdownDocument, Placement, cells};
use crate::documents::{MarkdownMode, modes::joint};
/// Line and column, from zero, of byte `at` in `text`.
@@ -25,7 +25,15 @@ impl MarkdownDocument {
true => joint(editor.text()).len(),
false => 0,
};
point(&self.text(cx), open.range.start + joined + input.cursor())
// A cell's caret reads its escaped write-back.
let cursor = match open.cell {
Some(_) => {
let edges = cells::edges(editor.text(), open.range.clone());
cells::kept_at(input.text(), input.cursor(), edges)
}
None => input.cursor(),
};
point(&self.text(cx), open.range.start + joined + cursor)
}
/// The selected Markdown: the source's, or the open block's.
+5 -3
View File
@@ -187,7 +187,9 @@ impl MarkdownDocument {
let now = (self.parsed.blocks.iter())
.find(|block| block.range.start <= at && at <= block.range.end)
.map(|block| self.drawn(block));
if written.is_some() && pressed.is_some() && now != pressed {
// A cell beside the written one keeps its place.
let beside = pointed.table && now.as_ref().is_some_and(|now| now.0 == TargetBlock::Table);
if written.is_some() && pressed.is_some() && now != pressed && !beside {
return log::warn!("markdown document: the write-back changed the pressed block");
}
self.editor
@@ -220,8 +222,8 @@ impl MarkdownDocument {
// A cell's typed pipes shift its bytes.
let pressed = match open.cell {
Some(_) => {
let closed = cells::closed(self.editor.read(cx).text(), open.range.end);
cells::kept_at(input.text(), pressed, closed)
let edges = cells::edges(self.editor.read(cx).text(), open.range.clone());
cells::kept_at(input.text(), pressed, edges)
}
None => pressed,
};
+59
View File
@@ -241,3 +241,62 @@ fn a_right_press_on_the_open_cells_padding_points_at_its_end(cx: &mut TestAppCon
);
assert_eq!(opened(&document, cx), "alpha||", "the cell stays open");
}
#[gpui::test]
fn the_caret_after_a_typed_pipe_reads_the_escaped_cell(cx: &mut TestAppContext) {
for (typed, column) in [("|中", 5), ("|x", 5)] {
let (document, cx) = open(WORDS, MarkdownMode::Visual, cx);
press_cell(0, 0, cx);
cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
cx.simulate_input(typed);
let caret = document.read_with(cx, |document, cx| document.caret(cx));
assert_eq!(caret, (0, column), "{typed}");
}
}
#[gpui::test]
fn emptied_bare_edge_cells_keep_the_table(cx: &mut TestAppContext) {
let cases = [
(0, 0, 5, "| | beta\n--- | ---\nc | d\n"),
(0, 1, 4, "alpha | |\n--- | ---\nc | d\n"),
(1, 0, 1, "alpha | beta\n--- | ---\n| | d\n"),
];
for (row, column, chars, written) in cases {
let (document, cx) = open(BARE, MarkdownMode::Visual, cx);
press_cell(row, column, cx);
for _ in 0..chars {
cx.simulate_keystrokes("backspace");
}
cx.simulate_keystrokes("escape");
assert_eq!(text(&document, cx), written);
press_cell(1, 1, cx);
assert_eq!(opened(&document, cx), "d", "{written:?} is still a table");
}
}
#[gpui::test]
fn a_right_press_beside_an_edited_cell_points(cx: &mut TestAppContext) {
let (document, cx) = open(WORDS, MarkdownMode::Visual, cx);
let heard = heard(&document, cx);
press_cell(0, 0, cx);
cx.simulate_input("X");
press_cell_with(0, 1, MouseButton::Right, cx);
let at = heard.borrow().last().expect("heard").at;
let written = "| alphaX | beta |\n| --- | --- |\n| c | d |\n";
assert_eq!(text(&document, cx), written);
assert_eq!(at, written.find("beta").expect("the cell") + 4);
}
#[gpui::test]
fn tab_in_a_long_document_opens_the_next_cell_once_read(cx: &mut TestAppContext) {
let long = format!("{WORDS}\n```\n{}\n```\n", "x".repeat(super::super::AT_ONCE));
let (document, cx) = open(&long, MarkdownMode::Visual, cx);
document.update_in(cx, |document, window, cx| document.reveal(0, window, cx));
super::frames(cx);
press_cell(0, 0, cx);
cx.simulate_input("X");
cx.simulate_keystrokes("tab");
cx.run_until_parked();
assert_eq!(opened(&document, cx), "beta");
assert!(text(&document, cx).starts_with("| alphaX | beta |"));
}
+1
View File
@@ -230,6 +230,7 @@ impl Render for MarkdownDocument {
// A top line asked before its text was read.
self.place_top(window, cx);
self.show_waiting(window, cx);
self.open_waiting(window, cx);
let colors = cx.theme().colors.clone();
let root = div()
.size_full()