mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 05:42:38 +00:00
M4: the list start reset runs once per edit and leaves plain paragraphs' levels alone
insert_model cleared the copied start with one set_para_lists call per newline, each scanning the whole text, and set_para_lists also zeroed the level of plain paragraphs. Add TextData::clear_list_starts (one pass, touches only start_offset, no-op when no paragraph has a start) and call it once over the created paragraphs, only when the text has a newline. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
1 parent
b190e675b9
commit
7c3f9ce7e3
3 files changed
+77
-4
No files matched your search
@@ -886,6 +886,25 @@ impl TextData {
|
||||
}
|
||||
pl.indent = pl.indent.min(MAX_LIST_INDENT);
|
||||
}
|
||||
self.fold_paragraph_lists();
|
||||
}
|
||||
|
||||
/// Give paragraphs `a..=b` (by index) no start number of their own, as an item made by Enter
|
||||
/// has none. Touches nothing else: a plain paragraph keeps its level, a bullet its settings.
|
||||
/// One pass over the range, and nothing at all when no paragraph carries a start.
|
||||
pub fn clear_list_starts(&mut self, a: usize, b: usize) {
|
||||
if self.paragraphs.iter().all(|p| p.start_offset == 0) {
|
||||
return;
|
||||
}
|
||||
self.fill_paragraphs();
|
||||
for pl in self.paragraphs.get_mut(a..=b).unwrap_or(&mut []) {
|
||||
pl.start_offset = 0;
|
||||
}
|
||||
self.fold_paragraph_lists();
|
||||
}
|
||||
|
||||
/// One style for all paragraphs with no nesting or start folds back into `list`.
|
||||
fn fold_paragraph_lists(&mut self) {
|
||||
if let Some(first) = self.paragraphs.first().copied()
|
||||
&& first == (ParaList { list: first.list, ..ParaList::default() })
|
||||
&& self.paragraphs.iter().all(|p| *p == first)
|
||||
@@ -1104,6 +1123,25 @@ mod case_tests {
|
||||
t.set_para_lists(0, 0, |p| p.list = P);
|
||||
assert_eq!(t.para_list(0), ParaList::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_list_starts_touches_only_starts() {
|
||||
use ListStyle::{None as P, Numbered as N};
|
||||
let mut t = TextData { characters: "a\nb\nc".into(), ..Default::default() };
|
||||
t.paragraphs = vec![
|
||||
ParaList { list: N, start_offset: 4, ..ParaList::default() },
|
||||
ParaList { list: P, indent: 1, ..ParaList::default() },
|
||||
ParaList { list: N, indent: 2, start_offset: 7, ..ParaList::default() },
|
||||
];
|
||||
t.clear_list_starts(1, 2);
|
||||
assert_eq!(t.para_list(0).start_offset, 4, "outside the range");
|
||||
assert_eq!(t.para_list(1), ParaList { list: P, indent: 1, ..ParaList::default() }, "a plain paragraph keeps its level");
|
||||
assert_eq!(t.para_list(2), ParaList { list: N, indent: 2, start_offset: 0, ..ParaList::default() });
|
||||
// Out of range and no-start cases leave it alone.
|
||||
t.clear_list_starts(5, 9);
|
||||
t.clear_list_starts(0, 0);
|
||||
assert_eq!(t.para_list(0).start_offset, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -322,10 +322,9 @@ fn insert_model(s: &mut Session, text: &str, ime_commit: bool) -> Result<ImeInse
|
||||
// As measured in Figma (2026-11-11): an item made by Enter has no start of its own, so
|
||||
// unlisting the item before it makes it restart at 1 rather than at the copied start.
|
||||
// Every paragraph after the first that the insertion made loses the start it was copied.
|
||||
for (i, c) in text.chars().enumerate() {
|
||||
if c == '\n' {
|
||||
t.set_para_lists(a + i + 1, a + i + 1, |pl| pl.start_offset = 0);
|
||||
}
|
||||
if text.contains('\n') {
|
||||
let end = a + text.chars().count();
|
||||
t.clear_list_starts(t.para_of(a) + 1, t.para_of(end));
|
||||
}
|
||||
te.caret = a + text.chars().count();
|
||||
te.anchor = te.caret;
|
||||
|
||||
@@ -446,3 +446,39 @@ fn typing_a_creation_character_and_a_space_starts_a_list() {
|
||||
}
|
||||
assert_eq!((data(&s, t).characters.as_str(), lists(&s, t)), ("2) ", vec![(P, 0)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splitting_a_plain_paragraph_that_kept_its_level_keeps_the_level() {
|
||||
// Backspace at a nested item's start unlists it but keeps indent 1; Enter inside it then
|
||||
// splits it, and neither half loses the level (the start reset changes only starts).
|
||||
let mut s = Session::new();
|
||||
let t = text(&mut s, "ab\ncd");
|
||||
let ps = vec![ParaList { list: N, start_offset: 4, ..ParaList::default() }, ParaList { list: N, indent: 1, ..ParaList::default() }];
|
||||
if let figcraft_doc::NodeKind::Text(td) = &mut s.doc_mut().unwrap().get_mut(t).unwrap().kind {
|
||||
td.paragraphs = ps;
|
||||
}
|
||||
begin(&mut s, t, Some(3), false).unwrap();
|
||||
key(&mut s, "Backspace", false, false, false).unwrap();
|
||||
assert_eq!(lists(&s, t), [(N, 0), (P, 1)], "unlisted, level kept");
|
||||
set_caret(&mut s, 4, false).unwrap();
|
||||
key(&mut s, "Enter", false, false, false).unwrap();
|
||||
assert_eq!(data(&s, t).characters, "ab\nc\nd");
|
||||
assert_eq!(lists(&s, t), [(N, 0), (P, 1), (P, 1)], "both halves keep indent 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_many_line_paste_clears_every_created_start() {
|
||||
let mut s = Session::new();
|
||||
let t = text(&mut s, "x");
|
||||
s.execute("node.set", &json!({"id": t, "props": {"characters": ""}})).unwrap();
|
||||
begin(&mut s, t, Some(0), false).unwrap();
|
||||
for c in "5. x".chars() {
|
||||
insert(&mut s, &c.to_string()).unwrap();
|
||||
}
|
||||
let lines: Vec<String> = (0..2001).map(|i| format!("l{i}")).collect();
|
||||
insert(&mut s, &lines.join("\n")).unwrap();
|
||||
let d = data(&s, t);
|
||||
assert_eq!(d.paragraph_count(), 2001);
|
||||
assert_eq!(d.para_list(0).start_offset, 4, "the first item keeps its start");
|
||||
assert!((1..d.paragraph_count()).all(|p| d.para_list(p).start_offset == 0 && d.para_list(p).list == N));
|
||||
}
|
||||
Reference in new issue
Block a user