anvilsign in

collin/anvil · 23166530

fix: board move no longer injects blank lines into checkbox lists

Collin Richards · 2026-06-10 18:26 UTC · 23166530fdbab72b38e236958e5944af94568f72 · parent 349f304c · browse files

modifiedcrates/anvil-web/src/todomd.rs+52 −9
⋯ 538 unchanged lines
539539 }
540540
541541 // Re-locate the target section in the post-removal document and splice the
542- // block in, keeping one blank line on each side.
542+ // block back in. Tickets (a `## heading` block) are blank-line separated;
543+ // checkbox tasks are a tight list — so only pad tickets, or a move would
544+ // keep injecting stray blank lines into a checkbox column.
545+ let is_ticket = block
546+ .first()
547+ .is_some_and(|l| heading(l.trim_end_matches('\r')).is_some());
543548 let refs: Vec<&str> = out.iter().map(|s| s.as_str()).collect();
544549 let (ts, te) = find_section_body(&refs, col, to_section)?;
545- match nth_task_start(&refs, col, ts, te, to_index) {
550+ let mut ins: Vec<String> = Vec::new();
551+ let at = match nth_task_start(&refs, col, ts, te, to_index) {
552+ // Before the to_index-th task.
546553 Some(at) => {
547- let mut ins = block;
548- ins.push(String::new()); // blank between the moved block and the task below it
549- out.splice(at..at, ins);
554+ if is_ticket {
555+ if at > ts && !refs[at - 1].trim().is_empty() {
556+ ins.push(String::new()); // ensure a blank before the heading
557+ }
558+ ins.extend(block);
559+ ins.push(String::new()); // and a blank before the following task
560+ } else {
561+ ins.extend(block); // tight: slot the list item straight in
562+ }
563+ at
550564 }
565+ // Past the last task: append after the section's last non-blank line.
551566 None => {
552- // Append after the section's last non-blank line.
553567 let mut at = te;
554568 while at > ts && refs[at - 1].trim().is_empty() {
555569 at -= 1;
556570 }
557- let mut ins = vec![String::new()];
571+ if is_ticket {
572+ ins.push(String::new());
573+ }
558574 ins.extend(block);
559- out.splice(at..at, ins);
575+ at
560576 }
561- }
577+ };
578+ out.splice(at..at, ins);
562579 Some(out.join("\n"))
563580 }
564581
⋯ 437 unchanged lines
10021019 assert!(move_task(doc, "ghost", "Now", "Done", 0).is_none());
10031020 assert!(move_task(doc, "a", "Now", "Nope", 0).is_none());
10041021 }
1022+
1023+ #[test]
1024+ fn move_checkbox_task_keeps_the_list_tight() {
1025+ // A checkbox column is a tight list — moving must not inject blanks.
1026+ let doc = "# Backlog\n\n- [ ] a\n- [ ] b\n- [ ] c\n";
1027+ assert_eq!(
1028+ move_task(doc, "c", "Backlog", "Backlog", 0).unwrap(),
1029+ "# Backlog\n\n- [ ] c\n- [ ] a\n- [ ] b\n"
1030+ );
1031+ }
1032+
1033+ #[test]
1034+ fn move_task_to_same_spot_is_a_noop() {
1035+ // Dropping a card back where it was must not change a byte (no stray
1036+ // newlines) — for both checkbox lists and ticket columns.
1037+ let checks = "# Backlog\n\n- [ ] a\n- [ ] b\n- [ ] c\n";
1038+ assert_eq!(
1039+ move_task(checks, "b", "Backlog", "Backlog", 1).as_deref(),
1040+ Some(checks)
1041+ );
1042+ let tickets = "# Now\n\n## a\n\n## b\n\n## c\n";
1043+ assert_eq!(
1044+ move_task(tickets, "b", "Now", "Now", 1).as_deref(),
1045+ Some(tickets)
1046+ );
1047+ }
10051048 }