Type: typing "1. ", "A) " or "* " at the start of a paragraph starts a list

The reference app detects lists typed as 1., 1), A. or A), and makes
custom bullets from *, #, > or + followed by a space. In VectorCraft
the characters stayed as text.

When the Type tool types a space on its own after one of these at the
start of a paragraph in no list, the typing so far becomes one undo
step. The new command text.startList then removes the typed characters
and the space and makes the paragraph a numbered list in 1, 2, 3 or
A, B, C, or a bulleted list with the filled dot. A paragraph with no
indents gets the 18 pt hanging indent. One Undo puts the characters
back as text with no list, and the caret goes on after them; Redo
removes them again. The same characters later in a paragraph, list
characters pasted in one piece with the space and a paragraph already
in a list start nothing.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-26 09:09:46 +07:00
committed by bflatastic
parent a8cc7b8f72
commit 5339fec9cd
6 changed files with 333 additions and 5 deletions
+39
View File
@@ -22,6 +22,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
edit_range
),
cmd!(
"text.startList",
"Bullets and Numbering",
[],
None,
"{id, at?: byte (default: the end of the text)} start a list from typed characters, what the Type tool runs when a space is typed on its own: when the text from the start of the paragraph that `at` is in up to `at` is \"1. \" or \"1) \" (a numbered list in 1, 2, 3), \"A. \" or \"A) \" (a numbered list in A, B, C), or \"* \", \"# \", \"> \" or \"+ \" (a bulleted list with the filled dot), and the paragraph is in no list, those characters are removed and the paragraph joins that list as one undo step (left and first-line indents of 0 become 18 and -18, as text.setFormat sets them); otherwise nothing changes → {id, started: bool, caret: byte (the paragraph's start when started, else `at`)}",
has_doc,
start_list
),
cmd!(
"text.setRangeStyle",
"Character",
@@ -220,6 +229,36 @@ fn edit_range(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"id": id.0, "caret": caret}))
}
fn start_list(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "text.startList";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
check_numbers(p, &["at"], C)?;
let t = text_ref(s, id)?;
let len = edit::runs_len(&t.runs);
let at = byte_param(p, "at").unwrap_or(len).min(len);
let Some((start, list)) = t.typed_list_at(at) else {
return Ok(json!({"id": id.0, "started": false, "caret": at}));
};
s.edit("Bullets and Numbering", |d, _| {
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
t.splice_paras(start, at, "");
edit::replace_range(&mut t.runs, start, at, "");
t.ruby_after_replace(start, at, 0);
let i = t.paragraphs_in(start, start).start;
t.edit_paras(Some(i..i + 1), |para| {
para.list = Some(list);
// A paragraph with no left or first-line indent gets the hanging indent.
if para.left_indent == 0.0 && para.first_line_indent == 0.0 {
para.left_indent = vectorcraft_doc::text::LIST_HANGING_INDENT;
para.first_line_indent = -vectorcraft_doc::text::LIST_HANGING_INDENT;
}
});
refresh_bounds(t);
Ok(())
})?;
Ok(json!({"id": id.0, "started": true, "caret": start}))
}
fn get_range(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("text.getRange", "missing `id`"))?;
let t = text_ref(s, id)?;