fix(tables): auto-realign on edit + stop dragging trailing | onto new row
Two bugs in the insert-mode table editing path: 1. <Tab> past the last cell (and <CR> on a table row) used <CR>+typed keystrokes to create a new row, which split the current line at the cursor — pulling the trailing | down onto the next line and mangling the formatting. Both paths now go through helpers that append() a fresh row beside the current one without touching it. 2. The table never realigned to new content. Ported the LSP-side render_aligned_table algorithm to Lua + VimL so smart_tab, smart_shift_tab, and smart_return tighten column widths locally on every navigation. No vim-lsp / nuwiki server roundtrip required. Neovim side schedules the work via vim.schedule (textlock-safe); plain Vim hands off via <Cmd>:call …<CR> to keep insert mode and avoid the cmdline-mode flash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+201
-43
@@ -684,6 +684,170 @@ function! nuwiki#commands#paste_url() abort
|
||||
call s:exec('nuwiki.link.pasteUrl', l:args)
|
||||
endfunction
|
||||
|
||||
" ===== Pure-VimL table alignment =====
|
||||
"
|
||||
" Mirrors `lua/nuwiki/commands.lua`'s `align_table_at` so plain Vim
|
||||
" buffers get the same "tighten columns as cells grow" behaviour the
|
||||
" Neovim path enjoys, without needing a vim-lsp roundtrip. Algorithm
|
||||
" matches `crates/nuwiki-lsp/src/commands.rs::ops::render_aligned_table`.
|
||||
|
||||
function! s:is_table_row(line) abort
|
||||
return a:line =~# '^\s*|.*|\s*$'
|
||||
endfunction
|
||||
|
||||
function! s:table_cell_starts(line) abort
|
||||
" 1-based byte positions of each `|`. Cursor lands one past pipe N at
|
||||
" 1-based byte (starts[N] + 1) — that's the first space inside the
|
||||
" cell the pipe opens.
|
||||
let l:out = []
|
||||
let l:pos = 0
|
||||
while 1
|
||||
let l:bar = stridx(a:line, '|', l:pos)
|
||||
if l:bar < 0 | break | endif
|
||||
call add(l:out, l:bar + 1)
|
||||
let l:pos = l:bar + 1
|
||||
endwhile
|
||||
return l:out
|
||||
endfunction
|
||||
|
||||
function! s:find_table_range(row) abort
|
||||
let l:total = line('$')
|
||||
let l:s = a:row
|
||||
while l:s > 1 && s:is_table_row(getline(l:s - 1))
|
||||
let l:s -= 1
|
||||
endwhile
|
||||
let l:e = a:row
|
||||
while l:e < l:total && s:is_table_row(getline(l:e + 1))
|
||||
let l:e += 1
|
||||
endwhile
|
||||
return [l:s, l:e]
|
||||
endfunction
|
||||
|
||||
function! s:parse_table_row(line) abort
|
||||
let l:indent = matchstr(a:line, '^\s*')
|
||||
let l:body = strpart(a:line, len(l:indent))
|
||||
let l:segments = []
|
||||
let l:pos = 0
|
||||
while 1
|
||||
let l:bar = stridx(l:body, '|', l:pos)
|
||||
if l:bar < 0
|
||||
call add(l:segments, strpart(l:body, l:pos))
|
||||
break
|
||||
endif
|
||||
call add(l:segments, strpart(l:body, l:pos, l:bar - l:pos))
|
||||
let l:pos = l:bar + 1
|
||||
endwhile
|
||||
" Drop the empty piece before the opening `|` and the trailing piece
|
||||
" after the closing `|`.
|
||||
if len(l:segments) > 0 && l:segments[0] ==# ''
|
||||
call remove(l:segments, 0)
|
||||
endif
|
||||
if len(l:segments) > 0 && l:segments[-1] =~# '^\s*$'
|
||||
call remove(l:segments, -1)
|
||||
endif
|
||||
call map(l:segments, {_, c -> substitute(substitute(c, '^\s\+', '', ''), '\s\+$', '', '')})
|
||||
return [l:indent, l:segments]
|
||||
endfunction
|
||||
|
||||
function! s:is_sep_cell(s) abort
|
||||
return a:s !=# '' && a:s =~# '^[-:[:space:]]\+$'
|
||||
endfunction
|
||||
|
||||
function! s:is_sep_row(cells) abort
|
||||
if empty(a:cells) | return 0 | endif
|
||||
for l:c in a:cells
|
||||
if !s:is_sep_cell(l:c) | return 0 | endif
|
||||
endfor
|
||||
return 1
|
||||
endfunction
|
||||
|
||||
function! s:compute_widths(rows) abort
|
||||
let l:max_cols = 0
|
||||
for l:r in a:rows
|
||||
if len(l:r.cells) > l:max_cols
|
||||
let l:max_cols = len(l:r.cells)
|
||||
endif
|
||||
endfor
|
||||
let l:widths = repeat([1], l:max_cols)
|
||||
for l:r in a:rows
|
||||
if !s:is_sep_row(l:r.cells)
|
||||
let l:i = 0
|
||||
for l:c in l:r.cells
|
||||
let l:w = strchars(l:c)
|
||||
if l:w > l:widths[l:i]
|
||||
let l:widths[l:i] = l:w
|
||||
endif
|
||||
let l:i += 1
|
||||
endfor
|
||||
endif
|
||||
endfor
|
||||
return l:widths
|
||||
endfunction
|
||||
|
||||
function! s:render_table_row(indent, cells, widths, all_sep) abort
|
||||
let l:out = a:indent . '|'
|
||||
let l:i = 0
|
||||
for l:w in a:widths
|
||||
if a:all_sep
|
||||
let l:out .= repeat('-', l:w + 2)
|
||||
else
|
||||
let l:c = l:i < len(a:cells) ? a:cells[l:i] : ''
|
||||
let l:pad = l:w - strchars(l:c)
|
||||
if l:pad < 0 | let l:pad = 0 | endif
|
||||
let l:out .= ' ' . l:c . repeat(' ', l:pad) . ' '
|
||||
endif
|
||||
let l:out .= '|'
|
||||
let l:i += 1
|
||||
endfor
|
||||
return l:out
|
||||
endfunction
|
||||
|
||||
function! s:align_table_at(row) abort
|
||||
let [l:s, l:e] = s:find_table_range(a:row)
|
||||
let l:rows = []
|
||||
let l:i = l:s
|
||||
while l:i <= l:e
|
||||
let [l:indent, l:cells] = s:parse_table_row(getline(l:i))
|
||||
call add(l:rows, { 'indent': l:indent, 'cells': l:cells })
|
||||
let l:i += 1
|
||||
endwhile
|
||||
let l:widths = s:compute_widths(l:rows)
|
||||
if empty(l:widths) | return | endif
|
||||
let l:new_lines = []
|
||||
for l:r in l:rows
|
||||
call add(l:new_lines, s:render_table_row(l:r.indent, l:r.cells, l:widths, s:is_sep_row(l:r.cells)))
|
||||
endfor
|
||||
call setline(l:s, l:new_lines)
|
||||
endfunction
|
||||
|
||||
" Public helpers invoked via `<Cmd>` from the `<expr>` mappings. We
|
||||
" can't mutate the buffer from inside the `<expr>` body (textlock), so
|
||||
" the mappings hand off here via `<Cmd>:call …<CR>`, which runs an Ex
|
||||
" command without leaving insert mode.
|
||||
|
||||
function! nuwiki#commands#_table_insert_row_below(row, cursor_col) abort
|
||||
call s:align_table_at(a:row)
|
||||
let l:line = getline(a:row)
|
||||
let l:indent = matchstr(l:line, '^\s*')
|
||||
let l:starts = s:table_cell_starts(l:line)
|
||||
let l:cells = max([len(l:starts) - 1, 1])
|
||||
let l:new_row = l:indent . '|' . repeat(' |', l:cells)
|
||||
call append(a:row, l:new_row)
|
||||
call cursor(a:row + 1, a:cursor_col)
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#_table_jump_to_cell(row, target_idx) abort
|
||||
call s:align_table_at(a:row)
|
||||
let l:starts = s:table_cell_starts(getline(a:row))
|
||||
if a:target_idx < 1 || a:target_idx >= len(l:starts)
|
||||
return
|
||||
endif
|
||||
" `starts[N]` is the 1-based byte position of the N-th `|`. The first
|
||||
" space inside the cell that pipe opens sits at byte `starts[N] + 1`
|
||||
" (1-based), which is what `cursor()` wants.
|
||||
call cursor(a:row, l:starts[a:target_idx - 1] + 1)
|
||||
endfunction
|
||||
|
||||
" Smart <CR> for insert mode — vimwiki `:VimwikiReturn` parity.
|
||||
" Used as `inoremap <expr> <CR> nuwiki#commands#smart_return()`. The
|
||||
" returned string is fed into Vim's input stream after the function
|
||||
@@ -691,13 +855,12 @@ endfunction
|
||||
function! nuwiki#commands#smart_return() abort
|
||||
let l:line = getline('.')
|
||||
" `<expr>` runs under textlock, so the function must be side-effect-
|
||||
" free on the buffer. The returned string is fed into Vim's input
|
||||
" stream after we unwind — we encode the table-row insertion as
|
||||
" explicit keystrokes (matches lua/nuwiki/commands.lua).
|
||||
" free on the buffer. Buffer-mutating branches return a `<Cmd>:call
|
||||
" …<CR>` keystroke that hands off to a regular function.
|
||||
if l:line =~# '^\s*|.*|\s*$'
|
||||
let l:n = max([len(split(l:line, '|', 1)) - 2, 1])
|
||||
let l:new_row = '|' . repeat(' |', l:n)
|
||||
return "\<CR>" . l:new_row . "\<Esc>0li"
|
||||
let l:indent = matchstr(l:line, '^\s*')
|
||||
let l:cursor_col = len(l:indent) + 2
|
||||
return "\<Cmd>call nuwiki#commands#_table_insert_row_below(" . line('.') . ", " . l:cursor_col . ")\<CR>"
|
||||
endif
|
||||
let l:m = matchlist(l:line, '^\(\s*\)\([-*#]\)\s\(.*\)$')
|
||||
if empty(l:m)
|
||||
@@ -730,21 +893,10 @@ endfunction
|
||||
" Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||
" Used as `<expr>` mappings so we can pass `<Tab>` / `<S-Tab>` through
|
||||
" verbatim when the cursor isn't on a table row.
|
||||
function! s:table_cell_starts(line) abort
|
||||
let l:out = []
|
||||
let l:pos = 0
|
||||
while 1
|
||||
let l:bar = stridx(a:line, '|', l:pos)
|
||||
if l:bar < 0 | break | endif
|
||||
call add(l:out, l:bar + 1)
|
||||
let l:pos = l:bar + 1
|
||||
endwhile
|
||||
return l:out
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#smart_tab() abort
|
||||
let l:line = getline('.')
|
||||
if l:line !~# '^\s*|.*|\s*$'
|
||||
if !s:is_table_row(l:line)
|
||||
return "\<Tab>"
|
||||
endif
|
||||
let l:starts = s:table_cell_starts(l:line)
|
||||
@@ -752,37 +904,43 @@ function! nuwiki#commands#smart_tab() abort
|
||||
return "\<Tab>"
|
||||
endif
|
||||
let l:col = col('.') - 1
|
||||
for l:s in l:starts
|
||||
if l:s > l:col + 1 && l:s < len(l:line)
|
||||
return "\<Esc>" . (l:s + 1) . '|i'
|
||||
endif
|
||||
endfor
|
||||
let l:n = len(l:starts) - 1
|
||||
let l:new_row = '|' . repeat(' |', l:n)
|
||||
return "\<CR>" . l:new_row . "\<Esc>0li"
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#smart_shift_tab() abort
|
||||
let l:line = getline('.')
|
||||
if l:line !~# '^\s*|.*|\s*$'
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:starts = s:table_cell_starts(l:line)
|
||||
if len(l:starts) < 2
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:col = col('.') - 1
|
||||
let l:current_idx = -1
|
||||
let l:i = 0
|
||||
let l:current_idx = 1
|
||||
let l:i = 1
|
||||
for l:s in l:starts
|
||||
if l:s <= l:col + 1 | let l:current_idx = l:i | endif
|
||||
let l:i += 1
|
||||
endfor
|
||||
if l:current_idx <= 0
|
||||
let l:target_idx = l:current_idx + 1
|
||||
if l:target_idx < len(l:starts)
|
||||
return "\<Cmd>call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\<CR>"
|
||||
endif
|
||||
" Past the final cell → add a fresh row below, cursor in its first cell.
|
||||
let l:indent = matchstr(l:line, '^\s*')
|
||||
let l:cursor_col = len(l:indent) + 2
|
||||
return "\<Cmd>call nuwiki#commands#_table_insert_row_below(" . line('.') . ", " . l:cursor_col . ")\<CR>"
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#smart_shift_tab() abort
|
||||
let l:line = getline('.')
|
||||
if !s:is_table_row(l:line)
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:target = l:starts[l:current_idx - 1]
|
||||
return "\<Esc>" . (l:target + 1) . '|i'
|
||||
let l:starts = s:table_cell_starts(l:line)
|
||||
if len(l:starts) < 2
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:col = col('.') - 1
|
||||
let l:current_idx = 0
|
||||
let l:i = 1
|
||||
for l:s in l:starts
|
||||
if l:s <= l:col + 1 | let l:current_idx = l:i | endif
|
||||
let l:i += 1
|
||||
endfor
|
||||
if l:current_idx <= 1
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:target_idx = l:current_idx - 1
|
||||
return "\<Cmd>call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\<CR>"
|
||||
endfunction
|
||||
|
||||
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
||||
|
||||
Reference in New Issue
Block a user