diff --git a/android/ffi/src/lib.rs b/android/ffi/src/lib.rs index a03ed23..e7349a2 100644 --- a/android/ffi/src/lib.rs +++ b/android/ffi/src/lib.rs @@ -501,15 +501,17 @@ impl ThoughtSync { // ── checklist text, as pure functions ─────────────────────────────────────── // -// Free functions rather than methods, because these touch no database. The editor's -// body field is LOCAL state — it is autosaved on an idle debounce, not written on -// every keystroke — so a checkbox tapped in the editor has to rewrite the text the -// field is holding, not a row the store would hand back a moment later. Routing that -// through the store would overwrite whatever was being typed. +// The pair the block editor is built on: one to read a body apart, one to put a line +// back together. Between them, Kotlin can render a checklist as real checkboxes and +// write the markdown back without owning the grammar — which is the point. Three +// implementations of it is the price already being paid (Rust, Python, TypeScript); +// a fourth in Compose would be one more place for a checklist to change shape when +// it syncs. // -// They also keep the grammar out of Kotlin. Three implementations of it is the price -// already being paid (Rust, Python, TypeScript); a fourth in Compose would be one -// more place for a checklist to change shape when it syncs. +// Free functions rather than methods, because they touch no database. The editor's +// body is LOCAL state — autosaved on an idle debounce, not written per keystroke — +// so editing a checklist there has to rewrite the text the editor is holding, not a +// row the store would hand back a moment later and overwrite the typing with. /// One checklist item as the body line that stores it. For an editor that shows a /// checkbox instead of the markup and has to write the markup back. @@ -528,21 +530,6 @@ pub fn checklist_items(body: String) -> Vec { .collect() } -/// The body with the item at `line`/`column` ticked or unticked, or null if that is -/// not a checkbox. See `derive::toggle_at` for why the address is line + column and -/// not a text offset. -#[uniffi::export] -pub fn checklist_toggle_at(body: String, line: u32, column: u32) -> Option { - local::derive::toggle_at(&body, line as usize, column as usize) -} - -/// What pressing Enter at the end of `line` should leave behind: null to let Enter be -/// Enter, "" to end the list, or the marker to start the next item. -#[uniffi::export] -pub fn checklist_continuation(line: String) -> Option { - local::derive::continuation(&line) -} - /// Helpers, deliberately NOT exported — uniffi only binds what an `#[uniffi::export]` /// block names, so these stay Rust-side. impl ThoughtSync { diff --git a/core/src/local/derive.rs b/core/src/local/derive.rs index 85cd9e6..b27c8d8 100644 --- a/core/src/local/derive.rs +++ b/core/src/local/derive.rs @@ -259,53 +259,6 @@ pub fn remove_item(body: &str, index: usize) -> String { map_task_line(body, index, |_| None) } -/// The body with the item on `line` toggled — or None when that line is not a task -/// line, or when `column` falls outside its `[ ]` marker. -/// -/// Addressed by LINE and COLUMN rather than by a text offset, because the two sides of -/// the FFI do not count the same way: Compose measures an offset in UTF-16 units and -/// Rust in bytes, so the same number means different places in a note with an emoji in -/// it. A line number is identical in every encoding. So is a column inside the marker, -/// which is ASCII and sits at the start of its line — and that is the only range this -/// function looks at. -/// -/// Only the marker toggles, not the whole line: the rest of it is text somebody needs -/// to be able to put a caret into. -pub fn toggle_at(body: &str, line: usize, column: usize) -> Option { - let target = body.split('\n').nth(line)?; - let parsed = parse_task_line(target)?; - // One column of slack past the `]`, because a checkbox on a phone should forgive - // a near miss. - let marker_end = target.chars().position(|c| c == ']')? + 1; - if column > marker_end { - return None; - } - - let mut index = 0; - for earlier in body.split('\n').take(line) { - if parse_task_line(earlier).is_some() { - index += 1; - } - } - Some(set_item_checked(body, index, !parsed.checked)) -} - -/// What pressing Enter at the end of `line` should leave behind. -/// -/// * `None` — not a task line. Enter does what Enter always does. -/// * `Some("")` — an EMPTY item: clear the marker and end the list. Without this half -/// a list is impossible to get out of without deleting characters by hand. -/// * `Some(marker)` — start the next item. The indent and bullet are carried over -/// rather than normalised, because continuing someone's `*` list with a `-` is an -/// edit they did not ask for. -pub fn continuation(line: &str) -> Option { - let parsed = parse_task_line(line)?; - if parsed.text.trim().is_empty() { - return Some(String::new()); - } - Some(format!("{}{} [ ] ", parsed.indent, parsed.bullet)) -} - /// Add an item at the end of the body. /// /// Spaced exactly as `import_export.py:_note_markdown` writes a list — a blank line @@ -495,35 +448,6 @@ mod tests { assert_eq!(found.iter().map(|i| i.line).collect::>(), vec![1, 3]); } - #[test] - fn toggle_at_only_fires_inside_the_marker() { - let body = "note\n- [ ] milk\n- [x] eggs"; - // Column 0 is the bullet, 4 is the `]`, 5 the slack past it. - assert_eq!( - toggle_at(body, 1, 0).as_deref(), - Some("note\n- [x] milk\n- [x] eggs") - ); - assert_eq!( - toggle_at(body, 2, 5).as_deref(), - Some("note\n- [ ] milk\n- [ ] eggs") - ); - // Past the marker is text someone wants to put a caret in. - assert!(toggle_at(body, 1, 9).is_none()); - // Not a task line, and off the end. - assert!(toggle_at(body, 0, 0).is_none()); - assert!(toggle_at(body, 99, 0).is_none()); - } - - #[test] - fn continuation_starts_the_next_item_or_ends_the_list() { - assert_eq!(continuation("- [x] milk").as_deref(), Some("- [ ] ")); - assert_eq!(continuation(" * [ ] milk").as_deref(), Some(" * [ ] ")); - // An empty item ends the list rather than adding another. - assert_eq!(continuation("- [ ]").as_deref(), Some("")); - assert_eq!(continuation("- [ ] ").as_deref(), Some("")); - assert!(continuation("just prose").is_none()); - } - #[test] fn a_stale_index_does_nothing() { // The index comes from a UI that may be a moment behind the store. A tap