diff --git a/src/documents/document/apart.rs b/src/documents/document/apart.rs index d42b4ac..3740f12 100644 --- a/src/documents/document/apart.rs +++ b/src/documents/document/apart.rs @@ -132,9 +132,10 @@ impl MarkdownDocument { // A cell reads as it writes back. let typed = match open.cell { Some(_) => { - let shift = |at: usize| cells::kept_at(typed, at); + let closed = cells::closed(text, open.range.end); + let shift = |at: usize| cells::kept_at(typed, at, closed); picked = shift(picked.start)..shift(picked.end); - cells::kept(typed) + cells::kept(typed, closed) } None => typed.to_string(), }; @@ -156,7 +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 raw = open.cell.is_some() && cells::kept(typed) != typed; + let closed = cells::closed(self.editor.read(cx).text(), open.range.end); + let raw = open.cell.is_some() && cells::kept(typed, closed) != typed; let end = offset + typed.len(); let grown = (edits.iter().map(|(_, with)| with.len()).sum::() + end) .checked_sub(edits.iter().map(|(range, _)| range.len()).sum::()); diff --git a/src/documents/document/cells.rs b/src/documents/document/cells.rs index c870435..d6d3c7e 100644 --- a/src/documents/document/cells.rs +++ b/src/documents/document/cells.rs @@ -157,27 +157,32 @@ fn guarded(typed: &str) -> String { out } -/// `typed` as a cell keeps it, its closing pipe intact. -pub(super) fn kept(typed: &str) -> String { +/// Whether a pipe follows the cell source ending at `end`. +pub(super) fn closed(text: &str, end: usize) -> bool { + text[end..].starts_with('|') +} + +/// `typed` as a cell keeps it, a `closed` pipe intact. +pub(super) fn kept(typed: &str, closed: bool) -> String { let mut out = guarded(typed); let slashes = out.len() - out.trim_end_matches('\\').len(); - if slashes % 2 == 1 { + if closed && slashes % 2 == 1 { out.push('\\'); } out } -/// Byte `at` of `typed` within `kept(typed)`. -pub(super) fn kept_at(typed: &str, at: usize) -> usize { +/// Byte `at` of `typed` within `kept(typed, closed)`. +pub(super) fn kept_at(typed: &str, at: usize, closed: bool) -> usize { match at == typed.len() { - true => kept(typed).len(), + true => kept(typed, closed).len(), false => guarded(&typed[..at]).len(), } } #[cfg(test)] mod tests { - use super::{Aim, CellAt, Move, beside, find, guarded, kept, kept_at}; + use super::{Aim, CellAt, Move, beside, closed, find, guarded, kept, kept_at}; const TABLE: &str = "intro\n\n| a | **b** |\n| --- | --- |\n| c | |\n| d | e |\n"; @@ -239,15 +244,16 @@ mod tests { #[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); + 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\\"), + &kept("a\\", shut), &text[cell.range.end..], ] .concat(); @@ -255,8 +261,17 @@ mod tests { assert_eq!(&written[b.range], "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\\"); + } + #[test] fn typed_pipes_shift_later_bytes() { - assert_eq!(kept_at("||x", 2), 4); + assert_eq!(kept_at("||x", 2, false), 4); } } diff --git a/src/documents/document/edit.rs b/src/documents/document/edit.rs index 11ff397..a16f738 100644 --- a/src/documents/document/edit.rs +++ b/src/documents/document/edit.rs @@ -31,7 +31,7 @@ pub(super) enum Landing { pub(super) fn moved(at: usize, edit: &Option<(Range, usize)>) -> Option { match edit { None => Some(at), - Some((replaced, _)) if at < replaced.start => Some(at), + Some((replaced, _)) if at <= replaced.start => Some(at), Some((replaced, written)) if at >= replaced.end => Some(at - replaced.len() + written), Some(_) => None, } @@ -231,14 +231,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 (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)), + false if open.cell.is_some() => (0, cells::kept(&typed, closed)), false => (0, typed.clone()), }; let caret = match open.cell { - Some(_) => cells::kept_at(&typed, caret), + Some(_) => cells::kept_at(&typed, caret, closed), None => caret, }; let at = open.range.start + lead + caret; @@ -303,6 +304,7 @@ mod tests { let edit = Some((2..6, 9)); assert_eq!(super::moved(10, &edit), Some(15)); assert_eq!(super::moved(1, &edit), Some(1), "before stays"); + assert_eq!(super::moved(2, &edit), Some(2), "its start stays"); assert_eq!(super::moved(3, &edit), None, "inside is gone"); assert_eq!(super::moved(7, &None), Some(7)); } diff --git a/src/documents/document/mod.rs b/src/documents/document/mod.rs index 9bb5f88..7b0e9a8 100644 --- a/src/documents/document/mod.rs +++ b/src/documents/document/mod.rs @@ -246,7 +246,7 @@ 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), + false if open.cell.is_some() => cells::kept(typed, cells::closed(text, open.range.end)), false => typed.to_string(), }; [&text[..open.range.start], &written, &text[open.range.end..]].concat() diff --git a/src/documents/document/point.rs b/src/documents/document/point.rs index e8e7754..204e5a6 100644 --- a/src/documents/document/point.rs +++ b/src/documents/document/point.rs @@ -1,6 +1,6 @@ -use std::rc::Rc; +use std::{ops::Range, rc::Rc}; -use gpui::{AppContext as _, Context, MouseButton, Pixels, Point, Window}; +use gpui::{AppContext as _, Context, Focusable as _, MouseButton, Pixels, Point, Window}; use markdown::mdast; use super::{ @@ -12,7 +12,7 @@ use super::{ }; use crate::documents::{ markdown::{file_tree, span, walk}, - render::{OnPoint, Place, Pointed}, + render::{CellAt, OnPoint, Place, Pointed}, tree::{Node, Reader, alert, is_toc, lines_at}, }; @@ -102,15 +102,44 @@ impl MarkdownDocument { }; let ix = Some(ix); document - .update(cx, |document, cx| match button { - MouseButton::Left => document.open(ix, landing, window, cx), - MouseButton::Right => document.point(ix, at, pointed, window, cx), - _ => {} + .update(cx, |document, cx| { + // The open cell's padding keeps its typing. + let open = match pointed.place { + Place::Cell(cell) => document.open_cell(&block) == Some(cell), + _ => false, + }; + match (button, open) { + (MouseButton::Left, true) => document.cell_end(window, cx), + (MouseButton::Right, true) => document.point_open(None, cx), + (MouseButton::Left, false) => document.open(ix, landing, window, cx), + (MouseButton::Right, false) => document.point(ix, at, pointed, window, cx), + _ => {} + } }) .expect("a block's drawing lives in its document"); }) } + /// The cell open in `block`, if any. + fn open_cell(&self, block: &Range) -> Option { + let open = self.open.as_ref()?; + let inside = block.start <= open.range.start && open.range.end <= block.end; + open.cell.filter(|_| inside) + } + + /// Caret to the open cell's end. + fn cell_end(&mut self, window: &mut Window, cx: &mut Context) { + let Some(open) = &self.open else { + return; + }; + let input = open.input.clone(); + input.update(cx, |input, cx| { + let end = input.text().len(); + input.select(end..end, cx); + }); + window.focus(&input.focus_handle(cx), cx); + } + /// A block's kind, text and what it draws. fn drawn(&self, block: &Block) -> (TargetBlock, String, Option>) { let text = self.parsed.text[block.range.clone()].to_string(); @@ -178,16 +207,22 @@ impl MarkdownDocument { cx.notify(); } - /// A right press at `position`; the open block stays open. - pub(super) fn point_open(&mut self, position: Point, cx: &mut Context) { + /// A right press at `position`, else at the field's end. + pub(super) fn point_open(&mut self, position: Option>, cx: &mut Context) { let Some(open) = &self.open else { return log::warn!("markdown document: a press on a block closed since"); }; let input = open.input.read(cx); - let pressed = input.offset_for_point(position); + let pressed = match position { + Some(position) => input.offset_for_point(position), + None => input.text().len(), + }; // A cell's typed pipes shift its bytes. let pressed = match open.cell { - Some(_) => cells::kept_at(input.text(), pressed), + Some(_) => { + let closed = cells::closed(self.editor.read(cx).text(), open.range.end); + cells::kept_at(input.text(), pressed, closed) + } None => pressed, }; let taken = self.snapshot(cx); diff --git a/src/documents/document/tests/cells.rs b/src/documents/document/tests/cells.rs index 56fbd8f..cf0e45f 100644 --- a/src/documents/document/tests/cells.rs +++ b/src/documents/document/tests/cells.rs @@ -171,3 +171,73 @@ fn a_right_press_on_a_cell_points_at_it(cx: &mut TestAppContext) { let at = heard.borrow().last().expect("heard").at; assert_eq!(at, WORDS.rfind(" d").expect("the cell") + 2); } + +#[gpui::test] +fn an_untouched_backslash_cell_keeps_its_bytes(cx: &mut TestAppContext) { + let table = "| a\\ | b |\n| --- | --- |\n| c | d |\n"; + let (document, cx) = open(table, MarkdownMode::Visual, cx); + press_cell(0, 0, cx); + assert_eq!(opened(&document, cx), "a\\"); + document.update_in(cx, |document, window, cx| { + document.set_mode(MarkdownMode::Source, window, cx) + }); + assert_eq!(text(&document, cx), table); +} + +const BARE: &str = "alpha | beta\n--- | ---\nc | d\n"; + +#[gpui::test] +fn tab_leaves_an_edited_first_cell_without_outer_pipes(cx: &mut TestAppContext) { + let (document, cx) = open(BARE, MarkdownMode::Visual, cx); + press_cell(0, 0, cx); + cx.simulate_input("X"); + cx.simulate_keystrokes("tab"); + assert_eq!(opened(&document, cx), "beta"); + assert_eq!(text(&document, cx), "alphaX | beta\n--- | ---\nc | d\n"); +} + +#[gpui::test] +fn a_neighbour_press_leaves_an_edited_first_cell(cx: &mut TestAppContext) { + let (document, cx) = open(BARE, MarkdownMode::Visual, cx); + press_cell(0, 0, cx); + cx.simulate_input("X"); + press_cell(0, 1, cx); + assert_eq!(opened(&document, cx), "beta"); +} + +#[gpui::test] +fn the_open_cells_padding_lands_at_its_end_in_one_step(cx: &mut TestAppContext) { + let (document, cx) = open(WORDS, MarkdownMode::Visual, cx); + press_cell(0, 0, cx); + cx.simulate_input("X"); + let x = x_of(&document, 1, cx); + press_field(x, MouseButton::Left, cx); + assert_eq!(caret(&document, cx), 1); + press_cell(0, 0, cx); + assert_eq!(caret(&document, cx), 6, "the padding lands at the end"); + cx.simulate_input("Y"); + cx.simulate_keystrokes("escape"); + assert_eq!( + text(&document, cx), + "| alphaXY | beta |\n| --- | --- |\n| c | d |\n" + ); + let editor = document.read_with(cx, |document, _| document.editor().clone()); + editor.update(cx, |editor, cx| editor.undo(cx)); + assert_eq!(text(&document, cx), WORDS); +} + +#[gpui::test] +fn a_right_press_on_the_open_cells_padding_points_at_its_end(cx: &mut TestAppContext) { + let (document, cx) = open(WORDS, MarkdownMode::Visual, cx); + let heard = heard(&document, cx); + press_cell(0, 0, cx); + cx.simulate_input("||"); + press_cell_with(0, 0, MouseButton::Right, cx); + let target = heard.borrow().last().cloned().expect("heard"); + assert!(target.in_table); + assert_eq!( + target.at, + WORDS.find("alpha").expect("the cell") + "alpha\\|\\|".len() + ); + assert_eq!(opened(&document, cx), "alpha||", "the cell stays open"); +} diff --git a/src/documents/document/tests/secondary.rs b/src/documents/document/tests/secondary.rs index 044064b..c6ef49d 100644 --- a/src/documents/document/tests/secondary.rs +++ b/src/documents/document/tests/secondary.rs @@ -176,8 +176,8 @@ fn only_the_latest_press_in_an_open_block_is_heard(cx: &mut TestAppContext) { }) }); document.update(cx, |document, cx| { - document.point_open(spots[0], cx); - document.point_open(spots[1], cx); + document.point_open(Some(spots[0]), cx); + document.point_open(Some(spots[1]), cx); }); cx.run_until_parked(); let links: Vec<_> = (heard.borrow().iter()) diff --git a/src/documents/document/visual.rs b/src/documents/document/visual.rs index c555816..85521b7 100644 --- a/src/documents/document/visual.rs +++ b/src/documents/document/visual.rs @@ -175,7 +175,7 @@ impl MarkdownDocument { cx.listener(|document, event: &MouseDownEvent, window, cx| { window.prevent_default(); cx.stop_propagation(); - document.point_open(event.position, cx); + document.point_open(Some(event.position), cx); }) } diff --git a/src/documents/render/table.rs b/src/documents/render/table.rs index d6a84c1..39bda49 100644 --- a/src/documents/render/table.rs +++ b/src/documents/render/table.rs @@ -59,24 +59,21 @@ impl Draw<'_> { cell.font_weight(FontWeight::MEDIUM) .text_color(colors.fg_muted) }); - match open { - // Presses in the open cell stay with its field. - Some(field) => drawn - .child(div().w_full().child(field)) + let inside = match open { + // Presses on the open field stay with it. + Some(field) => div() + .w_full() + .child(field) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Right, |_, _, cx| cx.stop_propagation()), - None => self.pressed_cell( - drawn.child( - div() - .max_w_full() - .flex() - .flex_col() - .gap_1() - .children(self.nodes(blocks, cx)), - ), - at, - ), - } + None => div() + .max_w_full() + .flex() + .flex_col() + .gap_1() + .children(self.nodes(blocks, cx)), + }; + self.pressed_cell(drawn.child(inside), at) }; div() .flex()