//! Cluster B — table rewriters: `insert`, `align`, `moveColumn`. 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 uri() -> Url { Url::parse("file:///tmp/tbl.wiki").unwrap() } fn one_text(edit: tower_lsp::lsp_types::WorkspaceEdit) -> String { let changes = edit.changes.expect("changes"); let (_, edits) = changes.into_iter().next().unwrap(); edits.into_iter().next().unwrap().new_text } // ===== render_blank_table ===== #[test] fn blank_table_has_header_separator_and_rows() { let out = ops::render_blank_table(3, 2); let lines: Vec<&str> = out.lines().collect(); // Header + separator + 2 data rows = 4. assert_eq!(lines.len(), 4); assert_eq!(lines[0], "| | | |"); assert_eq!(lines[1], "|--|--|--|"); assert_eq!(lines[2], "| | | |"); assert_eq!(lines[3], "| | | |"); } #[test] fn blank_table_handles_single_cell_grid() { let out = ops::render_blank_table(1, 1); assert_eq!(out, "| |\n|--|\n| |\n"); } // ===== align ===== #[test] fn align_pads_short_cells_to_widest() { let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n"; let ast = parse(src); let edit = ops::table_align_edit(src, &ast, &uri(), 0, true, false).expect("edit"); let body = one_text(edit); let lines: Vec<&str> = body.lines().collect(); eprintln!("rendered table:"); for (i, l) in lines.iter().enumerate() { eprintln!(" {i}: {l:?}"); } // Column widths: max(a,1)=1 → 1, max(bbb,2)=3, max(c,three)=5. assert_eq!(lines[0], "| a | bbb | c |"); // Separator row is repadded with `-` runs sized `width + 2` per // column (the two padding spaces of content rows). assert!(lines[1].starts_with("|---|"), "got: {:?}", lines[1]); assert_eq!(lines[2], "| 1 | 2 | three |"); } #[test] fn align_returns_none_when_cursor_off_table() { let src = "paragraph\n"; let ast = parse(src); assert!(ops::table_align_edit(src, &ast, &uri(), 0, true, false).is_none()); } #[test] fn table_reduce_last_col_keeps_last_column_minimal() { // With reduce_last_col=true, the last column isn't padded to its content // width — it stays at the minimum (1), so `three` is not surrounded by // alignment padding on the right edge. let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n"; let ast = parse(src); let edit = ops::table_align_edit(src, &ast, &uri(), 0, true, true).expect("edit"); let body = one_text(edit); let lines: Vec<&str> = body.lines().collect(); // First two columns still align; the last column stays at width 1 (not 5), // so the header's last cell renders `| c |` not `| c |`. assert_eq!(lines[0], "| a | bbb | c |", "got: {:?}", lines[0]); assert_eq!(lines[2], "| 1 | 2 | three |", "got: {:?}", lines[2]); } // ===== moveColumn ===== #[test] fn move_column_right_swaps_with_neighbour() { let src = "|a|b|c|\n|--|--|--|\n|1|2|3|\n"; let ast = parse(src); // Cursor on first column → swap right. let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, 1, true, false).expect("edit"); let body = one_text(edit); let lines: Vec<&str> = body.lines().collect(); assert!(lines[0].contains("b") && lines[0].contains("a")); // b should come before a after swapping col 0 → 1. let line0 = lines[0]; let b_pos = line0.find('b').unwrap(); let a_pos = line0.find('a').unwrap(); assert!(b_pos < a_pos, "expected b before a; got {line0:?}"); } #[test] fn move_column_left_at_zero_is_noop() { let src = "|a|b|\n|--|--|\n|1|2|\n"; let ast = parse(src); // Cursor in column 0; left would be -1 → returns None. let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, -1, true, false); assert!(edit.is_none()); } #[test] fn move_column_returns_none_off_table() { let src = "paragraph\n"; let ast = parse(src); assert!(ops::table_move_column_edit(src, &ast, &uri(), 0, 0, 1, true, false).is_none()); } // ===== COMMANDS list ===== #[test] fn commands_list_includes_cluster_b() { let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec(); for name in [ "nuwiki.table.insert", "nuwiki.table.align", "nuwiki.table.moveColumn", ] { assert!(names.contains(&name), "missing: {name}"); } }