documents: shortcodes read per DOM text node as GitHub reads them; HTML5's p closers end a p; a TOC press follows editor anchors
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 10m54s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

- The first `:name:`-shaped run in a text node decides for the whole node:
  blank text or a space before it turns every shortcode, else none. Filtered
  tags are text and do not split the node. Probed on GitHub's markdown API.
- `fieldset`, `menu` and the rest of HTML5's list end an open `p`.
- `CodeEditor` carries owner anchors through each edit; the document anchors
  every heading it reads, and a TOC press goes where its anchor went.

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 00:48:26 -04:00
1 parent a744316274
commit 8e5dad8177
12 files changed
+396 -138

No files matched your search

+50 -2
View File
@@ -1,6 +1,6 @@
use std::ops::Range;
use super::state::Marks;
use super::state::{CodeEditor, Marks};
/// Where `offset` lands after `edits`, sorted and apart, each a replaced range and the length written; none when an edit replaced it.
fn moved(offset: usize, edits: &[(Range<usize>, usize)]) -> Option<usize> {
@@ -17,6 +17,21 @@ fn moved(offset: usize, edits: &[(Range<usize>, usize)]) -> Option<usize> {
Some(at as usize)
}
/// Where the character at `offset` lands; text written at it goes before.
fn carried(offset: usize, edits: &[(Range<usize>, usize)]) -> Option<usize> {
let mut at = offset as isize;
for (range, written) in edits {
if offset < range.start {
break;
}
if offset < range.end {
return None;
}
at += *written as isize - range.len() as isize;
}
Some(at as usize)
}
/// A range through `edits`; none when an edit cut into it.
fn moved_range(range: &Range<usize>, edits: &[(Range<usize>, usize)]) -> Option<Range<usize>> {
Some(moved(range.start, edits)?..moved(range.end, edits)?)
@@ -43,6 +58,9 @@ pub(crate) fn shift(marks: &mut Marks, edits: &[(Range<usize>, usize)]) {
ghost.offset = moved(ghost.offset, edits)?;
Some(ghost)
});
for anchor in &mut marks.anchors {
*anchor = anchor.and_then(|at| carried(at, edits));
}
}
/// Drops the owner's offsets when the whole text changes; they named the old one.
@@ -51,6 +69,27 @@ pub(crate) fn clear(marks: &mut Marks) {
marks.backgrounds.clear();
marks.hints.clear();
marks.ghost = None;
marks.anchors.fill(None);
}
impl CodeEditor {
/// Offsets the editor carries through each edit.
pub fn set_anchors(&mut self, offsets: impl IntoIterator<Item = usize>) {
let text = self.buffer.text();
let whole = |at: usize| {
let fits = text.is_char_boundary(at);
if !fits {
log::error!("code editor: anchor {at} splits a character");
}
fits.then_some(at)
};
self.marks.anchors = offsets.into_iter().map(whole).collect();
}
/// Where each anchor is now; none once an edit took it.
pub fn anchors(&self) -> &[Option<usize>] {
&self.marks.anchors
}
}
/// Whether `range` runs forward between whole characters of `text`.
@@ -62,7 +101,7 @@ pub(crate) fn fits(text: &str, range: &Range<usize>) -> bool {
#[cfg(test)]
mod tests {
use super::{fits, moved};
use super::{carried, fits, moved};
#[test]
fn offsets_ride_their_edits() {
@@ -80,4 +119,13 @@ mod tests {
assert_eq!(moved(8, &edits), Some(10));
assert!(fits("héllo", &(1..3)) && !fits("héllo", &cut) && !fits("ab", &(1..9)));
}
#[test]
fn anchors_go_after_text_written_at_them() {
let edits = [(2..4, 1), (6..6, 3)];
assert_eq!(carried(2, &edits), None, "its character replaced");
assert_eq!(carried(4, &edits), Some(3), "just past a replace");
assert_eq!(carried(6, &edits), Some(8), "after an insertion at it");
assert_eq!(carried(1, &edits), Some(1));
}
}
+2
View File
@@ -84,6 +84,8 @@ pub(crate) struct Marks {
pub git: Vec<(usize, GitMark)>,
pub breakpoints: BTreeSet<usize>,
pub chat: Option<usize>,
/// The owner's offsets; an edit over one drops it.
pub anchors: Vec<Option<usize>>,
}
/// The text and cursors one undo step returns to.