documents: format checks nested marks, reports late refusals, reads small texts at once

Unwrapping a style now runs the crossing check on the found node, so an
end in a nested link address or strike mark refuses. Clear keeps an
autolink's backslashes. Block lookups use a sorted search, so the walk
grows linearly. A long format refuses a second one as Busy and reports
refusals, a changed text and a shown preview through FormatRefused.
markdown-rs grows with the square of its nodes, so texts read at once
only up to 16 KiB.

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 12:47:18 -04:00
1 parent d238e3b4f0
commit 0289fdaa19
6 files changed
+236 -51

No files matched your search

+149 -2
View File
@@ -1,11 +1,19 @@
//! Inline formats toggle as Typora's (R4.6).
use std::{cell::RefCell, rc::Rc, time::Instant};
use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext};
use markdown::mdast::Node;
use super::{open, opened, press, text};
use crate::documents::{
FormatRefusal, InlineFormat, LARGEST_READ, MarkdownDocument, MarkdownMode,
document::{AT_ONCE, format::toggle, visual},
DocumentEvent, FormatRefusal, InlineFormat, LARGEST_READ, MarkdownDocument, MarkdownMode,
document::{
AT_ONCE,
format::{toggle, toggle_tree},
visual,
},
markdown::file_tree,
};
/// `marked` toggled: `‸` is the caret, `⟨…⟩` the selection.
@@ -238,6 +246,21 @@ fn a_long_text_formats_once_read_apart(cx: &mut TestAppContext) {
assert!(text(&document, cx).starts_with("**x"));
}
#[gpui::test]
fn many_short_paragraphs_format_apart(cx: &mut TestAppContext) {
let source = "a\n\n".repeat(AT_ONCE / 3 + 1);
let (document, cx) = open(&source, MarkdownMode::Source, cx);
document.update(cx, |document, cx| {
document
.editor
.update(cx, |editor, cx| editor.select(std::iter::once(0..1), cx));
document.format(InlineFormat::Strong, cx).expect("taken");
});
assert_eq!(text(&document, cx), source, "not on the UI thread");
cx.run_until_parked();
assert!(text(&document, cx).starts_with("**a**\n\na"));
}
#[gpui::test]
fn a_long_format_lapses_when_the_text_moves(cx: &mut TestAppContext) {
let (document, cx) = long(cx, AT_ONCE + 1);
@@ -254,6 +277,130 @@ fn a_long_format_lapses_when_the_text_moves(cx: &mut TestAppContext) {
);
}
/// The refusals a document sends by event.
fn refusals(
document: &Entity<MarkdownDocument>,
cx: &mut VisualTestContext,
) -> Rc<RefCell<Vec<FormatRefusal>>> {
let heard = Rc::new(RefCell::new(Vec::new()));
let kept = heard.clone();
cx.update(|_, cx| {
cx.subscribe(document, move |_, event: &DocumentEvent, _| {
if let DocumentEvent::FormatRefused(why) = event {
kept.borrow_mut().push(*why);
}
})
.detach()
});
heard
}
#[gpui::test]
fn a_long_lapse_comes_by_event(cx: &mut TestAppContext) {
let (document, cx) = long(cx, AT_ONCE + 1);
let heard = refusals(&document, cx);
document.update(cx, |document, cx| {
document.format(InlineFormat::Strong, cx).expect("taken");
document
.editor
.update(cx, |editor, cx| editor.edit([(0..0, "y".to_string())], cx));
});
cx.run_until_parked();
assert_eq!(*heard.borrow(), [FormatRefusal::Changed]);
}
#[test]
fn an_unwrap_refuses_an_end_in_nested_marks() {
use InlineFormat::*;
let across = Err(FormatRefusal::Across);
assert_eq!(toggled("**[a](ht‸tps://x)**", Strong), across);
assert_eq!(toggled("**~~a~‸~**", Strong), across);
assert_eq!(toggled("<u>[a](ht‸tps://x)</u>", Underline), across);
assert_eq!(toggled("**a *b⟨* c⟩**", Strong), across);
}
#[test]
fn clear_keeps_an_autolink_backslash() {
assert_eq!(
ok(r"a‸ <https://x/\*>", InlineFormat::Clear),
r"a‸ https://x/\*"
);
}
/// The walk past the parse, on `n` then `4n` repeats.
fn linear(unit: &str, run: impl Fn(&str, &Node)) {
let time = |n: usize| {
let text = unit.repeat(n);
let root = file_tree(&text);
let start = Instant::now();
run(&text, &root);
start.elapsed()
};
let (small, large) = (time(4000), time(16000));
assert!(large < small * 8, "{small:?} then {large:?}");
}
#[test]
fn many_paragraphs_format_in_linear_time() {
linear("a\n\n", |text, root| {
toggle_tree(text, root, 0..1, InlineFormat::Strong).expect("toggled");
});
linear("**a**\n\n", |text, root| {
toggle_tree(text, root, 0..text.len(), InlineFormat::Clear).expect("cleared");
});
}
#[gpui::test]
fn a_second_long_format_refuses_while_one_runs(cx: &mut TestAppContext) {
let (document, cx) = long(cx, AT_ONCE + 1);
let second = document.update(cx, |document, cx| {
document.format(InlineFormat::Strong, cx).expect("taken");
document.format(InlineFormat::Emphasis, cx)
});
assert_eq!(second, Err(FormatRefusal::Busy));
cx.run_until_parked();
document.update(cx, |document, cx| {
document.format(InlineFormat::Emphasis, cx).expect("taken")
});
cx.run_until_parked();
assert!(text(&document, cx).starts_with("***x"));
document.update(cx, |document, cx| {
document.editor.update(cx, |editor, cx| editor.undo(cx));
});
assert!(text(&document, cx).starts_with("**x"));
assert!(!text(&document, cx).starts_with("***"));
}
#[gpui::test]
fn a_long_refusal_comes_by_event(cx: &mut TestAppContext) {
let source = format!("`code` {}", "x".repeat(AT_ONCE));
let (document, cx) = open(&source, MarkdownMode::Source, cx);
let heard = refusals(&document, cx);
let taken = document.update(cx, |document, cx| {
document
.editor
.update(cx, |editor, cx| editor.select(std::iter::once(2..2), cx));
document.format(InlineFormat::Strong, cx)
});
assert_eq!(taken, Ok(()));
cx.run_until_parked();
assert_eq!(*heard.borrow(), [FormatRefusal::Code]);
assert_eq!(text(&document, cx), source);
}
#[gpui::test]
fn a_long_format_drops_once_preview_shows(cx: &mut TestAppContext) {
let (document, cx) = long(cx, AT_ONCE + 1);
let heard = refusals(&document, cx);
document.update_in(cx, |document, window, cx| {
document.format(InlineFormat::Strong, cx).expect("taken");
document.set_mode(MarkdownMode::Preview, window, cx);
});
cx.run_until_parked();
assert!(text(&document, cx).starts_with("xx"));
assert_eq!(*heard.borrow(), [FormatRefusal::Preview]);
}
#[gpui::test]
fn a_text_past_the_largest_read_refuses(cx: &mut TestAppContext) {
let (document, cx) = long(cx, LARGEST_READ + 1);