documents: drawn text reads back from its end, decoded as written

Each piece records how its source decodes: Markdown's escapes and
references for text, HTML's for an HTML block's runs, none for code
spans, cut as CommonMark cuts their end spaces. Matching runs from the
piece's end, so quote marks and indents at a line's start are never
taken for text, and an alert's text needs no moved position: alert()
takes no source again.

Lines split on LF, CRLF and CR alike; LineMap matches each drawn line
to the written line it ends once, so an HTML block maps in linear time
and a tab cut to spaces lands on the tab. A write-back refuses the
press when the pressed block's kind, text or drawing changed.

The hostile HTML test measures growth: a fixed 2 s budget failed under
the full suite's load on the old code too.

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-14 11:07:25 -04:00
1 parent 83d747d87e
commit d4c614cb7f
10 files changed
+536 -221

No files matched your search

+11 -6
View File
@@ -3,11 +3,10 @@ use std::rc::Rc;
use gpui::{Context, MouseButton, Window};
use markdown::mdast;
use super::{DocumentEvent, MarkdownDocument, edit::Landing, edit::moved};
use super::{DocumentEvent, MarkdownDocument, edit::Landing, edit::moved, parse::Block};
use crate::documents::{
render::{OnPoint, Place, Pointed},
tree::lines_at,
tree::{alert, is_toc},
tree::{Node, alert, is_toc, lines_at},
};
/// A top-level block's kind.
@@ -36,7 +35,7 @@ impl TargetBlock {
mdast::Node::Paragraph(_) if is_toc(node, source) => Self::Toc,
mdast::Node::Paragraph(_) => Self::Paragraph,
mdast::Node::Heading(heading) => Self::Heading(heading.depth),
mdast::Node::Blockquote(quote) if alert(quote, source).is_some() => Self::Alert,
mdast::Node::Blockquote(quote) if alert(quote).is_some() => Self::Alert,
mdast::Node::Blockquote(_) => Self::Quote,
mdast::Node::List(_) => Self::List,
mdast::Node::Code(_) => Self::Code,
@@ -95,6 +94,12 @@ impl MarkdownDocument {
})
}
/// 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();
(block.kind, text, block.nodes.clone())
}
/// Caret to `at`, else block `ix`; the owner hears.
pub(super) fn point(
&mut self,
@@ -109,7 +114,7 @@ impl MarkdownDocument {
block => block.flatten(),
};
let kind = block.map(|block| block.kind);
let pressed = block.map(|block| self.parsed.text[block.range.clone()].to_string());
let pressed = block.map(|block| self.drawn(block));
let at = at.or(block.map(|block| block.range.start));
let written = self.commit(cx);
if written.is_some() {
@@ -128,7 +133,7 @@ impl MarkdownDocument {
// A write-back may change the pressed block.
let now = (self.parsed.blocks.iter())
.find(|block| block.range.start <= at && at <= block.range.end)
.map(|block| self.parsed.text[block.range.clone()].to_string());
.map(|block| self.drawn(block));
if written.is_some() && pressed.is_some() && now != pressed {
return log::warn!("markdown document: the write-back changed the pressed block");
}
+1
View File
@@ -371,6 +371,7 @@ mod links;
mod look;
mod place;
mod point;
mod reading;
mod toc;
mod tools;
+102 -41
View File
@@ -6,11 +6,7 @@ use gpui::{Entity, Modifiers, MouseButton, Pixels, TestAppContext, VisualTestCon
use super::{open, type_in};
use crate::{
documents::{
DocumentEvent, DocumentTarget, MarkdownDocument, MarkdownMode, TargetBlock,
document::parse::parsed,
tree::{Inline, Node},
},
documents::{DocumentEvent, DocumentTarget, MarkdownDocument, MarkdownMode, TargetBlock},
theme::{ActiveTheme, TextSize},
};
@@ -191,42 +187,6 @@ fn press_lines(k: usize, line: usize, button: MouseButton, cx: &mut VisualTestCo
press_at(x, y, button, cx);
}
/// The source byte under drawn `shown` in block 0.
fn read_back(source: &str, shown: &str) -> Option<usize> {
fn find<'a>(nodes: &'a [Node], shown: &str) -> Option<&'a Inline> {
nodes.iter().find_map(|node| match node {
Node::Paragraph(inline) | Node::Heading(_, inline) if inline.text.contains(shown) => {
Some(inline)
}
Node::Quote(body) | Node::Alert { body, .. } | Node::Footnote { body, .. } => {
find(body, shown)
}
_ => None,
})
}
let parsed = parsed(source);
let block = &parsed.blocks[0];
let inline = find(block.nodes.as_ref().expect("drawn"), shown).expect("shown");
let at = inline.text.find(shown).expect("shown");
let written = inline.source_at(&source[block.range.start..], at)?;
Some(block.range.start + written)
}
#[test]
fn quote_marks_escapes_trims_and_alert_markers_read_back() {
let cases = [
("> alpha\n> beta", "eta"),
("> [!NOTE]\n> alpha beta", "beta"),
("a \\*star\\* z", "*star"),
("a ` xy ` z", "xy"),
("<div>alpha beta</div>\n", "eta"),
("> <div>\n> alpha beta</div>\n", "beta"),
];
for (source, shown) in cases {
assert_eq!(read_back(source, shown), source.find(shown), "{source:?}");
}
}
#[gpui::test]
fn a_secondary_press_in_a_quote_lands_past_its_mark(cx: &mut TestAppContext) {
let source = "> alpha\n> beta";
@@ -328,3 +288,104 @@ fn a_write_back_that_changes_the_pressed_block_points_nowhere(cx: &mut TestAppCo
);
assert_ne!(caret(&document, cx), written.find("eta").unwrap());
}
#[gpui::test]
fn crlf_lines_press_where_they_were_written(cx: &mut TestAppContext) {
let source = "```
alpha
beta
```
";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
press_lines(2, 0, MouseButton::Right, cx);
let at = source.find("pha").unwrap();
let expected = DocumentTarget {
in_code: true,
..target(at, TargetBlock::Code)
};
assert_eq!(*events.borrow(), [expected]);
press_lines(2, 0, MouseButton::Left, cx);
assert_eq!(opened_caret(&document, cx), at);
}
#[gpui::test]
fn crlf_front_matter_presses_where_written(cx: &mut TestAppContext) {
let source = "---
title: value
author: person
---
";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
press_lines(7, 0, MouseButton::Right, cx);
let at = source.find("value").unwrap();
assert_eq!(*events.borrow(), [target(at, TargetBlock::FrontMatter)]);
press_lines(7, 0, MouseButton::Left, cx);
assert_eq!(opened_caret(&document, cx), at);
}
#[gpui::test]
fn a_crlf_definition_presses_where_written(cx: &mut TestAppContext) {
let source = "[ref]: https://ely.dev
\"title\"
";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
press_on(0, 10, MouseButton::Right, cx);
assert_eq!(*events.borrow(), [target(10, TargetBlock::Definition)]);
}
#[gpui::test]
fn a_changed_definition_points_nowhere(cx: &mut TestAppContext) {
let source = "[ref]: https://old.dev
Go [there][ref]
";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
type_in(&document, Some(0), Some("[ref]: https://new.dev"), cx);
press_on(1, 4, MouseButton::Right, cx);
assert_eq!(
super::text(&document, cx),
source.replace("old.dev", "new.dev")
);
assert!(
events.borrow().is_empty(),
"the link changed under the press"
);
}
#[gpui::test]
fn a_quoted_greater_than_presses_on_itself(cx: &mut TestAppContext) {
let source = "> <div>
> >alpha beta</div>";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
let bounds = cx.debug_bounds("markdown-block-0").expect("drawn");
let (body, _) = sizes(cx);
let x = bounds.left() + px(31.0) + body * 0.6 * 0.25;
press_at(x, bounds.top() + px(10.0), MouseButton::Right, cx);
let at = source.find(">alpha").unwrap();
assert_eq!(*events.borrow(), [target(at, TargetBlock::Quote)]);
}
#[gpui::test]
fn an_escaped_backslash_presses_on_itself(cx: &mut TestAppContext) {
let source = r"a \\ z";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
press_on(0, 2, MouseButton::Right, cx);
let at = source.find(r"\ z").unwrap();
assert_eq!(*events.borrow(), [target(at, TargetBlock::Paragraph)]);
}
#[gpui::test]
fn an_html_space_moved_by_a_tag_presses_on_its_word(cx: &mut TestAppContext) {
let source = "<div>alpha <b> beta</b></div>";
let (document, cx) = open(source, MarkdownMode::Visual, cx);
let events = heard(&document, cx);
press_on(0, 8, MouseButton::Right, cx);
let at = source.find("eta").unwrap();
assert_eq!(*events.borrow(), [target(at, TargetBlock::Html)]);
}
+123
View File
@@ -0,0 +1,123 @@
//! Drawn text reads back to its source byte (R4.5).
use crate::documents::{
TargetBlock,
document::parse::parsed,
tree::{Inline, Node, lines_at},
};
/// The source byte under drawn `shown` in block 0.
fn read_back(source: &str, shown: &str) -> Option<usize> {
fn find<'a>(nodes: &'a [Node], shown: &str) -> Option<&'a Inline> {
nodes.iter().find_map(|node| match node {
Node::Paragraph(inline) | Node::Heading(_, inline) if inline.text.contains(shown) => {
Some(inline)
}
Node::Quote(body) | Node::Alert { body, .. } | Node::Footnote { body, .. } => {
find(body, shown)
}
_ => None,
})
}
let parsed = parsed(source);
let block = &parsed.blocks[0];
let inline = find(block.nodes.as_ref().expect("drawn"), shown).expect("shown");
let at = inline.text.find(shown).expect("shown");
let written = inline.source_at(&source[block.range.start..], at)?;
Some(block.range.start + written)
}
/// The source byte under drawn `word` of block 0's code.
fn code_back(source: &str, word: &str) -> Option<usize> {
fn code(nodes: &[Node]) -> (&str, usize) {
match &nodes[0] {
Node::Code { text, from, .. } => (text, *from),
Node::Quote(body) => code(body),
other => panic!("not code: {other:?}"),
}
}
let parsed = parsed(source);
let block = &parsed.blocks[0];
let (drawn, from) = code(block.nodes.as_ref().expect("drawn"));
let at = drawn.find(word).expect("drawn");
let written = lines_at(&source[block.range.start..], from, drawn, at)?;
Some(block.range.start + written)
}
#[test]
fn text_reads_back_past_marks_escapes_trims_and_markers() {
let cases = [
("> alpha\n> beta", "eta"),
("> [!NOTE]\n> alpha beta", "beta"),
("> [!NOTE] \n> alpha beta", "beta"),
("> [!NOTE]\\\n> alpha beta", "beta"),
("> [!NOTE]\n> \\*beta\\*", "*beta"),
("> [!NOTE]&#10;alpha beta", "beta"),
("a \\*star\\* z", "*star"),
("a \\\\ z", "\\ z"),
("a ` xy ` z", "xy"),
("a ` xy ` z", " xy "),
("a ` >xy ` z", ">xy"),
("a `\\\\xy` z", "xy"),
("<div>alpha beta</div>\n", "eta"),
("> <div>\n> alpha beta</div>\n", "beta"),
("> <div>\n> >alpha beta</div>", ">alpha"),
("> <div>\n> >alpha beta</div>", "beta"),
("<div>alpha <b>beta</b> gamma</div>", "gamma"),
("<div>alpha <b>beta</b> gamma</div>", "beta"),
("<div>alpha <b> beta</b></div>", "eta"),
("<div>one</div>\n<div>alpha beta</div>", "beta"),
];
for (source, shown) in cases {
assert_eq!(read_back(source, shown), source.find(shown), "{source:?}");
}
}
#[test]
fn a_reference_reads_back_to_its_ampersand() {
let source = "a &amp; b";
assert_eq!(read_back(source, "& b"), source.find('&'));
assert_eq!(read_back(source, "b"), source.find('b'));
}
#[test]
fn text_after_an_emoji_reads_back_and_before_it_lands_at_its_start() {
let source = "ab :smile: y";
assert_eq!(read_back(source, "y"), source.find('y'));
assert_eq!(read_back(source, "b"), Some(0));
}
#[test]
fn code_reads_back_line_for_line_whatever_its_line_ends() {
let cases = [
"```\nalpha\nbeta\n```\n",
"```\r\nalpha\r\nbeta\r\n```\r\n",
"```\ralpha\rbeta\r```\r",
" alpha\n beta\n",
"> ```\n> alpha\n> beta\n> ```\n",
];
for source in cases {
for word in ["pha", "eta"] {
assert_eq!(code_back(source, word), source.find(word), "{source:?}");
}
}
}
#[test]
fn code_cut_from_a_tab_reads_back_past_it() {
for spaces in 0..=3 {
for prefix in ["\t", " \t", " \t", " \t", "\t\t"] {
let indent = " ".repeat(spaces);
let source = format!("{indent}```\n{indent}alpha\n{prefix}beta\n{indent}```\n");
assert_eq!(code_back(&source, "eta"), source.find("eta"), "{source:?}");
}
}
}
#[test]
fn a_decoded_line_end_reads_without_a_written_one() {
let quote = parsed("> alpha&#10;beta");
assert_eq!(quote.blocks[0].kind, TargetBlock::Quote);
let alert = parsed("> [!NOTE]&#10;alpha beta");
assert_eq!(alert.blocks[0].kind, TargetBlock::Alert);
}