mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 02:34:33 +00:00
M4: a typed number and full stop start a numbered list at that number, as measured in Figma
Typing `5. ` at the start of a plain paragraph starts a numbered list whose first item reads 5 (Figma has no start-number field); the next item goes on with 6. Digits only, 1 to 9999; `0. `, `12a. ` and `2)` stay text (not measured), `1)` works as before. The measured restart rows (plain paragraph, bullet item) are no longer marked silent. 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:
co-authored by
Claude Sonnet 5.5
parent
9e815546ba
commit
b8f05d1948
@@ -284,20 +284,35 @@ fn insert_model(s: &mut Session, text: &str, ime_commit: bool) -> Result<ImeInse
|
||||
let text = normalized;
|
||||
edit(s, |t, te| {
|
||||
let (a, b) = range(te);
|
||||
// "- ", "* ", "1. " or "1) " typed at the start of a plain paragraph starts a list (the
|
||||
// creation characters of Create bulleted and numbered lists).
|
||||
// "- ", "* ", "<number>. " or "1) " typed at the start of a plain paragraph starts a list
|
||||
// (the creation characters of Create bulleted and numbered lists). As measured in Figma
|
||||
// (2026-11-11), a typed "5. " starts the numbered list at 5 (there is no start-number
|
||||
// field). "0. " and a bracket after a number other than 1 are not measured: plain text.
|
||||
if text == " " && a == b && t.para_list(t.para_of(a)).list == ListStyle::None {
|
||||
let chars: Vec<char> = t.characters.chars().collect();
|
||||
let start = line_start(&chars, a);
|
||||
let typed: String = chars.get(start..a).unwrap_or(&[]).iter().collect();
|
||||
let mut first = 1_u32;
|
||||
let list = match typed.as_str() {
|
||||
"-" | "*" => ListStyle::Bulleted,
|
||||
"1." | "1)" => ListStyle::Numbered,
|
||||
_ => ListStyle::None,
|
||||
"1)" => ListStyle::Numbered,
|
||||
_ => match typed.strip_suffix('.').filter(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit())) {
|
||||
Some(d) => match d.parse::<u32>() {
|
||||
Ok(n) if (1..=MAX_TYPED_LIST_START).contains(&n) => {
|
||||
first = n;
|
||||
ListStyle::Numbered
|
||||
}
|
||||
_ => ListStyle::None,
|
||||
},
|
||||
None => ListStyle::None,
|
||||
},
|
||||
};
|
||||
if list != ListStyle::None {
|
||||
t.replace_range(start, a, "");
|
||||
t.set_para_lists(start, start, |pl| pl.list = list);
|
||||
t.set_para_lists(start, start, |pl| {
|
||||
pl.list = list;
|
||||
pl.start_offset = first - 1;
|
||||
});
|
||||
(te.caret, te.anchor) = (start, start);
|
||||
outcome = ImeInsertion::Transformed;
|
||||
return;
|
||||
@@ -310,6 +325,9 @@ fn insert_model(s: &mut Session, text: &str, ime_commit: bool) -> Result<ImeInse
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
/// The largest number a typed `N. ` can start a numbered list at.
|
||||
const MAX_TYPED_LIST_START: u32 = 9999;
|
||||
|
||||
/// What a list key does to the selected items.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ListMove {
|
||||
|
||||
@@ -84,7 +84,11 @@ fn typed_list_starts() {
|
||||
("- ", "", B, "hyphen and space: bullets"),
|
||||
("* ", "", B, "asterisk and space: bullets"),
|
||||
("1. ", "", N, "one, full stop, space: numbers"),
|
||||
("2. ", "2. ", P, "silent: only 1. starts a numbered list"),
|
||||
("2. ", "", N, "as measured in Figma: a typed number starts the list there"),
|
||||
("0. ", "0. ", P, "zero is not measured: text"),
|
||||
("12a. ", "12a. ", P, "digits only: text"),
|
||||
("1) ", "", N, "one and a closing bracket: numbers"),
|
||||
("2) ", "2) ", P, "silent: only 1) starts with a bracket"),
|
||||
("a- ", "a- ", P, "not at the paragraph's start: text"),
|
||||
("-x ", "-x ", P, "the space must follow the marker"),
|
||||
];
|
||||
@@ -108,6 +112,32 @@ fn typed_list_starts() {
|
||||
assert_eq!((data(&s, t).characters.as_str(), lists(&s, t)), ("Intro\n- ", vec![(P, 0), (B, 0)]), "an item's own text stays text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_number_starts_the_list_there() {
|
||||
// As measured in Figma (2026-11-11): typing "5. x", Enter, "y" reads 5. then 6.; the start
|
||||
// is the typed number less one, and the next item goes on counting from it.
|
||||
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();
|
||||
}
|
||||
assert_eq!(data(&s, t).para_list(0).start_offset, 4, "5 starts at offset 4");
|
||||
key(&mut s, "Enter", false, false, false).unwrap();
|
||||
insert(&mut s, "y").unwrap();
|
||||
crate::textedit::end(&mut s).unwrap();
|
||||
let d = data(&s, t);
|
||||
assert_eq!(d.characters, "x\ny");
|
||||
assert_eq!(lists(&s, t), [(N, 0), (N, 0)]);
|
||||
// Enter copies the list onto the new item; `list_markers` ignores the start of an item that
|
||||
// goes on counting (a numbered item right after a numbered item at its level), so 5. then 6.
|
||||
assert_eq!((d.para_list(0).start_offset, d.para_list(1).start_offset), (4, 4), "the copy is ignored: the next item counts on");
|
||||
s.undo().unwrap();
|
||||
assert_eq!(data(&s, t).characters, "", "one undo step for the session");
|
||||
assert_eq!(data(&s, t).para_list(0).start_offset, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_keys_at_list_items() {
|
||||
// (chars, lists before, caret, key, shift, chars after, lists after, derivation)
|
||||
|
||||
@@ -93,8 +93,16 @@ fn list_numbering_by_level() {
|
||||
vec!["\u{2022}", "\u{2022}", "\u{2022}", "\u{2022}"],
|
||||
"bullets keep the same styling at all levels of indentation",
|
||||
),
|
||||
(vec![pl(N, 0), pl(N, 0), pl(P, 0), pl(N, 0)], vec!["1.", "2.", "", "1."], "silent: a plain paragraph ends a list, the next starts at 1"),
|
||||
(vec![pl(N, 0), pl(B, 0), pl(N, 0)], vec!["1.", "\u{2022}", "1."], "silent: the other kind of list at the same level starts the count again"),
|
||||
(
|
||||
vec![pl(N, 0), pl(N, 0), pl(P, 0), pl(N, 0)],
|
||||
vec!["1.", "2.", "", "1."],
|
||||
"as measured in Figma (2026-11-11): a plain paragraph ends a list, the next starts at 1",
|
||||
),
|
||||
(
|
||||
vec![pl(N, 0), pl(B, 0), pl(N, 0)],
|
||||
vec!["1.", "\u{2022}", "1."],
|
||||
"as measured in Figma (2026-11-11): a bullet item between numbered ones starts the count again",
|
||||
),
|
||||
(vec![pl(N, 0), pl(N, 1), pl(N, 0), pl(N, 1)], vec!["1.", "a.", "2.", "a."], "a shallower item starts the deeper count again"),
|
||||
(vec![pl(N, 2), pl(N, 2), pl(N, 2), pl(N, 2)], vec!["i.", "ii.", "iii.", "iv."], "roman numerals at the third level"),
|
||||
];
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user