//! Heading level rewriter — `nuwiki.heading.addLevel` / //! `nuwiki.heading.removeLevel`. use nuwiki_core::syntax::vimwiki::VimwikiSyntax; use nuwiki_core::syntax::SyntaxPlugin; use nuwiki_lsp::commands::ops; use tower_lsp::lsp_types::Url; fn parse(src: &str) -> nuwiki_core::ast::DocumentNode { VimwikiSyntax::new().parse(src) } fn dummy_uri() -> Url { Url::parse("file:///tmp/note.wiki").unwrap() } fn one_text_edit(edit: tower_lsp::lsp_types::WorkspaceEdit) -> tower_lsp::lsp_types::TextEdit { let changes = edit.changes.expect("expected changes map"); let (_, edits) = changes.into_iter().next().expect("at least one uri"); assert_eq!(edits.len(), 1, "expected exactly one text edit"); edits.into_iter().next().unwrap() } #[test] fn add_heading_level_promotes_h2_to_h3() { let src = "== Sub ==\n"; let doc = parse(src); let edit = ops::heading_change_level(src, &doc, &dummy_uri(), 0, 0, true, 1).expect("add_level edit"); let te = one_text_edit(edit); assert_eq!(te.new_text, "=== Sub ==="); } #[test] fn add_heading_level_caps_at_6() { let src = "====== Deep ======\n"; let doc = parse(src); let edit = ops::heading_change_level(src, &doc, &dummy_uri(), 0, 0, true, 1); // No-op because level 6 is the cap; clamp matched current level. assert!(edit.is_none()); } #[test] fn remove_heading_level_demotes() { let src = "=== Mid ===\n"; let doc = parse(src); let edit = ops::heading_change_level(src, &doc, &dummy_uri(), 0, 0, true, -1) .expect("remove_level edit"); let te = one_text_edit(edit); assert_eq!(te.new_text, "== Mid =="); } #[test] fn remove_heading_level_at_h1_is_noop() { let src = "= Top =\n"; let doc = parse(src); let edit = ops::heading_change_level(src, &doc, &dummy_uri(), 0, 0, true, -1); assert!(edit.is_none()); } #[test] fn heading_change_returns_none_when_cursor_not_on_heading() { let src = "paragraph\n"; let doc = parse(src); let edit = ops::heading_change_level(src, &doc, &dummy_uri(), 0, 0, true, 1); assert!(edit.is_none()); } #[test] fn rewrite_heading_with_level_keeps_leading_whitespace_outside_span() { // The lexer's heading span starts at the first `=`, not the line's // start, so the leading whitespace that marks a "centered" heading // lives outside the edit range — preserved automatically. The // rewriter's output is just the `=...=` portion. let src = " = Title =\n"; let doc = parse(src); let h = match &doc.children[0] { nuwiki_core::ast::BlockNode::Heading(h) => h, _ => panic!("expected heading"), }; let new = ops::rewrite_heading_with_level(src, h.span, 2).unwrap(); assert_eq!(new, "== Title =="); }