From dcb5d89c0ccad0dfa3fa4801a13c60a8a5ff99a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Wed, 3 Jun 2026 12:22:42 +0000 Subject: [PATCH] =?UTF-8?q?feat(config):=20P3=20config=20batch=20=E2=80=94?= =?UTF-8?q?=20group=203=20(cycle=5Fbullets)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cycle_bullets (default off): when an unordered list item changes indent level (gll/glh), rotate its glyph through the configured bullet_types as a depth-keyed ring buffer — change_level_edit gains cycle_bullets + bullet_types params, locates the marker via find_marker_span, and rewrites the glyph to bullet_types[new_depth % len] for single-char unordered markers. Ordered markers are untouched. list_change_level reads both from the wiki config. Tests: cycle_bullets_rotates_glyph_on_indent + cycle_bullets_off_leaves_glyph. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/nuwiki-lsp/src/commands.rs | 23 ++++++++++++++ crates/nuwiki-lsp/tests/commands_lists.rs | 38 +++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/crates/nuwiki-lsp/src/commands.rs b/crates/nuwiki-lsp/src/commands.rs index 3663335..3bd4bfe 100644 --- a/crates/nuwiki-lsp/src/commands.rs +++ b/crates/nuwiki-lsp/src/commands.rs @@ -1072,6 +1072,10 @@ fn list_change_level(backend: &Backend, args: Vec) -> Result) -> Result 0`) or dedent (`delta < 0`) the item at `line`. /// When `whole_subtree` is set, all descendants get the same shift /// so the visual tree stays consistent. + #[allow(clippy::too_many_arguments)] pub fn change_level_edit( text: &str, ast: &DocumentNode, @@ -2690,6 +2697,8 @@ pub mod ops { delta: i32, whole_subtree: bool, utf8: bool, + cycle_bullets: bool, + bullet_types: &[String], ) -> Option { let item = find_list_item_at(ast, line, 0)?; let mut edits: Vec<(Span, String)> = Vec::new(); @@ -2701,6 +2710,20 @@ pub mod ops { // (a partial shift would break the tree). push_level_edit_for_line(item.span.start.line, text, delta, &mut edits); } + // vimwiki `cycle_bullets`: as an unordered item changes indent level, + // rotate its glyph through `bullet_types` (ring buffer keyed by depth). + if cycle_bullets && !bullet_types.is_empty() { + if let Some((mspan, marker, col_off)) = find_marker_span(text, item.span) { + if marker.chars().count() == 1 && bullet_types.iter().any(|b| b == marker) { + let old_depth = (col_off / 2) as i32; + let new_depth = (old_depth + delta).max(0) as usize; + let glyph = &bullet_types[new_depth % bullet_types.len()]; + if glyph.as_str() != marker { + edits.push((mspan, glyph.clone())); + } + } + } + } if edits.is_empty() { return None; } diff --git a/crates/nuwiki-lsp/tests/commands_lists.rs b/crates/nuwiki-lsp/tests/commands_lists.rs index a30416a..3e3b4b9 100644 --- a/crates/nuwiki-lsp/tests/commands_lists.rs +++ b/crates/nuwiki-lsp/tests/commands_lists.rs @@ -254,7 +254,7 @@ fn remove_checkbox_none_without_checkbox() { fn change_level_indents_single_item() { let src = "- one\n- two\n"; let ast = parse(src); - let edit = ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true).expect("edit"); + let edit = ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true, false, &[]).expect("edit"); let edits = &edit.changes.unwrap()[&uri()]; assert_eq!(edits.len(), 1); // delta=1 → +2 spaces @@ -265,16 +265,48 @@ fn change_level_indents_single_item() { fn change_level_dedents_clamps_to_zero() { let src = "- root\n"; let ast = parse(src); - let edit = ops::change_level_edit(src, &ast, &uri(), 0, -1, false, true); + let edit = ops::change_level_edit(src, &ast, &uri(), 0, -1, false, true, false, &[]); // Already at column 0 — dedent is a no-op. assert!(edit.is_none()); } +#[test] +fn cycle_bullets_rotates_glyph_on_indent() { + // With cycle_bullets + bullet_types [-,*,#], indenting a `-` item (depth 0 + // → 1) rotates the glyph to `*` (and dedenting back to `-`). + let bullets: Vec = ["-", "*", "#"].iter().map(|s| s.to_string()).collect(); + let src = "- one\n- two\n"; + let ast = parse(src); + let edit = + ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true, true, &bullets).expect("edit"); + let texts: Vec = edit.changes.unwrap()[&uri()] + .iter() + .map(|e| e.new_text.clone()) + .collect(); + assert!(texts.contains(&" ".to_string()), "indent edit: {texts:?}"); + assert!(texts.contains(&"*".to_string()), "glyph rotated to *: {texts:?}"); +} + +#[test] +fn cycle_bullets_off_leaves_glyph() { + let bullets: Vec = ["-", "*", "#"].iter().map(|s| s.to_string()).collect(); + let src = "- one\n- two\n"; + let ast = parse(src); + let edit = ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true, false, &bullets) + .expect("edit"); + let texts: Vec = edit.changes.unwrap()[&uri()] + .iter() + .map(|e| e.new_text.clone()) + .collect(); + // Only the indent edit; no glyph rewrite. + assert!(!texts.iter().any(|t| t == "*"), "no glyph change: {texts:?}"); +} + #[test] fn change_level_whole_subtree_walks_descendants() { let src = "- parent\n - child a\n - child b\n- sibling\n"; let ast = parse(src); - let edit = ops::change_level_edit(src, &ast, &uri(), 0, 1, true, true).expect("edit"); + let edit = ops::change_level_edit(src, &ast, &uri(), 0, 1, true, true, false, &[]).expect("edit"); let edits = &edit.changes.unwrap()[&uri()]; // Parent + 2 children get indented. Sibling stays. assert!(edits.len() >= 3, "got {} edits", edits.len());