feat(config): P3 config batch — group 3 (cycle_bullets)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1072,6 +1072,10 @@ fn list_change_level(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
|||||||
};
|
};
|
||||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||||
let (line, _) = nav::lsp_to_byte_pos(parsed.position, &doc.text, utf8);
|
let (line, _) = nav::lsp_to_byte_pos(parsed.position, &doc.text, utf8);
|
||||||
|
let (cycle_bullets, bullet_types) = backend
|
||||||
|
.wiki_for_uri(&parsed.uri)
|
||||||
|
.map(|w| (w.config.cycle_bullets, w.config.bullet_types.clone()))
|
||||||
|
.unwrap_or((false, Vec::new()));
|
||||||
Ok(ops::change_level_edit(
|
Ok(ops::change_level_edit(
|
||||||
&doc.text,
|
&doc.text,
|
||||||
&doc.ast,
|
&doc.ast,
|
||||||
@@ -1080,6 +1084,8 @@ fn list_change_level(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
|||||||
parsed.delta,
|
parsed.delta,
|
||||||
parsed.whole_subtree,
|
parsed.whole_subtree,
|
||||||
utf8,
|
utf8,
|
||||||
|
cycle_bullets,
|
||||||
|
&bullet_types,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2682,6 +2688,7 @@ pub mod ops {
|
|||||||
/// Indent (`delta > 0`) or dedent (`delta < 0`) the item at `line`.
|
/// Indent (`delta > 0`) or dedent (`delta < 0`) the item at `line`.
|
||||||
/// When `whole_subtree` is set, all descendants get the same shift
|
/// When `whole_subtree` is set, all descendants get the same shift
|
||||||
/// so the visual tree stays consistent.
|
/// so the visual tree stays consistent.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn change_level_edit(
|
pub fn change_level_edit(
|
||||||
text: &str,
|
text: &str,
|
||||||
ast: &DocumentNode,
|
ast: &DocumentNode,
|
||||||
@@ -2690,6 +2697,8 @@ pub mod ops {
|
|||||||
delta: i32,
|
delta: i32,
|
||||||
whole_subtree: bool,
|
whole_subtree: bool,
|
||||||
utf8: bool,
|
utf8: bool,
|
||||||
|
cycle_bullets: bool,
|
||||||
|
bullet_types: &[String],
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
let item = find_list_item_at(ast, line, 0)?;
|
let item = find_list_item_at(ast, line, 0)?;
|
||||||
let mut edits: Vec<(Span, String)> = Vec::new();
|
let mut edits: Vec<(Span, String)> = Vec::new();
|
||||||
@@ -2701,6 +2710,20 @@ pub mod ops {
|
|||||||
// (a partial shift would break the tree).
|
// (a partial shift would break the tree).
|
||||||
push_level_edit_for_line(item.span.start.line, text, delta, &mut edits);
|
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() {
|
if edits.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ fn remove_checkbox_none_without_checkbox() {
|
|||||||
fn change_level_indents_single_item() {
|
fn change_level_indents_single_item() {
|
||||||
let src = "- one\n- two\n";
|
let src = "- one\n- two\n";
|
||||||
let ast = parse(src);
|
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()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 1);
|
assert_eq!(edits.len(), 1);
|
||||||
// delta=1 → +2 spaces
|
// delta=1 → +2 spaces
|
||||||
@@ -265,16 +265,48 @@ fn change_level_indents_single_item() {
|
|||||||
fn change_level_dedents_clamps_to_zero() {
|
fn change_level_dedents_clamps_to_zero() {
|
||||||
let src = "- root\n";
|
let src = "- root\n";
|
||||||
let ast = parse(src);
|
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.
|
// Already at column 0 — dedent is a no-op.
|
||||||
assert!(edit.is_none());
|
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<String> = ["-", "*", "#"].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<String> = 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<String> = ["-", "*", "#"].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<String> = 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]
|
#[test]
|
||||||
fn change_level_whole_subtree_walks_descendants() {
|
fn change_level_whole_subtree_walks_descendants() {
|
||||||
let src = "- parent\n - child a\n - child b\n- sibling\n";
|
let src = "- parent\n - child a\n - child b\n- sibling\n";
|
||||||
let ast = parse(src);
|
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()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
// Parent + 2 children get indented. Sibling stays.
|
// Parent + 2 children get indented. Sibling stays.
|
||||||
assert!(edits.len() >= 3, "got {} edits", edits.len());
|
assert!(edits.len() >= 3, "got {} edits", edits.len());
|
||||||
|
|||||||
Reference in New Issue
Block a user