feat(13.1-B): table rewriters — insert, align, moveColumn
Closes the table-rewriter cluster from SPEC §13.1. Server-side (ops module): - `render_blank_table(cols, rows)` — pure templating: header row, separator `|--|--|--|`, then `rows` empty data rows. Wired through `table_insert` which dispatches a `TextEdit` at the cursor. - `table_align_edit` — locates the `TableNode` containing the cursor line, computes max width per column across every row, re-emits the table with each cell space-padded to its column's width. The parser drops `|---|---|` separator rows but sets `TableNode.has_header`; the renderer re-inserts a padded separator after the header so the result still parses. - `table_move_column_edit` — column-under-cursor swap with the left or right neighbour (`dir = "left" | "right"`). Cursor column is computed by counting pipe separators before the cursor's byte offset on the row's line. Column widths swap alongside the data so the post-swap table stays aligned. No-ops cleanly when the swap would fall off either edge. Editor glue: - `lua/nuwiki/commands.lua` / `autoload/nuwiki/commands.vim` — the three "not yet implemented" stubs are replaced with real LSP dispatchers. `table_insert` accepts optional `cols`/`rows` arguments (defaults: 3×2). - Default keymaps `gqq` / `gq1` / `gww` / `gw1` now call `table_align`; `<A-Left>` / `<A-Right>` call `table_move_column_left` / `_right`. No more "deferred" notifications on the table keys. Tests: 8 new in `cluster_b_table_rewriters.rs` — blank-table shape (3×2 + 1×1), column-width alignment with separator-row repad, swap- right shape, swap-left clamp at column 0, no-table-found fallbacks, COMMANDS list completeness. Total 410 Rust tests pass; clippy clean; Neovim keymap harness still 20/20. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -513,14 +513,51 @@ function! nuwiki#commands#list_change_lvl(...) abort
|
||||
let l:delta = (l:dir ==# 'increase' || l:dir ==# 'indent') ? 1 : -1
|
||||
call nuwiki#commands#list_change_level(l:delta, 0)
|
||||
endfunction
|
||||
function! nuwiki#commands#table_insert() abort
|
||||
call s:notify_deferred(':VimwikiTable')
|
||||
" §13.1 Cluster B — table rewriters.
|
||||
|
||||
function! nuwiki#commands#table_insert(...) abort
|
||||
let l:cols = a:0 >= 1 ? str2nr(a:1) : 3
|
||||
let l:rows = a:0 >= 2 ? str2nr(a:2) : 2
|
||||
if l:cols <= 0
|
||||
let l:cols = 3
|
||||
endif
|
||||
if l:rows <= 0
|
||||
let l:rows = 2
|
||||
endif
|
||||
let l:p = s:cursor_position()
|
||||
let l:args = [{
|
||||
\ 'uri': l:p['textDocument']['uri'],
|
||||
\ 'position': l:p['position'],
|
||||
\ 'cols': l:cols,
|
||||
\ 'rows': l:rows,
|
||||
\ }]
|
||||
call s:exec('nuwiki.table.insert', l:args)
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#table_align() abort
|
||||
let l:p = s:cursor_position()
|
||||
let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }]
|
||||
call s:exec('nuwiki.table.align', l:args)
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#table_move_left() abort
|
||||
call s:notify_deferred(':VimwikiTableMoveColumnLeft')
|
||||
let l:p = s:cursor_position()
|
||||
let l:args = [{
|
||||
\ 'uri': l:p['textDocument']['uri'],
|
||||
\ 'position': l:p['position'],
|
||||
\ 'dir': 'left',
|
||||
\ }]
|
||||
call s:exec('nuwiki.table.moveColumn', l:args)
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#table_move_right() abort
|
||||
call s:notify_deferred(':VimwikiTableMoveColumnRight')
|
||||
let l:p = s:cursor_position()
|
||||
let l:args = [{
|
||||
\ 'uri': l:p['textDocument']['uri'],
|
||||
\ 'position': l:p['position'],
|
||||
\ 'dir': 'right',
|
||||
\ }]
|
||||
call s:exec('nuwiki.table.moveColumn', l:args)
|
||||
endfunction
|
||||
function! nuwiki#commands#colorize(color) abort
|
||||
call s:notify_deferred(':VimwikiColorize')
|
||||
|
||||
@@ -71,6 +71,9 @@ pub const COMMANDS: &[&str] = &[
|
||||
"nuwiki.list.renumber",
|
||||
"nuwiki.list.changeSymbol",
|
||||
"nuwiki.list.changeLevel",
|
||||
"nuwiki.table.insert",
|
||||
"nuwiki.table.align",
|
||||
"nuwiki.table.moveColumn",
|
||||
];
|
||||
|
||||
pub(crate) async fn execute(
|
||||
@@ -177,6 +180,11 @@ pub(crate) async fn execute(
|
||||
"nuwiki.list.changeLevel" => {
|
||||
list_change_level(backend, args).map(|o| o.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.table.insert" => table_insert(backend, args).map(|o| o.map(CommandOutcome::Edit)),
|
||||
"nuwiki.table.align" => table_align(backend, args).map(|o| o.map(CommandOutcome::Edit)),
|
||||
"nuwiki.table.moveColumn" => {
|
||||
table_move_column(backend, args).map(|o| o.map(CommandOutcome::Edit))
|
||||
}
|
||||
other => Err(format!("unknown nuwiki command: {other}")),
|
||||
}
|
||||
}
|
||||
@@ -929,6 +937,82 @@ fn list_change_level(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
||||
))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster B: table rewriters =====
|
||||
|
||||
fn table_insert(_backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
#[derive(Deserialize)]
|
||||
struct Args {
|
||||
uri: Url,
|
||||
position: LspPosition,
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
}
|
||||
let raw = args
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| "missing { uri, position, cols, rows }".to_string())?;
|
||||
let parsed: Args = serde_json::from_value(raw).map_err(|e| format!("invalid args: {e}"))?;
|
||||
if parsed.cols == 0 || parsed.rows == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = ops::render_blank_table(parsed.cols, parsed.rows);
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
b.edit(
|
||||
parsed.uri,
|
||||
crate::edits::text_edit_insert(parsed.position, text),
|
||||
);
|
||||
Ok(Some(b.build()))
|
||||
}
|
||||
|
||||
fn table_align(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
let p = parse_pos(args)?;
|
||||
let doc = match backend.documents.get(&p.uri) {
|
||||
Some(d) => d,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||
let (line, _) = nav::lsp_to_byte_pos(p.position, &doc.text, utf8);
|
||||
Ok(ops::table_align_edit(
|
||||
&doc.text, &doc.ast, &p.uri, line, utf8,
|
||||
))
|
||||
}
|
||||
|
||||
fn table_move_column(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
#[derive(Deserialize)]
|
||||
struct Args {
|
||||
uri: Url,
|
||||
position: LspPosition,
|
||||
#[serde(default)]
|
||||
dir: Option<String>,
|
||||
}
|
||||
let raw = args
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| "missing { uri, position, dir }".to_string())?;
|
||||
let parsed: Args = serde_json::from_value(raw).map_err(|e| format!("invalid args: {e}"))?;
|
||||
let dir = parsed.dir.as_deref().unwrap_or("right");
|
||||
let delta = match dir {
|
||||
"left" => -1i32,
|
||||
"right" => 1,
|
||||
other => return Err(format!("unknown direction: {other}")),
|
||||
};
|
||||
let doc = match backend.documents.get(&parsed.uri) {
|
||||
Some(d) => d,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||
let (line, col) = nav::lsp_to_byte_pos(parsed.position, &doc.text, utf8);
|
||||
Ok(ops::table_move_column_edit(
|
||||
&doc.text,
|
||||
&doc.ast,
|
||||
&parsed.uri,
|
||||
line,
|
||||
col,
|
||||
delta,
|
||||
utf8,
|
||||
))
|
||||
}
|
||||
|
||||
// ===== Phase 17: HTML export commands =====
|
||||
|
||||
fn export_current(
|
||||
@@ -2149,6 +2233,264 @@ pub mod ops {
|
||||
Some((span, marker_str, i))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster B: table rewriters =====
|
||||
|
||||
use nuwiki_core::ast::{TableNode, TableRowNode};
|
||||
|
||||
/// Plain-text body of a freshly-inserted vimwiki table with `cols`
|
||||
/// columns and `rows` data rows (header + separator + rows).
|
||||
pub fn render_blank_table(cols: usize, rows: usize) -> String {
|
||||
let cell = " ";
|
||||
let mut out = String::new();
|
||||
// Header row.
|
||||
out.push('|');
|
||||
for _ in 0..cols {
|
||||
out.push_str(cell);
|
||||
out.push('|');
|
||||
}
|
||||
out.push('\n');
|
||||
// Separator row.
|
||||
out.push('|');
|
||||
for _ in 0..cols {
|
||||
out.push_str("--");
|
||||
out.push('|');
|
||||
}
|
||||
out.push('\n');
|
||||
// Data rows.
|
||||
for _ in 0..rows {
|
||||
out.push('|');
|
||||
for _ in 0..cols {
|
||||
out.push_str(cell);
|
||||
out.push('|');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Reformat the table containing `line` so every column shares the
|
||||
/// same width as its widest cell. Header-separator rows get
|
||||
/// repadded with `-` runs so the table still parses cleanly.
|
||||
pub fn table_align_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
uri: &Url,
|
||||
line: u32,
|
||||
utf8: bool,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let table = find_table_at_line(ast, line)?;
|
||||
let rendered = render_aligned_table(table, text)?;
|
||||
let span = table.span;
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
b.edit(uri.clone(), text_edit_replace(span, rendered, text, utf8));
|
||||
Some(b.build())
|
||||
}
|
||||
|
||||
/// Swap the column under cursor with its left (`delta = -1`) or
|
||||
/// right (`delta = +1`) neighbour across every row of the table.
|
||||
pub fn table_move_column_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
uri: &Url,
|
||||
line: u32,
|
||||
col: u32,
|
||||
delta: i32,
|
||||
utf8: bool,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let table = find_table_at_line(ast, line)?;
|
||||
let cur_col = cell_column_at(table, text, line, col)?;
|
||||
let other = cur_col as i32 + delta;
|
||||
if other < 0 {
|
||||
return None;
|
||||
}
|
||||
let other = other as usize;
|
||||
// All rows must have both columns to make the swap meaningful.
|
||||
let max_cols = table.rows.iter().map(|r| r.cells.len()).max().unwrap_or(0);
|
||||
if other >= max_cols {
|
||||
return None;
|
||||
}
|
||||
let rendered = render_swapped_table(table, text, cur_col, other)?;
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
b.edit(
|
||||
uri.clone(),
|
||||
text_edit_replace(table.span, rendered, text, utf8),
|
||||
);
|
||||
Some(b.build())
|
||||
}
|
||||
|
||||
fn find_table_at_line(ast: &DocumentNode, line: u32) -> Option<&TableNode> {
|
||||
for block in &ast.children {
|
||||
if let BlockNode::Table(t) = block {
|
||||
if (t.span.start.line..=t.span.end.line).contains(&line) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
// Tables inside blockquotes are uncommon but possible.
|
||||
if let BlockNode::Blockquote(BlockquoteNode { children, .. }) = block {
|
||||
for c in children {
|
||||
if let BlockNode::Table(t) = c {
|
||||
if (t.span.start.line..=t.span.end.line).contains(&line) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Locate the column index under (`line`, `col`) by walking the
|
||||
/// row that contains `line` and counting pipe separators before
|
||||
/// the cursor.
|
||||
fn cell_column_at(table: &TableNode, text: &str, line: u32, col: u32) -> Option<usize> {
|
||||
let row = table
|
||||
.rows
|
||||
.iter()
|
||||
.find(|r| (r.span.start.line..=r.span.end.line).contains(&line))?;
|
||||
let row_src = source_slice(text, row.span);
|
||||
let bytes = row_src.as_bytes();
|
||||
// `col` is the cursor's byte column on `line`. Re-base it
|
||||
// against the row's start column on that line.
|
||||
let row_start_col = row.span.start.column;
|
||||
if col < row_start_col {
|
||||
return None;
|
||||
}
|
||||
let mut rel = (col - row_start_col) as usize;
|
||||
rel = rel.min(bytes.len());
|
||||
let mut pipes_before = 0usize;
|
||||
for (i, b) in bytes.iter().enumerate() {
|
||||
if i >= rel {
|
||||
break;
|
||||
}
|
||||
if *b == b'|' {
|
||||
pipes_before += 1;
|
||||
}
|
||||
}
|
||||
// First pipe opens the row, so column index is `pipes - 1`.
|
||||
if pipes_before == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
Some(pipes_before - 1)
|
||||
}
|
||||
|
||||
fn source_slice(text: &str, span: Span) -> &str {
|
||||
let start = span.start.offset.min(text.len());
|
||||
let end = span.end.offset.min(text.len());
|
||||
&text[start..end]
|
||||
}
|
||||
|
||||
/// Compute column widths (max content length across rows).
|
||||
fn column_widths(table: &TableNode, text: &str) -> Vec<usize> {
|
||||
let max_cols = table.rows.iter().map(|r| r.cells.len()).max().unwrap_or(0);
|
||||
let mut widths = vec![1usize; max_cols];
|
||||
for row in &table.rows {
|
||||
for (i, cell) in row.cells.iter().enumerate() {
|
||||
let content = source_slice(text, cell.span).trim();
|
||||
if !is_separator_cell(content) {
|
||||
widths[i] = widths[i].max(content.chars().count());
|
||||
}
|
||||
}
|
||||
}
|
||||
widths
|
||||
}
|
||||
|
||||
fn is_separator_cell(content: &str) -> bool {
|
||||
!content.is_empty()
|
||||
&& content
|
||||
.chars()
|
||||
.all(|c| c == '-' || c == ':' || c.is_whitespace())
|
||||
}
|
||||
|
||||
fn render_aligned_table(table: &TableNode, text: &str) -> Option<String> {
|
||||
let widths = column_widths(table, text);
|
||||
let mut out = String::new();
|
||||
for (i, row) in table.rows.iter().enumerate() {
|
||||
render_row(row, text, &widths, &mut out);
|
||||
// The lexer dropped the `|---|---|` separator row, so
|
||||
// re-emit one after the header when `has_header` is set.
|
||||
if i == 0 && table.has_header {
|
||||
emit_separator_row(&widths, &mut out);
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn emit_separator_row(widths: &[usize], out: &mut String) {
|
||||
out.push('|');
|
||||
for w in widths {
|
||||
out.push_str(&"-".repeat(w + 2));
|
||||
out.push('|');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
fn render_row(row: &TableRowNode, text: &str, widths: &[usize], out: &mut String) {
|
||||
let mut all_separator = !row.cells.is_empty();
|
||||
for cell in &row.cells {
|
||||
let content = source_slice(text, cell.span).trim();
|
||||
if !is_separator_cell(content) {
|
||||
all_separator = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.push('|');
|
||||
for (i, _cell) in row.cells.iter().enumerate() {
|
||||
let width = *widths.get(i).unwrap_or(&1);
|
||||
if all_separator {
|
||||
out.push_str(&"-".repeat(width + 2));
|
||||
} else {
|
||||
let content = source_slice(text, row.cells[i].span).trim();
|
||||
let pad = width.saturating_sub(content.chars().count());
|
||||
out.push(' ');
|
||||
out.push_str(content);
|
||||
out.push_str(&" ".repeat(pad));
|
||||
out.push(' ');
|
||||
}
|
||||
out.push('|');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
fn render_swapped_table(table: &TableNode, text: &str, a: usize, b: usize) -> Option<String> {
|
||||
// Swap the column-width slots first so the rendered table
|
||||
// remains visually aligned after the move.
|
||||
let mut widths = column_widths(table, text);
|
||||
if a < widths.len() && b < widths.len() {
|
||||
widths.swap(a, b);
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (row_idx, row) in table.rows.iter().enumerate() {
|
||||
out.push('|');
|
||||
for i in 0..row.cells.len() {
|
||||
let src_col = if i == a {
|
||||
b
|
||||
} else if i == b {
|
||||
a
|
||||
} else {
|
||||
i
|
||||
};
|
||||
let width = *widths.get(i).unwrap_or(&1);
|
||||
let content = if src_col < row.cells.len() {
|
||||
source_slice(text, row.cells[src_col].span).trim()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let pad = width.saturating_sub(content.chars().count());
|
||||
out.push(' ');
|
||||
out.push_str(content);
|
||||
out.push_str(&" ".repeat(pad));
|
||||
out.push(' ');
|
||||
out.push('|');
|
||||
}
|
||||
out.push('\n');
|
||||
// Re-emit the dropped separator row after the header.
|
||||
if row_idx == 0 && table.has_header {
|
||||
emit_separator_row(&widths, &mut out);
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
// ===== Phase 16: diary helpers =====
|
||||
|
||||
use crate::edits::op_create;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
//! §13.1 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).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).is_none());
|
||||
}
|
||||
|
||||
// ===== 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).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);
|
||||
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).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}");
|
||||
}
|
||||
}
|
||||
@@ -181,13 +181,13 @@ if !has('nvim')
|
||||
nnoremap <silent><buffer> <Leader>whh :call nuwiki#commands#export_browse()<CR>
|
||||
nnoremap <silent><buffer> <Leader>wha :call nuwiki#commands#export_all()<CR>
|
||||
|
||||
" Tables (deferred §13.1)
|
||||
nnoremap <silent><buffer> gqq :echohl WarningMsg<bar>echom 'nuwiki: :VimwikiTableAlignQ not yet implemented — see SPEC §13.1'<bar>echohl None<CR>
|
||||
nnoremap <silent><buffer> gq1 :echohl WarningMsg<bar>echom 'nuwiki: :VimwikiTableAlignQ1 not yet implemented — see SPEC §13.1'<bar>echohl None<CR>
|
||||
nnoremap <silent><buffer> gww :echohl WarningMsg<bar>echom 'nuwiki: :VimwikiTableAlignW not yet implemented — see SPEC §13.1'<bar>echohl None<CR>
|
||||
nnoremap <silent><buffer> gw1 :echohl WarningMsg<bar>echom 'nuwiki: :VimwikiTableAlignW1 not yet implemented — see SPEC §13.1'<bar>echohl None<CR>
|
||||
nnoremap <silent><buffer> <A-Left> :echohl WarningMsg<bar>echom 'nuwiki: :VimwikiTableMoveColumnLeft not yet implemented'<bar>echohl None<CR>
|
||||
nnoremap <silent><buffer> <A-Right> :echohl WarningMsg<bar>echom 'nuwiki: :VimwikiTableMoveColumnRight not yet implemented'<bar>echohl None<CR>
|
||||
" Tables
|
||||
nnoremap <silent><buffer> gqq :call nuwiki#commands#table_align()<CR>
|
||||
nnoremap <silent><buffer> gq1 :call nuwiki#commands#table_align()<CR>
|
||||
nnoremap <silent><buffer> gww :call nuwiki#commands#table_align()<CR>
|
||||
nnoremap <silent><buffer> gw1 :call nuwiki#commands#table_align()<CR>
|
||||
nnoremap <silent><buffer> <A-Left> :call nuwiki#commands#table_move_left()<CR>
|
||||
nnoremap <silent><buffer> <A-Right> :call nuwiki#commands#table_move_right()<CR>
|
||||
endif
|
||||
|
||||
finish
|
||||
|
||||
+27
-3
@@ -474,9 +474,33 @@ function M.list_change_lvl(direction)
|
||||
M.list_change_level(delta, false)
|
||||
end
|
||||
|
||||
M.table_insert = _not_yet(':VimwikiTable')
|
||||
M.table_move_column_left = _not_yet(':VimwikiTableMoveColumnLeft')
|
||||
M.table_move_column_right = _not_yet(':VimwikiTableMoveColumnRight')
|
||||
-- §13.1 Cluster B — table rewriters.
|
||||
|
||||
function M.table_insert(cols, rows)
|
||||
cols = tonumber(cols) or 3
|
||||
rows = tonumber(rows) or 2
|
||||
local p = pos_args()[1]
|
||||
p.cols = cols
|
||||
p.rows = rows
|
||||
exec('nuwiki.table.insert', { p })
|
||||
end
|
||||
|
||||
function M.table_align()
|
||||
exec('nuwiki.table.align', pos_args())
|
||||
end
|
||||
|
||||
function M.table_move_column_left()
|
||||
local p = pos_args()[1]
|
||||
p.dir = 'left'
|
||||
exec('nuwiki.table.moveColumn', { p })
|
||||
end
|
||||
|
||||
function M.table_move_column_right()
|
||||
local p = pos_args()[1]
|
||||
p.dir = 'right'
|
||||
exec('nuwiki.table.moveColumn', { p })
|
||||
end
|
||||
|
||||
M.colorize = _not_yet(':VimwikiColorize')
|
||||
|
||||
-- §13.1 Cluster C — link helpers.
|
||||
|
||||
@@ -255,12 +255,12 @@ function M.attach(bufnr, mappings)
|
||||
|
||||
-- ===== Tables =====
|
||||
if on('table_editing') then
|
||||
map('n', 'gqq', deferred(':VimwikiTableAlignQ'), { desc = 'nuwiki: table align (deferred)' }, bufnr)
|
||||
map('n', 'gq1', deferred(':VimwikiTableAlignQ1'), { desc = 'nuwiki: table align row1 (deferred)' }, bufnr)
|
||||
map('n', 'gww', deferred(':VimwikiTableAlignW'), { desc = 'nuwiki: table align wide (deferred)' }, bufnr)
|
||||
map('n', 'gw1', deferred(':VimwikiTableAlignW1'), { desc = 'nuwiki: table align row1 wide (deferred)' }, bufnr)
|
||||
map('n', '<A-Left>', deferred(':VimwikiTableMoveColumnLeft'), { desc = 'nuwiki: table col left (deferred)' }, bufnr)
|
||||
map('n', '<A-Right>', deferred(':VimwikiTableMoveColumnRight'), { desc = 'nuwiki: table col right (deferred)' }, bufnr)
|
||||
map('n', 'gqq', cmd.table_align, { desc = 'nuwiki: align table' }, bufnr)
|
||||
map('n', 'gq1', cmd.table_align, { desc = 'nuwiki: align table' }, bufnr)
|
||||
map('n', 'gww', cmd.table_align, { desc = 'nuwiki: align table (wide)' }, bufnr)
|
||||
map('n', 'gw1', cmd.table_align, { desc = 'nuwiki: align table (wide)' }, bufnr)
|
||||
map('n', '<A-Left>', cmd.table_move_column_left, { desc = 'nuwiki: move column left' }, bufnr)
|
||||
map('n', '<A-Right>', cmd.table_move_column_right, { desc = 'nuwiki: move column right' }, bufnr)
|
||||
end
|
||||
|
||||
-- ===== HTML =====
|
||||
|
||||
Reference in New Issue
Block a user