core: drop the two helpers the block editor made unnecessary
`toggle_at` existed to map a tap on `[ ]` inside a plain text field to an item, and `continuation` to make Return start the next one. The block editor needs neither: a checkbox is a real Checkbox, so it is tapped rather than located, and Return is the field's own IME action rather than a shape recognised in a string. Removed rather than kept for later (rule 22). Both were exported over the FFI with no Kotlin caller, which is API surface promising something nothing does — and their tests were weight on code nothing runs. The section comment above them described the tap-in-a-text-field problem, which is no longer the problem this pair solves. Rewritten to say what is actually there: one function to read a body apart, one to put a line back together, and between them Kotlin renders checkboxes without owning the grammar.
This commit is contained in:
+10
-23
@@ -501,15 +501,17 @@ impl ThoughtSync {
|
|||||||
|
|
||||||
// ── checklist text, as pure functions ───────────────────────────────────────
|
// ── checklist text, as pure functions ───────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Free functions rather than methods, because these touch no database. The editor's
|
// The pair the block editor is built on: one to read a body apart, one to put a line
|
||||||
// body field is LOCAL state — it is autosaved on an idle debounce, not written on
|
// back together. Between them, Kotlin can render a checklist as real checkboxes and
|
||||||
// every keystroke — so a checkbox tapped in the editor has to rewrite the text the
|
// write the markdown back without owning the grammar — which is the point. Three
|
||||||
// field is holding, not a row the store would hand back a moment later. Routing that
|
// implementations of it is the price already being paid (Rust, Python, TypeScript);
|
||||||
// through the store would overwrite whatever was being typed.
|
// 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
|
// Free functions rather than methods, because they touch no database. The editor's
|
||||||
// already being paid (Rust, Python, TypeScript); a fourth in Compose would be one
|
// body is LOCAL state — autosaved on an idle debounce, not written per keystroke —
|
||||||
// more place for a checklist to change shape when it syncs.
|
// 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
|
/// 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.
|
/// checkbox instead of the markup and has to write the markup back.
|
||||||
@@ -528,21 +530,6 @@ pub fn checklist_items(body: String) -> Vec<BodyItem> {
|
|||||||
.collect()
|
.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<String> {
|
|
||||||
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<String> {
|
|
||||||
local::derive::continuation(&line)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helpers, deliberately NOT exported — uniffi only binds what an `#[uniffi::export]`
|
/// Helpers, deliberately NOT exported — uniffi only binds what an `#[uniffi::export]`
|
||||||
/// block names, so these stay Rust-side.
|
/// block names, so these stay Rust-side.
|
||||||
impl ThoughtSync {
|
impl ThoughtSync {
|
||||||
|
|||||||
@@ -259,53 +259,6 @@ pub fn remove_item(body: &str, index: usize) -> String {
|
|||||||
map_task_line(body, index, |_| None)
|
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<String> {
|
|
||||||
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<String> {
|
|
||||||
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.
|
/// Add an item at the end of the body.
|
||||||
///
|
///
|
||||||
/// Spaced exactly as `import_export.py:_note_markdown` writes a list — a blank line
|
/// 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<_>>(), vec![1, 3]);
|
assert_eq!(found.iter().map(|i| i.line).collect::<Vec<_>>(), 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]
|
#[test]
|
||||||
fn a_stale_index_does_nothing() {
|
fn a_stale_index_does_nothing() {
|
||||||
// The index comes from a UI that may be a moment behind the store. A tap
|
// The index comes from a UI that may be a moment behind the store. A tap
|
||||||
|
|||||||
Reference in New Issue
Block a user