Documents: untouched cells keep their bytes; open cells take padding presses
A trailing backslash doubles only before a closing pipe. A write at a block's start keeps the block, so Tab leaves a first cell without outer pipes. Padding of the open cell moves its caret to the end, and a right press there points at the end. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6a3f3486fd
commit
2b6bd1d369
9 files changed
+171
-50
No files matched your search
@@ -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::<usize>() + end)
|
||||
.checked_sub(edits.iter().map(|(range, _)| range.len()).sum::<usize>());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ pub(super) enum Landing {
|
||||
pub(super) fn moved(at: usize, edit: &Option<(Range<usize>, usize)>) -> Option<usize> {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<usize>) -> Option<CellAt> {
|
||||
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<Self>) {
|
||||
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<Vec<Node>>) {
|
||||
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<Pixels>, cx: &mut Context<Self>) {
|
||||
/// A right press at `position`, else at the field's end.
|
||||
pub(super) fn point_open(&mut self, position: Option<Point<Pixels>>, cx: &mut Context<Self>) {
|
||||
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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user