" autoload/nuwiki/commands.vim — Vim-side wrappers around the LSP " commands the server advertises. " " Lives in autoload/ so the cost is paid only when a `:Vimwiki*` / " `:Nuwiki*` command is actually invoked. Dispatches via vim-lsp's " `lsp#send_request`. coc.nvim's command surface is rich enough that " users should just call `:CocCommand nuwiki.x` directly there — this " file targets the vim-lsp path. let s:server = 'nuwiki' function! s:buf_uri() abort return 'file://' . fnamemodify(expand('%:p'), ':p') endfunction function! s:cursor_position() abort return { \ 'textDocument': { 'uri': s:buf_uri() }, \ 'position': { 'line': line('.') - 1, 'character': col('.') - 1 }, \ } endfunction function! s:has_vimlsp() abort return exists('*lsp#send_request') endfunction function! s:has_coc() abort return exists('*CocActionAsync') endfunction " Adapt a coc.nvim (err, result) callback into the vim-lsp notification " shape that existing on_notification handlers expect. function! s:coc_wrap(upstream, err, result) abort if a:err isnot v:null && a:err !=# 0 echohl ErrorMsg echom 'nuwiki: ' . (type(a:err) == type('') ? a:err : string(a:err)) echohl None return endif call a:upstream({ 'response': { 'result': a:result } }) endfunction function! s:exec(command, arguments, ...) abort if s:has_vimlsp() let l:opts = { \ 'method': 'workspace/executeCommand', \ 'params': { 'command': a:command, 'arguments': a:arguments }, \ } if a:0 >= 1 let l:opts['on_notification'] = a:1 endif call lsp#send_request(s:server, l:opts) return endif if s:has_coc() " CocActionAsync('runCommand', cmdid, arg1, arg2, …, callback) " The arguments list is spread as individual positional args. let l:args = ['runCommand', a:command] + a:arguments if a:0 >= 1 call add(l:args, function('s:coc_wrap', [a:1])) endif call call('CocActionAsync', l:args) return endif echohl ErrorMsg echom 'nuwiki: no supported LSP client. Install vim-lsp or configure coc.nvim.' echohl None endfunction " Shared jump-to-definition helper used by follow_link* and the ftplugin. function! s:jump_definition() abort if exists(':LspDefinition') == 2 LspDefinition elseif s:has_coc() call CocActionAsync('jumpDefinition') else echohl WarningMsg echom 'nuwiki: no LSP client available for jump-to-definition' echohl None endif endfunction " Shared references helper used by the ftplugin. function! nuwiki#commands#backlinks() abort if exists(':LspReferences') == 2 LspReferences elseif s:has_coc() call CocActionAsync('references') else echohl WarningMsg echom 'nuwiki: no LSP client available for references' echohl None endif endfunction function! s:open_uri_from(response, ...) abort let l:result = get(get(a:response, 'response', {}), 'result', {}) if type(l:result) != type({}) || !has_key(l:result, 'uri') return endif let l:path = substitute(l:result['uri'], '^file://', '', '') let l:cmd = a:0 >= 1 ? a:1 : 'edit' execute l:cmd . ' ' . fnameescape(l:path) endfunction " ===== Wiki picker ===== function! nuwiki#commands#wiki_index(count) abort let l:args = a:count > 0 ? [{ 'wiki': a:count - 1 }] : [{}] call s:exec('nuwiki.wiki.openIndex', l:args, \ function('s:open_uri_from')) endfunction " `:VimwikiUISelect` — pick a wiki from the configured list and open its " index. The list is read straight from config (see wiki_list below), not " from the server, so the picker works from any buffer before a wiki file is " ever opened — opening the chosen index then auto-starts the LSP via the " FileType autocmd. Plain Vim has no `vim.ui.select`, so use `inputlist()`. function! nuwiki#commands#wiki_ui_select() abort let l:wikis = nuwiki#commands#wiki_list() if empty(l:wikis) echohl WarningMsg | echom 'nuwiki: no wikis configured' | echohl None return endif let l:prompt = ['Select a wiki:'] let l:i = 0 for l:w in l:wikis let l:i += 1 call add(l:prompt, printf('%d. %s (%s)', l:i, l:w.name, l:w.root)) endfor let l:choice = inputlist(l:prompt) if l:choice < 1 || l:choice > len(l:wikis) return endif let l:w = l:wikis[l:choice - 1] execute 'edit ' . fnameescape(l:w.root . '/' . l:w.idx . l:w.ext) endfunction " `:VimwikiNextLink` / `:VimwikiPrevLink` — pure-VimL `[[…]]` jumper. function! nuwiki#commands#link_next() abort call search('\[\[', 'W') endfunction function! nuwiki#commands#link_prev() abort call search('\[\[', 'bW') endfunction " `:VimwikiBaddLink` — add the link target file to the buffer list " without focus. Routes through vim-lsp's textDocument/definition. function! nuwiki#commands#badd_link() abort if s:has_vimlsp() call lsp#send_request(s:server, { \ 'method': 'textDocument/definition', \ 'params': s:cursor_position(), \ 'on_notification': function('s:badd_from_response'), \ }) return endif if s:has_coc() let l:locs = CocAction('getDefinition') if type(l:locs) == type([]) && !empty(l:locs) let l:uri = get(l:locs[0], 'uri', get(l:locs[0], 'targetUri', '')) if !empty(l:uri) let l:path = substitute(l:uri, '^file://', '', '') execute 'badd ' . fnameescape(l:path) echom 'nuwiki: added ' . l:path endif endif return endif echohl ErrorMsg | echom 'nuwiki: no supported LSP client for badd_link' | echohl None endfunction function! s:badd_from_response(notification) abort let l:result = get(get(a:notification, 'response', {}), 'result', {}) let l:uri = '' if type(l:result) == type({}) let l:uri = get(l:result, 'uri', get(l:result, 'targetUri', '')) elseif type(l:result) == type([]) && !empty(l:result) let l:uri = get(l:result[0], 'uri', get(l:result[0], 'targetUri', '')) endif if l:uri ==# '' return endif let l:path = substitute(l:uri, '^file://', '', '') execute 'badd ' . fnameescape(l:path) echom 'nuwiki: added ' . l:path endfunction " ===== Smart : follow or wrap-then-follow ===== function! s:cursor_inside_wikilink() abort let l:line = getline('.') let l:col = col('.') let l:i = 1 let l:open = 0 while l:i < strlen(l:line) if strpart(l:line, l:i - 1, 2) ==# '[[' let l:open = l:i let l:i += 2 elseif strpart(l:line, l:i - 1, 2) ==# ']]' let l:open = 0 let l:i += 2 else let l:i += 1 endif if l:i > l:col break endif endwhile return l:open > 0 endfunction function! s:wrap_cword_as_wikilink() abort let l:word = expand('') if l:word ==# '' return 0 endif let l:line = getline('.') let l:col = col('.') " Find word boundaries around cursor (1-based). let l:left = l:col while l:left > 1 && strpart(l:line, l:left - 2, 1) =~# '[A-Za-z0-9_-]' let l:left -= 1 endwhile let l:right = l:col while l:right <= strlen(l:line) && strpart(l:line, l:right - 1, 1) =~# '[A-Za-z0-9_-]' let l:right += 1 endwhile let l:word_real = strpart(l:line, l:left - 1, l:right - l:left) if l:word_real ==# '' return 0 endif let l:new_line = strpart(l:line, 0, l:left - 1) . '[[' . l:word_real . ']]' . strpart(l:line, l:right - 1) call setline('.', l:new_line) call cursor(line('.'), l:left + 2) return 1 endfunction " Two-step behaviour matching vimwiki: " 1. First press on a bare word → wrap it as `[[word]]` and stop. " 2. Cursor is now inside `[[…]]`; next press follows the link. " 3. Press inside an existing `[[…]]` follows immediately. function! nuwiki#commands#follow_link_or_create() abort if s:cursor_inside_wikilink() call s:jump_definition() return endif if s:wrap_cword_as_wikilink() " Wrapped — wait for the user's second to follow. return endif " Nothing to wrap — fall through to plain definition. call s:jump_definition() endfunction " Pure-VimL bullet-continuation for `o` / `O` — mirrors the Lua side " so re-pressing the key keeps the list marker (and checkbox if any). function! nuwiki#commands#open_below_with_bullet() abort let l:line = getline('.') let l:match = matchlist(l:line, '^\(\s*\)\([-*#]\)\s') if empty(l:match) call feedkeys('o', 'n') return endif let l:prefix = l:match[1] . l:match[2] . ' ' if l:line =~# '^\s*[-*#]\s\+\[[ xXoO.\-]\]' let l:prefix .= '[ ] ' endif call feedkeys('o' . l:prefix, 'n') endfunction function! nuwiki#commands#open_above_with_bullet() abort let l:line = getline('.') let l:match = matchlist(l:line, '^\(\s*\)\([-*#]\)\s') if empty(l:match) call feedkeys('O', 'n') return endif let l:prefix = l:match[1] . l:match[2] . ' ' call feedkeys('O' . l:prefix, 'n') endfunction " ===== Heading navigation (pure VimL) ===== function! s:heading_level(line) abort let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s') if l:lead ==# '' return 0 endif let l:trail = matchstr(a:line, '\s\zs=\+\ze\s*$') if len(l:trail) != len(l:lead) return 0 endif return len(l:lead) endfunction function! s:current_heading_level() abort let l:lnum = line('.') while l:lnum > 0 let l:lvl = s:heading_level(getline(l:lnum)) if l:lvl > 0 return [l:lvl, l:lnum] endif let l:lnum -= 1 endwhile return [0, 0] endfunction function! s:jump_heading(direction, predicate) abort let l:total = line('$') let l:i = line('.') + a:direction while l:i >= 1 && l:i <= l:total let l:lvl = s:heading_level(getline(l:i)) if l:lvl > 0 && a:predicate(l:lvl, l:i) call cursor(l:i, 1) return endif let l:i += a:direction endwhile endfunction function! nuwiki#commands#next_header() abort call s:jump_heading(1, {l, n -> 1}) endfunction function! nuwiki#commands#prev_header() abort call s:jump_heading(-1, {l, n -> 1}) endfunction function! nuwiki#commands#next_sibling_header() abort let [l:lvl, _] = s:current_heading_level() call s:jump_heading(1, {l, n -> l <= l:lvl}) endfunction function! nuwiki#commands#prev_sibling_header() abort let [l:lvl, _] = s:current_heading_level() call s:jump_heading(-1, {l, n -> l <= l:lvl}) endfunction function! nuwiki#commands#parent_header() abort let [l:lvl, _] = s:current_heading_level() if l:lvl <= 1 return endif call s:jump_heading(-1, {l, n -> l < l:lvl}) endfunction function! nuwiki#commands#wiki_tab_index(count) abort let l:args = a:count > 0 ? [{ 'wiki': a:count - 1 }] : [{}] call s:exec('nuwiki.wiki.tabOpenIndex', l:args, \ {n -> s:open_uri_from(n, 'tabedit')}) endfunction function! nuwiki#commands#wiki_goto_page(name) abort let l:name = a:name if l:name ==# '' let l:name = input('Goto page: ') endif if l:name ==# '' return endif call s:exec('nuwiki.wiki.gotoPage', [{ 'page': l:name }], \ function('s:open_uri_from')) endfunction " ===== Diary ===== function! s:diary_open(cmd_name, ...) abort let l:open = a:0 >= 1 ? a:1 : 'edit' call s:exec(a:cmd_name, [{ 'uri': s:buf_uri() }], \ {n -> s:open_uri_from(n, l:open)}) endfunction function! nuwiki#commands#diary_today() abort call s:diary_open('nuwiki.diary.openToday') endfunction function! nuwiki#commands#diary_today_tab() abort call s:diary_open('nuwiki.diary.openToday', 'tabedit') endfunction function! nuwiki#commands#diary_yesterday() abort call s:diary_open('nuwiki.diary.openYesterday') endfunction function! nuwiki#commands#diary_tomorrow() abort call s:diary_open('nuwiki.diary.openTomorrow') endfunction function! nuwiki#commands#diary_index() abort call s:diary_open('nuwiki.diary.openIndex') endfunction function! nuwiki#commands#diary_next() abort call s:diary_open('nuwiki.diary.next') endfunction function! nuwiki#commands#diary_prev() abort call s:diary_open('nuwiki.diary.prev') endfunction function! nuwiki#commands#diary_generate_index() abort call s:exec('nuwiki.diary.generateIndex', [{ 'uri': s:buf_uri() }]) endfunction " ===== Global entry-point helpers (no LSP required) ===== " " These are used by the global mappings in plugin/nuwiki.vim so users can " open their wiki from any buffer. Files are opened directly from config; " the LSP auto-starts via the FileType autocmd once the buffer loads. " Return a dict with {root, ext, idx, diary_rel, diary_idx} for wiki #n " (0-based). Prefers g:nuwiki_wikis[n]; falls back to scalar g:nuwiki_* " vars and finally to built-in defaults. function! s:wiki_cfg(n) abort let l:w = {} if exists('g:nuwiki_wikis') && len(g:nuwiki_wikis) > a:n let l:w = g:nuwiki_wikis[a:n] endif let l:root = expand(get(l:w, 'root', \ get(g:, 'nuwiki_wiki_root', '~/vimwiki'))) let l:ext = get(l:w, 'file_extension', \ get(g:, 'nuwiki_file_extension', '.wiki')) if l:ext[0] !=# '.' | let l:ext = '.' . l:ext | endif let l:idx = get(l:w, 'index', 'index') let l:diary_rel = get(l:w, 'diary_rel_path', \ get(g:, 'nuwiki_diary_rel_path', 'diary')) let l:diary_idx = get(l:w, 'diary_index', 'diary') return { 'root': l:root, 'ext': l:ext, 'idx': l:idx, \ 'diary_rel': l:diary_rel, 'diary_idx': l:diary_idx } endfunction " Build the list of configured wikis as {name, root, ext, idx, diary_rel, " diary_idx} dicts, straight from config — no LSP round-trip. Drives the wiki " picker so it works before any wiki buffer (and therefore the server) exists. " Falls back to a single entry from the scalar `g:nuwiki_wiki_root` config. function! nuwiki#commands#wiki_list() abort let l:list = [] if exists('g:nuwiki_wikis') && !empty(g:nuwiki_wikis) let l:n = 0 for l:w in g:nuwiki_wikis let l:c = s:wiki_cfg(l:n) let l:c['name'] = get(l:w, 'name', fnamemodify(l:c.root, ':t')) call add(l:list, l:c) let l:n += 1 endfor else let l:c = s:wiki_cfg(0) let l:c['name'] = fnamemodify(l:c.root, ':t') call add(l:list, l:c) endif return l:list endfunction function! nuwiki#commands#open_wiki_path(tab) abort let l:c = s:wiki_cfg(0) execute (a:tab ? 'tabedit' : 'edit') . ' ' \ . fnameescape(l:c.root . '/' . l:c.idx . l:c.ext) endfunction function! nuwiki#commands#open_diary_path(day_offset) abort let l:c = s:wiki_cfg(0) let l:date = strftime('%Y-%m-%d', localtime() + a:day_offset * 86400) execute 'edit ' . fnameescape(l:c.root . '/' . l:c.diary_rel \ . '/' . l:date . l:c.ext) endfunction function! nuwiki#commands#open_diary_index_path() abort let l:c = s:wiki_cfg(0) execute 'edit ' . fnameescape(l:c.root . '/' . l:c.diary_rel \ . '/' . l:c.diary_idx . l:c.ext) endfunction " ===== List + heading editing ===== function! s:exec_pos(cmd) abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] call s:exec(a:cmd, l:args) endfunction function! nuwiki#commands#toggle_list_item() abort call s:exec_pos('nuwiki.list.toggleCheckbox') endfunction function! nuwiki#commands#cycle_list_item() abort call s:exec_pos('nuwiki.list.cycleCheckbox') endfunction " `glp` cycles backward — same command, `reverse` flag in the payload. function! nuwiki#commands#cycle_list_item_back() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], \ 'position': l:p['position'], 'reverse': v:true }] call s:exec('nuwiki.list.cycleCheckbox', l:args) endfunction function! nuwiki#commands#reject_list_item() abort call s:exec_pos('nuwiki.list.rejectCheckbox') endfunction function! nuwiki#commands#heading_add() abort call s:exec_pos('nuwiki.heading.addLevel') endfunction function! nuwiki#commands#heading_remove() abort call s:exec_pos('nuwiki.heading.removeLevel') endfunction function! nuwiki#commands#next_task() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] call s:exec('nuwiki.list.nextTask', l:args, \ {n -> s:goto_location(n)}) endfunction function! s:goto_location(notification) abort let l:loc = get(get(a:notification, 'response', {}), 'result', {}) if type(l:loc) != type({}) || !has_key(l:loc, 'range') return endif let l:line = l:loc['range']['start']['line'] + 1 let l:col = l:loc['range']['start']['character'] + 1 call cursor(l:line, l:col) endfunction " ===== Generation + workspace ===== function! nuwiki#commands#toc_generate() abort call s:exec('nuwiki.toc.generate', [{'uri': s:buf_uri()}]) endfunction function! nuwiki#commands#links_generate() abort call s:exec('nuwiki.links.generate', [{'uri': s:buf_uri()}]) endfunction function! nuwiki#commands#check_links() abort call s:exec('nuwiki.workspace.checkLinks', [{ 'uri': s:buf_uri() }], \ {n -> s:results_to_qf(n, 'broken links')}) endfunction function! nuwiki#commands#find_orphans() abort call s:exec('nuwiki.workspace.findOrphans', [{ 'uri': s:buf_uri() }], \ {n -> s:results_to_qf(n, 'orphans')}) endfunction function! s:results_to_qf(notification, title) abort let l:rows = get(get(a:notification, 'response', {}), 'result', []) if type(l:rows) != type([]) || empty(l:rows) echom 'nuwiki: no ' . a:title return endif let l:items = [] for l:row in l:rows let l:path = substitute(get(l:row, 'uri', ''), '^file://', '', '') let l:lnum = 1 let l:col = 1 if has_key(l:row, 'range') let l:lnum = l:row['range']['start']['line'] + 1 let l:col = l:row['range']['start']['character'] + 1 endif let l:text = get(l:row, 'message', get(l:row, 'name', '')) call add(l:items, { 'filename': l:path, 'lnum': l:lnum, 'col': l:col, 'text': l:text }) endfor call setqflist([], ' ', { 'title': 'nuwiki ' . a:title, 'items': l:items }) copen endfunction " ===== Tags ===== function! nuwiki#commands#tags_search(query) abort let l:q = a:query if l:q ==# '' let l:q = input('Search tags: ') endif call s:exec('nuwiki.tags.search', [{ 'uri': s:buf_uri(), 'query': l:q }], \ {n -> s:results_to_qf(n, 'tag matches')}) endfunction function! nuwiki#commands#tags_generate_links(tag) abort let l:args = { 'uri': s:buf_uri() } if a:tag !=# '' let l:args['tag'] = a:tag endif call s:exec('nuwiki.tags.generateLinks', [l:args]) endfunction function! nuwiki#commands#tags_rebuild() abort call s:exec('nuwiki.tags.rebuild', [{ 'uri': s:buf_uri() }]) endfunction " ===== HTML export ===== function! nuwiki#commands#export_current() abort call s:exec('nuwiki.export.currentToHtml', [{'uri': s:buf_uri()}]) endfunction function! nuwiki#commands#export_all() abort call s:exec('nuwiki.export.allToHtml', [{'uri': s:buf_uri()}]) endfunction function! nuwiki#commands#export_all_force() abort call s:exec('nuwiki.export.allToHtmlForce', [{'uri': s:buf_uri()}]) endfunction function! nuwiki#commands#export_rss() abort call s:exec('nuwiki.export.rss', [{'uri': s:buf_uri()}]) endfunction function! nuwiki#commands#export_browse() abort call s:exec('nuwiki.export.browse', [{ 'uri': s:buf_uri() }], \ {n -> s:open_browser(n)}) endfunction function! s:open_browser(notification) abort let l:result = get(get(a:notification, 'response', {}), 'result', {}) let l:url = get(l:result, 'browse', '') if l:url ==# '' return endif if has('mac') silent execute '!open ' . shellescape(l:url, 1) . ' &' elseif has('unix') silent execute '!xdg-open ' . shellescape(l:url, 1) . ' &' else echom 'nuwiki: exported → ' . l:url return endif " A silent `:!` shells out via the alternate screen and returns without " repainting, leaving the editor UI garbled until the next redraw. Force a " full repaint so the browser launch doesn't leave visual artifacts behind. redraw! endfunction " ===== File ops ===== function! nuwiki#commands#delete_file() abort if input('Delete current page? [y/N] ') !~? '^y' return endif call s:exec('nuwiki.file.delete', [{ 'uri': s:buf_uri() }]) endfunction function! nuwiki#commands#rename_file() abort if exists(':LspRename') == 2 LspRename elseif s:has_coc() call CocActionAsync('rename') else echohl WarningMsg echom 'nuwiki: no LSP client available for rename' echohl None endif endfunction " ===== List, table & clipboard commands ===== " Cluster A — list rewriters. " `gl` — remove done items from the current list only. Passing the " cursor position scopes the server to the contiguous list block under it. function! nuwiki#commands#list_remove_done() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] call s:exec('nuwiki.list.removeDone', l:args) endfunction " `gL` — remove done items across the whole buffer (no position). function! nuwiki#commands#list_remove_done_all() abort let l:args = [{ 'uri': s:buf_uri() }] call s:exec('nuwiki.list.removeDone', l:args) endfunction " `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only. function! nuwiki#commands#list_remove_checkbox() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] call s:exec('nuwiki.list.removeCheckbox', l:args) endfunction " `:VimwikiRemoveCBInList` — strip checkboxes from every item in the list. function! nuwiki#commands#list_remove_checkbox_in_list() abort let l:p = s:cursor_position() let l:args = [{ \ 'uri': l:p['textDocument']['uri'], \ 'position': l:p['position'], \ 'whole_list': v:true, \ }] call s:exec('nuwiki.list.removeCheckbox', l:args) endfunction function! nuwiki#commands#list_renumber() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] call s:exec('nuwiki.list.renumber', l:args) endfunction function! nuwiki#commands#list_renumber_all() abort let l:p = s:cursor_position() let l:args = [{ \ 'uri': l:p['textDocument']['uri'], \ 'position': l:p['position'], \ 'whole_file': v:true, \ }] call s:exec('nuwiki.list.renumber', l:args) endfunction function! nuwiki#commands#list_change_symbol(symbol, whole_list) abort let l:p = s:cursor_position() let l:args = [{ \ 'uri': l:p['textDocument']['uri'], \ 'position': l:p['position'], \ 'symbol': a:symbol, \ 'whole_list': a:whole_list ? v:true : v:false, \ }] call s:exec('nuwiki.list.changeSymbol', l:args) endfunction function! nuwiki#commands#list_change_level(delta, whole_subtree) abort let l:p = s:cursor_position() let l:args = [{ \ 'uri': l:p['textDocument']['uri'], \ 'position': l:p['position'], \ 'delta': a:delta, \ 'whole_subtree': a:whole_subtree ? v:true : v:false, \ }] call s:exec('nuwiki.list.changeLevel', l:args) endfunction " `:VimwikiListChangeLvl decrease|increase 0` compat entry. function! nuwiki#commands#list_change_lvl(...) abort let l:dir = a:0 >= 1 ? a:1 : 'increase' let l:delta = (l:dir ==# 'increase' || l:dir ==# 'indent') ? 1 : -1 call nuwiki#commands#list_change_level(l:delta, 0) endfunction " Cycle the list marker through vimwiki's canonical order. let s:symbol_order = ['-', '*', '#', '1.', '1)', 'a)', 'A)', 'i)', 'I)'] function! s:detect_symbol() abort let l:line = getline('.') let l:m = matchlist(l:line, '^\s*\([-*#]\)\s') if !empty(l:m) | return l:m[1] | endif let l:m = matchlist(l:line, '^\s*\d\+\([.)]\)\s') if !empty(l:m) | return '1' . l:m[1] | endif if l:line =~# '^\s*[ivxlcdm]\+)\s' | return 'i)' | endif if l:line =~# '^\s*[IVXLCDM]\+)\s' | return 'I)' | endif if l:line =~# '^\s*[a-z]\+)\s' | return 'a)' | endif if l:line =~# '^\s*[A-Z]\+)\s' | return 'A)' | endif return '' endfunction function! nuwiki#commands#list_cycle_symbol(direction) abort let l:cur = s:detect_symbol() if l:cur ==# '' | return | endif let l:idx = index(s:symbol_order, l:cur) if l:idx < 0 | return | endif let l:n = (l:idx + a:direction) % len(s:symbol_order) if l:n < 0 | let l:n += len(s:symbol_order) | endif call nuwiki#commands#list_change_symbol(s:symbol_order[l:n], 0) endfunction function! nuwiki#commands#list_toggle_or_add_checkbox() abort let l:line = getline('.') if l:line =~# '^\s*[-*#]\s\+\[[ xXoO.\-]\]' || l:line =~# '^\s*\w\+[.)]\s\+\[[ xXoO.\-]\]' call nuwiki#commands#toggle_list_item() return endif let l:end = matchend(l:line, '^\s*[-*#] ') if l:end < 0 let l:end = matchend(l:line, '^\s*\w\+[.)] ') endif if l:end < 0 | return | endif let l:new = strpart(l:line, 0, l:end) . '[ ] ' . strpart(l:line, l:end) call setline('.', l:new) endfunction " 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 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 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 " `:VimwikiColorize {color}` — pure-VimL wrap as inline span tag, " matching vimwiki's `%s` template. function! nuwiki#commands#colorize(...) abort let l:color = a:0 >= 1 ? a:1 : '' if l:color ==# '' let l:color = input('Colour: ') endif if l:color ==# '' return endif let l:open_tag = '' let l:close_tag = '' let l:line = getline('.') let l:col = col('.') " Word boundaries around cursor (1-based). let l:left = l:col while l:left > 1 && strpart(l:line, l:left - 2, 1) =~# '[A-Za-z0-9_-]' let l:left -= 1 endwhile let l:right = l:col while l:right <= strlen(l:line) && strpart(l:line, l:right - 1, 1) =~# '[A-Za-z0-9_-]' let l:right += 1 endwhile let l:word = strpart(l:line, l:left - 1, l:right - l:left) if l:word ==# '' return endif let l:new = strpart(l:line, 0, l:left - 1) \ . l:open_tag . l:word . l:close_tag \ . strpart(l:line, l:right - 1) call setline('.', l:new) endfunction " Cluster C — link helpers. function! nuwiki#commands#paste_link() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] call s:exec('nuwiki.link.pasteWikilink', l:args) endfunction function! nuwiki#commands#paste_url() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] 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 `` from the `` mappings. We " can't mutate the buffer from inside the `` body (textlock), so " the mappings hand off here via `:call …`, 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 for insert mode — vimwiki `:VimwikiReturn` parity. " Used as `inoremap nuwiki#commands#smart_return()`. The " returned string is fed into Vim's input stream after the function " finishes, keeping the undo block coherent. function! nuwiki#commands#smart_return() abort let l:line = getline('.') " `` runs under textlock, so the function must be side-effect- " free on the buffer. Buffer-mutating branches return a `:call " …` keystroke that hands off to a regular function. if l:line =~# '^\s*|.*|\s*$' let l:indent = matchstr(l:line, '^\s*') let l:cursor_col = len(l:indent) + 2 return "\call nuwiki#commands#_table_insert_row_below(" . line('.') . ", " . l:cursor_col . ")\" endif let l:m = matchlist(l:line, '^\(\s*\)\([-*#]\)\s\(.*\)$') if empty(l:m) let l:m2 = matchlist(l:line, '^\(\s*\)\(\d\+\)\([.)]\)\s\(.*\)$') if !empty(l:m2) let l:m = [l:line, l:m2[1], l:m2[2] . l:m2[3], l:m2[4]] endif endif if !empty(l:m) let l:indent = l:m[1] let l:marker = l:m[2] let l:rest = l:m[3] let l:cb = matchlist(l:rest, '^\(\[[ xXoO.\-]\]\)\s*\(.*\)$') let l:has_cb = !empty(l:cb) let l:body = l:has_cb ? l:cb[2] : l:rest if l:body =~# '^\s*$' " Empty item — drop to normal, clear the line, re-enter insert, " newline. Indent gets dropped (matches vimwiki's break-out). return "\0DA\" endif " If any auto-indent mechanism is active (autoindent, smartindent, " cindent, or indentexpr), Vim already inserts the previous line's " indent after ``. Skipping our own indent in that case avoids " doubling it and pushing the new bullet one level deeper. let l:auto = &autoindent || &smartindent || &cindent || !empty(&indentexpr) let l:cont = (l:auto ? '' : l:indent) . l:marker . ' ' if l:has_cb let l:cont .= '[ ] ' endif return "\" . l:cont endif return "\" endfunction " Insert-mode `` / `` table navigation — vimwiki parity. " Used as `` mappings so we can pass `` / `` through " verbatim when the cursor isn't on a table row. function! nuwiki#commands#smart_tab() abort let l:line = getline('.') if !s:is_table_row(l:line) return "\" endif let l:starts = s:table_cell_starts(l:line) if len(l:starts) < 2 return "\" endif let l:col = col('.') - 1 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 let l:target_idx = l:current_idx + 1 if l:target_idx < len(l:starts) return "\call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\" endif " Past the final cell. Prefer cycling into the next existing row (skipping " any separator row); only insert a fresh row when we're already on the " table's last row. let l:row = line('.') let [l:s_row, l:e_row] = s:find_table_range(l:row) let l:next = l:row + 1 while l:next <= l:e_row let [l:_, l:next_cells] = s:parse_table_row(getline(l:next)) if !s:is_sep_row(l:next_cells) return "\call nuwiki#commands#_table_jump_to_cell(" . l:next . ", 1)\" endif let l:next += 1 endwhile let l:indent = matchstr(l:line, '^\s*') let l:cursor_col = len(l:indent) + 2 return "\call nuwiki#commands#_table_insert_row_below(" . l:row . ", " . l:cursor_col . ")\" endfunction function! nuwiki#commands#smart_shift_tab() abort let l:line = getline('.') if !s:is_table_row(l:line) return "\" endif let l:starts = s:table_cell_starts(l:line) if len(l:starts) < 2 return "\" 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 "\" endif let l:target_idx = l:current_idx - 1 return "\call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\" endfunction " `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]` " without following. Pure-VimL. function! nuwiki#commands#normalize_link() abort if s:cursor_inside_wikilink() return endif call s:wrap_cword_as_wikilink() endfunction function! s:echo_url_from(response, ...) abort let l:result = get(get(a:response, 'response', {}), 'result', '') if type(l:result) == type('') && l:result !=# '' echom l:result endif endfunction " `:VimwikiCatUrl` — echo the `file://` URL of the current page's HTML " export (the server computes the path from the wiki's html config). function! nuwiki#commands#cat_url() abort let l:args = [{ 'uri': s:buf_uri() }] call s:exec('nuwiki.link.catUrl', l:args, function('s:echo_url_from')) endfunction " ===== Name-parity aliases ===== " " The Lua surface and the VimL surface diverged on a handful of names: " " Lua → VimL " heading_add_level heading_add " heading_remove_level heading_remove " table_move_column_left table_move_left " table_move_column_right table_move_right " " Real callers always go through the keymaps / `:Vimwiki*` commands " defined in `ftplugin/vimwiki.vim`, which already route correctly. " These aliases exist so the *public* `nuwiki#commands#*` surface " matches Lua's `require('nuwiki.commands').*` 1:1 for any user who " calls them by name from their own config. function! nuwiki#commands#heading_add_level() abort call nuwiki#commands#heading_add() endfunction function! nuwiki#commands#heading_remove_level() abort call nuwiki#commands#heading_remove() endfunction function! nuwiki#commands#table_move_column_left() abort call nuwiki#commands#table_move_left() endfunction function! nuwiki#commands#table_move_column_right() abort call nuwiki#commands#table_move_right() endfunction