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)
|
call s:exec('nuwiki.link.pasteUrl', l:args)
|
||||||
endfunction
|
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.
|
" Smart <CR> for insert mode — vimwiki `:VimwikiReturn` parity.
|
||||||
" Used as `inoremap <expr> <CR> nuwiki#commands#smart_return()`. The
|
" Used as `inoremap <expr> <CR> nuwiki#commands#smart_return()`. The
|
||||||
" returned string is fed into Vim's input stream after the function
|
" returned string is fed into Vim's input stream after the function
|
||||||
@@ -691,13 +855,12 @@ endfunction
|
|||||||
function! nuwiki#commands#smart_return() abort
|
function! nuwiki#commands#smart_return() abort
|
||||||
let l:line = getline('.')
|
let l:line = getline('.')
|
||||||
" `<expr>` runs under textlock, so the function must be side-effect-
|
" `<expr>` runs under textlock, so the function must be side-effect-
|
||||||
" free on the buffer. The returned string is fed into Vim's input
|
" free on the buffer. Buffer-mutating branches return a `<Cmd>:call
|
||||||
" stream after we unwind — we encode the table-row insertion as
|
" …<CR>` keystroke that hands off to a regular function.
|
||||||
" explicit keystrokes (matches lua/nuwiki/commands.lua).
|
|
||||||
if l:line =~# '^\s*|.*|\s*$'
|
if l:line =~# '^\s*|.*|\s*$'
|
||||||
let l:n = max([len(split(l:line, '|', 1)) - 2, 1])
|
let l:indent = matchstr(l:line, '^\s*')
|
||||||
let l:new_row = '|' . repeat(' |', l:n)
|
let l:cursor_col = len(l:indent) + 2
|
||||||
return "\<CR>" . l:new_row . "\<Esc>0li"
|
return "\<Cmd>call nuwiki#commands#_table_insert_row_below(" . line('.') . ", " . l:cursor_col . ")\<CR>"
|
||||||
endif
|
endif
|
||||||
let l:m = matchlist(l:line, '^\(\s*\)\([-*#]\)\s\(.*\)$')
|
let l:m = matchlist(l:line, '^\(\s*\)\([-*#]\)\s\(.*\)$')
|
||||||
if empty(l:m)
|
if empty(l:m)
|
||||||
@@ -730,21 +893,10 @@ endfunction
|
|||||||
" Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
" Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||||
" Used as `<expr>` mappings so we can pass `<Tab>` / `<S-Tab>` through
|
" Used as `<expr>` mappings so we can pass `<Tab>` / `<S-Tab>` through
|
||||||
" verbatim when the cursor isn't on a table row.
|
" 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
|
function! nuwiki#commands#smart_tab() abort
|
||||||
let l:line = getline('.')
|
let l:line = getline('.')
|
||||||
if l:line !~# '^\s*|.*|\s*$'
|
if !s:is_table_row(l:line)
|
||||||
return "\<Tab>"
|
return "\<Tab>"
|
||||||
endif
|
endif
|
||||||
let l:starts = s:table_cell_starts(l:line)
|
let l:starts = s:table_cell_starts(l:line)
|
||||||
@@ -752,37 +904,43 @@ function! nuwiki#commands#smart_tab() abort
|
|||||||
return "\<Tab>"
|
return "\<Tab>"
|
||||||
endif
|
endif
|
||||||
let l:col = col('.') - 1
|
let l:col = col('.') - 1
|
||||||
for l:s in l:starts
|
let l:current_idx = 1
|
||||||
if l:s > l:col + 1 && l:s < len(l:line)
|
let l:i = 1
|
||||||
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
|
|
||||||
for l:s in l:starts
|
for l:s in l:starts
|
||||||
if l:s <= l:col + 1 | let l:current_idx = l:i | endif
|
if l:s <= l:col + 1 | let l:current_idx = l:i | endif
|
||||||
let l:i += 1
|
let l:i += 1
|
||||||
endfor
|
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>"
|
return "\<S-Tab>"
|
||||||
endif
|
endif
|
||||||
let l:target = l:starts[l:current_idx - 1]
|
let l:starts = s:table_cell_starts(l:line)
|
||||||
return "\<Esc>" . (l:target + 1) . '|i'
|
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
|
endfunction
|
||||||
|
|
||||||
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
||||||
|
|||||||
+222
-48
@@ -582,28 +582,192 @@ function M.list_toggle_or_add_checkbox()
|
|||||||
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ===== Pure-Lua table alignment =====
|
||||||
|
--
|
||||||
|
-- The LSP exposes `nuwiki.table.align` (see `M.table_align()` below),
|
||||||
|
-- but that path is async — by the time the workspace edit lands the
|
||||||
|
-- cursor has already been positioned by our keystrokes, which leaves
|
||||||
|
-- the cursor visually mid-cell after columns expand. For insert-mode
|
||||||
|
-- editing we replicate the alignment logic locally so we can realign
|
||||||
|
-- *before* placing the cursor, all in a single `vim.schedule` tick.
|
||||||
|
-- The algorithm matches `crates/nuwiki-lsp/src/commands.rs::ops::
|
||||||
|
-- render_aligned_table`.
|
||||||
|
|
||||||
|
local function is_table_row(line)
|
||||||
|
return line ~= nil and line:match('^%s*|.*|%s*$') ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function table_cell_starts(line)
|
||||||
|
-- Returns 1-based byte positions of each `|` separator. Cell N spans
|
||||||
|
-- (starts[N], starts[N+1]); placing the cursor at 0-based byte column
|
||||||
|
-- `starts[i]` lands one position past the i-th `|` (the first space
|
||||||
|
-- of the cell that the `|` opens).
|
||||||
|
local out = {}
|
||||||
|
local pos = 0
|
||||||
|
while true do
|
||||||
|
local bar = line:find('|', pos + 1, true)
|
||||||
|
if not bar then break end
|
||||||
|
table.insert(out, bar)
|
||||||
|
pos = bar
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_table_range(row)
|
||||||
|
-- 1-based inclusive line numbers spanning the table that contains `row`.
|
||||||
|
local total = vim.api.nvim_buf_line_count(0)
|
||||||
|
local s = row
|
||||||
|
while s > 1 and is_table_row(vim.fn.getline(s - 1)) do
|
||||||
|
s = s - 1
|
||||||
|
end
|
||||||
|
local e = row
|
||||||
|
while e < total and is_table_row(vim.fn.getline(e + 1)) do
|
||||||
|
e = e + 1
|
||||||
|
end
|
||||||
|
return s, e
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_table_row(line)
|
||||||
|
local indent = line:match('^(%s*)') or ''
|
||||||
|
local body = line:sub(#indent + 1)
|
||||||
|
local segments = {}
|
||||||
|
local pos = 1
|
||||||
|
while true do
|
||||||
|
local bar = body:find('|', pos, true)
|
||||||
|
if not bar then
|
||||||
|
table.insert(segments, body:sub(pos))
|
||||||
|
break
|
||||||
|
end
|
||||||
|
table.insert(segments, body:sub(pos, bar - 1))
|
||||||
|
pos = bar + 1
|
||||||
|
end
|
||||||
|
-- Drop the empty piece before the opening `|` and the trailing piece
|
||||||
|
-- after the closing `|`.
|
||||||
|
if #segments > 0 and segments[1] == '' then table.remove(segments, 1) end
|
||||||
|
if #segments > 0 and segments[#segments]:match('^%s*$') then
|
||||||
|
table.remove(segments, #segments)
|
||||||
|
end
|
||||||
|
for i, c in ipairs(segments) do
|
||||||
|
segments[i] = c:match('^%s*(.-)%s*$') or ''
|
||||||
|
end
|
||||||
|
return indent, segments
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_separator_cell(s)
|
||||||
|
return s ~= '' and s:match('^[%-:%s]+$') ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_separator_row(cells)
|
||||||
|
if #cells == 0 then return false end
|
||||||
|
for _, c in ipairs(cells) do
|
||||||
|
if not is_separator_cell(c) then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function content_width(s)
|
||||||
|
-- Match the LSP-side `chars().count()` so user-triggered `gqq` and
|
||||||
|
-- the auto-realign here agree on column widths.
|
||||||
|
local ok, w = pcall(vim.fn.strchars, s, 1)
|
||||||
|
if ok and type(w) == 'number' then return w end
|
||||||
|
return #s
|
||||||
|
end
|
||||||
|
|
||||||
|
local function compute_table_widths(rows)
|
||||||
|
local widths = {}
|
||||||
|
local max_cols = 0
|
||||||
|
for _, r in ipairs(rows) do
|
||||||
|
if #r.cells > max_cols then max_cols = #r.cells end
|
||||||
|
end
|
||||||
|
for i = 1, max_cols do widths[i] = 1 end
|
||||||
|
for _, r in ipairs(rows) do
|
||||||
|
if not is_separator_row(r.cells) then
|
||||||
|
for i, c in ipairs(r.cells) do
|
||||||
|
local w = content_width(c)
|
||||||
|
if w > widths[i] then widths[i] = w end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return widths
|
||||||
|
end
|
||||||
|
|
||||||
|
local function render_table_row(indent, cells, widths, all_sep)
|
||||||
|
local out = { indent, '|' }
|
||||||
|
for i = 1, #widths do
|
||||||
|
local w = widths[i]
|
||||||
|
if all_sep then
|
||||||
|
table.insert(out, string.rep('-', w + 2))
|
||||||
|
else
|
||||||
|
local c = cells[i] or ''
|
||||||
|
local pad = w - content_width(c)
|
||||||
|
if pad < 0 then pad = 0 end
|
||||||
|
table.insert(out, ' ' .. c .. string.rep(' ', pad) .. ' ')
|
||||||
|
end
|
||||||
|
table.insert(out, '|')
|
||||||
|
end
|
||||||
|
return table.concat(out)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function align_table_at(row)
|
||||||
|
local s, e = find_table_range(row)
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(0, s - 1, e, false)
|
||||||
|
local rows = {}
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
local indent, cells = parse_table_row(l)
|
||||||
|
table.insert(rows, { indent = indent, cells = cells })
|
||||||
|
end
|
||||||
|
local widths = compute_table_widths(rows)
|
||||||
|
if #widths == 0 then return end
|
||||||
|
local new_lines = {}
|
||||||
|
for _, r in ipairs(rows) do
|
||||||
|
table.insert(new_lines, render_table_row(r.indent, r.cells, widths, is_separator_row(r.cells)))
|
||||||
|
end
|
||||||
|
vim.api.nvim_buf_set_lines(0, s - 1, e, false, new_lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_insert_mode()
|
||||||
|
if vim.api.nvim_get_mode().mode ~= 'i' then
|
||||||
|
vim.cmd('startinsert')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function schedule_insert_table_row_below(row)
|
||||||
|
-- After `align_table_at`, the row count of the table containing
|
||||||
|
-- `row` is fixed and `getline(row)` reflects the aligned content.
|
||||||
|
vim.schedule(function()
|
||||||
|
align_table_at(row)
|
||||||
|
local new_line = vim.fn.getline(row)
|
||||||
|
local starts = table_cell_starts(new_line)
|
||||||
|
if #starts < 2 then return end
|
||||||
|
local indent = new_line:match('^(%s*)') or ''
|
||||||
|
local cell_count = #starts - 1
|
||||||
|
local new_row = indent .. '|' .. string.rep(' |', cell_count)
|
||||||
|
vim.api.nvim_buf_set_lines(0, row, row, false, { new_row })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
|
||||||
|
ensure_insert_mode()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
--- Smart `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`.
|
--- Smart `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`.
|
||||||
--- Used as an `<expr>` keymap so the function is side-effect-free on
|
--- Used as an `<expr>` keymap so the function is side-effect-free on
|
||||||
--- the buffer (textlock is active). The returned string is fed into
|
--- the buffer (textlock is active). Non-table branches encode their
|
||||||
--- Vim's input stream — we encode everything (including the table-row
|
--- behaviour as returned keystrokes; the table-row branch schedules a
|
||||||
--- insertion) as keystrokes so the user's typed keys after `<CR>` land
|
--- post-textlock callback that aligns the table, inserts a fresh row,
|
||||||
--- at the right cursor position.
|
--- and places the cursor inside the new row's first cell.
|
||||||
function M.smart_return()
|
function M.smart_return()
|
||||||
local line = vim.api.nvim_get_current_line()
|
local line = vim.api.nvim_get_current_line()
|
||||||
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
||||||
local cr = tc('<CR>')
|
local cr = tc('<CR>')
|
||||||
local esc = tc('<Esc>')
|
local esc = tc('<Esc>')
|
||||||
|
|
||||||
-- Table row → newline + empty row + cursor inside first cell.
|
-- Table row → align existing rows, then add a fresh empty row below.
|
||||||
if line:match('^%s*|.*|%s*$') then
|
-- Doing this via `<CR>` + typed row keystrokes would split the
|
||||||
local cell_count = 0
|
-- current line at the cursor and drag the trailing `|` onto the new
|
||||||
for _ in line:gmatch('|') do cell_count = cell_count + 1 end
|
-- line — so we punt to `vim.schedule` instead.
|
||||||
cell_count = math.max(cell_count - 1, 1)
|
if is_table_row(line) then
|
||||||
local new_row = '|' .. string.rep(' |', cell_count)
|
local cur = vim.api.nvim_win_get_cursor(0)
|
||||||
-- After `<CR>` + new_row, cursor sits at end of new row. Bounce to
|
schedule_insert_table_row_below(cur[1])
|
||||||
-- normal, jump to col 0, step right past the leading `|`, re-enter
|
return ''
|
||||||
-- insert: `<Esc>0li` puts the cursor immediately after the `|`.
|
|
||||||
return cr .. new_row .. esc .. '0li'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- List item with marker?
|
-- List item with marker?
|
||||||
@@ -629,66 +793,76 @@ end
|
|||||||
|
|
||||||
--- Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
--- Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||||
--- Used as `<expr>` keymaps so we can fall back to a literal `<Tab>` /
|
--- Used as `<expr>` keymaps so we can fall back to a literal `<Tab>` /
|
||||||
--- `<S-Tab>` when the cursor isn't on a table row. Inside a row:
|
--- `<S-Tab>` when the cursor isn't on a table row. Inside a row both
|
||||||
--- `<Tab>` jumps to the next cell, creating a fresh row below the
|
--- variants align the table first (so columns stay tight as cells
|
||||||
--- current one if we were in the final cell. `<S-Tab>` jumps to the
|
--- grow), then jump to the adjacent cell; `<Tab>` past the final cell
|
||||||
--- previous cell, no-op past the first.
|
--- adds a fresh row below, `<S-Tab>` past the first cell is a no-op.
|
||||||
local function table_cell_starts(line)
|
|
||||||
-- Returns 0-based byte offsets right after each `|` separator.
|
|
||||||
local out = {}
|
|
||||||
local pos = 0
|
|
||||||
while true do
|
|
||||||
local bar = line:find('|', pos + 1, true)
|
|
||||||
if not bar then break end
|
|
||||||
table.insert(out, bar) -- 1-based: position immediately AFTER the |
|
|
||||||
pos = bar
|
|
||||||
end
|
|
||||||
return out
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.smart_tab()
|
function M.smart_tab()
|
||||||
local line = vim.api.nvim_get_current_line()
|
local line = vim.api.nvim_get_current_line()
|
||||||
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
||||||
local tab = tc('<Tab>')
|
local tab = tc('<Tab>')
|
||||||
if not line:match('^%s*|.*|%s*$') then
|
if not is_table_row(line) then
|
||||||
return tab
|
return tab
|
||||||
end
|
end
|
||||||
local cell_starts = table_cell_starts(line)
|
local cell_starts = table_cell_starts(line)
|
||||||
if #cell_starts < 2 then return tab end
|
if #cell_starts < 2 then return tab end
|
||||||
local _, col = unpack(vim.api.nvim_win_get_cursor(0)) -- 0-based byte col
|
local cur = vim.api.nvim_win_get_cursor(0)
|
||||||
-- Find the next cell start strictly after the cursor's column.
|
local row, col = cur[1], cur[2]
|
||||||
for _, start in ipairs(cell_starts) do
|
local current_idx = 1
|
||||||
if start > col + 1 and start < #line then
|
for i, start in ipairs(cell_starts) do
|
||||||
-- `<Esc>` + jump to that col (`start + 1` 1-based → +1 for space
|
if start <= col + 1 then current_idx = i end
|
||||||
-- inside the cell), then `i` to re-enter insert.
|
|
||||||
local target = start + 1
|
|
||||||
return tc('<Esc>') .. tostring(target) .. '|i'
|
|
||||||
end
|
end
|
||||||
|
local target_idx = current_idx + 1
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
align_table_at(row)
|
||||||
|
local new_line = vim.fn.getline(row)
|
||||||
|
local starts = table_cell_starts(new_line)
|
||||||
|
if target_idx < #starts then
|
||||||
|
-- Land at the first space of the target cell (byte position
|
||||||
|
-- equal to the 1-based pipe position).
|
||||||
|
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] })
|
||||||
|
ensure_insert_mode()
|
||||||
|
else
|
||||||
|
local indent = new_line:match('^(%s*)') or ''
|
||||||
|
local cell_count = math.max(#starts - 1, 1)
|
||||||
|
local new_row = indent .. '|' .. string.rep(' |', cell_count)
|
||||||
|
vim.api.nvim_buf_set_lines(0, row, row, false, { new_row })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
|
||||||
|
ensure_insert_mode()
|
||||||
end
|
end
|
||||||
-- Past last cell → insert a new row below, cursor in first cell.
|
end)
|
||||||
local cell_count = #cell_starts - 1
|
return ''
|
||||||
local new_row = '|' .. string.rep(' |', cell_count)
|
|
||||||
return tc('<CR>') .. new_row .. tc('<Esc>') .. '0li'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.smart_shift_tab()
|
function M.smart_shift_tab()
|
||||||
local line = vim.api.nvim_get_current_line()
|
local line = vim.api.nvim_get_current_line()
|
||||||
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
||||||
local stab = tc('<S-Tab>')
|
local stab = tc('<S-Tab>')
|
||||||
if not line:match('^%s*|.*|%s*$') then
|
if not is_table_row(line) then
|
||||||
return stab
|
return stab
|
||||||
end
|
end
|
||||||
local cell_starts = table_cell_starts(line)
|
local cell_starts = table_cell_starts(line)
|
||||||
if #cell_starts < 2 then return stab end
|
if #cell_starts < 2 then return stab end
|
||||||
local _, col = unpack(vim.api.nvim_win_get_cursor(0))
|
local cur = vim.api.nvim_win_get_cursor(0)
|
||||||
-- Find the cell the cursor is in, then go to the *previous* cell.
|
local row, col = cur[1], cur[2]
|
||||||
local current_idx
|
local current_idx
|
||||||
for i, start in ipairs(cell_starts) do
|
for i, start in ipairs(cell_starts) do
|
||||||
if start <= col + 1 then current_idx = i end
|
if start <= col + 1 then current_idx = i end
|
||||||
end
|
end
|
||||||
if not current_idx or current_idx <= 1 then return stab end
|
if not current_idx or current_idx <= 1 then return stab end
|
||||||
local target = cell_starts[current_idx - 1]
|
local target_idx = current_idx - 1
|
||||||
return tc('<Esc>') .. tostring(target + 1) .. '|i'
|
|
||||||
|
vim.schedule(function()
|
||||||
|
align_table_at(row)
|
||||||
|
local new_line = vim.fn.getline(row)
|
||||||
|
local starts = table_cell_starts(new_line)
|
||||||
|
if target_idx < 1 or target_idx >= #starts then return end
|
||||||
|
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] })
|
||||||
|
ensure_insert_mode()
|
||||||
|
end)
|
||||||
|
return ''
|
||||||
end
|
end
|
||||||
|
|
||||||
-- §13.1 Cluster B — table rewriters.
|
-- §13.1 Cluster B — table rewriters.
|
||||||
|
|||||||
Reference in New Issue
Block a user