documents: presses read back through quotes, code, HTML and front matter
Each drawn character finds its written one, skipping only white space, escapes and quote marks, so a press lands on the character itself. Code, front matter and link definitions read back line for line. An HTML block keeps each text run's source byte, and an alert keeps its text past the marker; alert() now takes the source. A secondary press refuses a target its write-back changed, and its focus no longer opens an empty document. Run's element stack moves to tree/stack.rs and the HTML-block reader to tree/html_block.rs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5af11fe489
commit
83d747d87e
16 files changed
+868
-480
No files matched your search
+116
-38
@@ -1,4 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::{iter::Peekable, ops::Range, str::CharIndices};
|
||||
|
||||
use markdown::mdast::Node;
|
||||
|
||||
@@ -27,14 +27,36 @@ pub(super) fn written(node: &Node, source: &str) -> Option<Range<usize>> {
|
||||
Some(whole.start + marks..whole.end)
|
||||
}
|
||||
|
||||
/// Where a fenced block's `text` starts, if written as drawn.
|
||||
pub(super) fn code_from(source: &str, node: &Node, text: &str) -> Option<usize> {
|
||||
/// Where a block's text lines start: past a fence line.
|
||||
pub(super) fn body(source: &str, node: &Node) -> usize {
|
||||
let whole = span(node);
|
||||
let written = &source[whole.clone()];
|
||||
let body = written.find('\n')? + 1;
|
||||
written[body..]
|
||||
.starts_with(text)
|
||||
.then_some(whole.start + body)
|
||||
let fenced = matches!(node, Node::Yaml(_) | Node::Toml(_))
|
||||
|| ["```", "~~~"]
|
||||
.iter()
|
||||
.any(|fence| written.starts_with(fence));
|
||||
match fenced {
|
||||
true => written
|
||||
.find('\n')
|
||||
.map_or(whole.end, |end| whole.start + end + 1),
|
||||
false => whole.start,
|
||||
}
|
||||
}
|
||||
|
||||
/// Source byte under drawn byte `at`; lines start at `from`.
|
||||
pub(crate) fn lines_at(source: &str, from: usize, drawn: &str, at: usize) -> Option<usize> {
|
||||
let line = drawn[..at].matches('\n').count();
|
||||
let column = at - drawn[..at].rfind('\n').map_or(0, |end| end + 1);
|
||||
let shown = drawn.split('\n').nth(line)?;
|
||||
let mut start = from;
|
||||
for _ in 0..line {
|
||||
start += source.get(start..)?.find('\n')? + 1;
|
||||
}
|
||||
let written = source[start..].split('\n').next()?.trim_end_matches('\r');
|
||||
// Each drawn line ends its written one.
|
||||
written
|
||||
.ends_with(shown)
|
||||
.then(|| start + written.len() - shown.len() + column)
|
||||
}
|
||||
|
||||
impl Drawn {
|
||||
@@ -58,19 +80,15 @@ impl Drawn {
|
||||
.chain(rows.iter_mut().flatten())
|
||||
.flatten()
|
||||
.for_each(|node| node.rebase(start)),
|
||||
Drawn::Code { from, .. } => {
|
||||
if let Some(from) = from {
|
||||
*from -= start;
|
||||
}
|
||||
}
|
||||
Drawn::Code { from, .. } | Drawn::FrontMatter { from, .. } => *from -= start,
|
||||
Drawn::Image { at, .. } => *at -= start,
|
||||
Drawn::Rule | Drawn::Math(_) | Drawn::Toc(_) | Drawn::FrontMatter(_) => {}
|
||||
Drawn::Rule | Drawn::Math(_) | Drawn::Toc(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Inline {
|
||||
/// Source byte under drawn byte `at`, if a piece holds it.
|
||||
/// Source byte under drawn byte `at`, in some piece.
|
||||
pub(crate) fn source_at(&self, source: &str, at: usize) -> Option<usize> {
|
||||
let piece = self.sources.partition_point(|(start, _)| *start <= at);
|
||||
let (start, span) = self.sources.get(piece.checked_sub(1)?)?;
|
||||
@@ -80,40 +98,100 @@ impl Inline {
|
||||
.map_or(self.text.len(), |(next, _)| *next);
|
||||
let drawn = self.text.get(*start..end.max(*start))?;
|
||||
let written = source.get(span.clone())?;
|
||||
// Text that does not read back lands at its start.
|
||||
// Unreadable text lands at its start.
|
||||
Some(span.start + align(drawn, written, at - start).unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Where drawn byte `at` was written, past joins and escapes.
|
||||
/// Where drawn byte `at` was written, past marks.
|
||||
fn align(drawn: &str, written: &str, at: usize) -> Option<usize> {
|
||||
let (mut shown, mut source) = (drawn.char_indices().peekable(), written.char_indices());
|
||||
let mut next = source.next();
|
||||
loop {
|
||||
let here = next.map_or(written.len(), |(byte, _)| byte);
|
||||
let Some(&(byte, shown_char)) = shown.peek() else {
|
||||
return Some(here);
|
||||
};
|
||||
let mut reading = Reading::new(written, 0);
|
||||
for (byte, shown) in drawn.char_indices() {
|
||||
let found = reading.take(shown)?;
|
||||
if byte >= at {
|
||||
return Some(here);
|
||||
return Some(found);
|
||||
}
|
||||
let (_, written_char) = next?;
|
||||
match (shown_char, written_char) {
|
||||
(a, b) if a == b => {
|
||||
shown.next();
|
||||
}
|
||||
(' ', '\r' | '\n') => {
|
||||
shown.next();
|
||||
next = source.next();
|
||||
while let Some((_, ' ' | '\t' | '\r' | '\n')) = next {
|
||||
next = source.next();
|
||||
}
|
||||
Some(reading.end)
|
||||
}
|
||||
|
||||
/// Finds drawn chars, front to back, in written text.
|
||||
struct Reading<'a> {
|
||||
written: Peekable<CharIndices<'a>>,
|
||||
base: usize,
|
||||
/// Past the last char found.
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reading<'a> {
|
||||
fn new(written: &'a str, base: usize) -> Self {
|
||||
Self {
|
||||
written: written.char_indices().peekable(),
|
||||
base,
|
||||
end: base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where `shown` was written; true if a char holds it.
|
||||
fn at(&mut self, shown: char) -> Option<(usize, bool)> {
|
||||
loop {
|
||||
let Some(&(byte, written)) = self.written.peek() else {
|
||||
return shown.is_whitespace().then_some((self.end, false));
|
||||
};
|
||||
let byte = self.base + byte;
|
||||
match (shown, written) {
|
||||
(a, b) if a == b => return Some((byte, true)),
|
||||
(a, b) if a.is_whitespace() => return Some((byte, b.is_whitespace())),
|
||||
// Joins, indents, escapes and quote marks.
|
||||
(_, ' ' | '\t' | '\r' | '\n' | '\\' | '>') => {
|
||||
self.written.next();
|
||||
}
|
||||
continue;
|
||||
_ => return None,
|
||||
}
|
||||
(_, ' ' | '\t' | '\r' | '\n' | '\\') => {}
|
||||
_ => return None,
|
||||
}
|
||||
next = source.next();
|
||||
}
|
||||
|
||||
fn take(&mut self, shown: char) -> Option<usize> {
|
||||
let (byte, held) = self.at(shown)?;
|
||||
if held {
|
||||
let (_, written) = self.written.next().expect("a held char");
|
||||
self.end = byte + written.len_utf8();
|
||||
}
|
||||
Some(byte)
|
||||
}
|
||||
}
|
||||
|
||||
/// A node's value bytes mapped to source, front to back.
|
||||
pub(super) struct Follow<'a> {
|
||||
value: &'a str,
|
||||
done: usize,
|
||||
reading: Option<Reading<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Follow<'a> {
|
||||
pub(super) fn new(value: &'a str, source: &'a str, span: Range<usize>) -> Self {
|
||||
let reading = Reading::new(&source[span.clone()], span.start);
|
||||
Self {
|
||||
value,
|
||||
done: 0,
|
||||
reading: Some(reading),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where value byte `at` was written.
|
||||
pub(super) fn to(&mut self, at: usize) -> Option<usize> {
|
||||
let reading = self.reading.as_mut()?;
|
||||
let lost = self.value[self.done..at]
|
||||
.chars()
|
||||
.any(|shown| reading.take(shown).is_none());
|
||||
self.done = at;
|
||||
if lost {
|
||||
log::warn!("markdown: a block's text lost its source");
|
||||
self.reading = None;
|
||||
return None;
|
||||
}
|
||||
let next = self.value[at..].chars().next()?;
|
||||
self.reading.as_mut()?.at(next).map(|(byte, _)| byte)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user