feat(vim): :Vimwiki* / :Nuwiki* commands for plain Vim via vim-lsp
CI / cargo fmt --check (push) Successful in 30s
CI / cargo clippy (push) Successful in 1m16s
CI / cargo test (push) Successful in 1m27s

The Phase 19 ftplugin only registered the command surface on Neovim
and finished early for plain Vim, leaving `start-vim.sh` users with
only commentstring + suffixesadd — exactly the "no commands" report.

`autoload/nuwiki/commands.vim` now ships the Vim-side dispatch:
- `s:exec` sends `workspace/executeCommand` through vim-lsp's
  `lsp#send_request`. Optional callback for commands that return data.
- `s:open_uri_from` reads `{ uri }` out of the LSP response and runs
  `:edit` (or `:tabedit` for the tab variants).
- `s:results_to_qf` lifts `checkLinks` / `findOrphans` / `tags.search`
  arrays into the quickfix list and opens `:copen` — same UX as the
  Neovim path.
- `s:open_browser` fires `open` / `xdg-open` after `export.browse`.
- §13.1 deferred commands stub out with a "not yet implemented"
  notification so users get the same signal as on Neovim.

`ftplugin/vimwiki.vim` defines the same `:Vimwiki*` / `:Nuwiki*`
command set on the plain-Vim path, each delegating to its
`nuwiki#commands#…` autoload counterpart. `:VimwikiFollowLink` /
`:VimwikiBacklinks` map straight to vim-lsp's `:LspDefinition` /
`:LspReferences`. coc.nvim users can still use `:CocCommand` directly
and ignore the aliases entirely.

Verified with `vim -e -s -u .vimrc index.wiki`:
  filetype=vimwiki, did_ftplugin=1,
  :VimwikiTOC, :NuwikiIndex, :VimwikiToggleListItem all defined.

Total 377 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:23:06 +00:00
parent 09b9bd98a3
commit 04df170791
2 changed files with 332 additions and 3 deletions
+250
View File
@@ -0,0 +1,250 @@
" autoload/nuwiki/commands.vim — Vim-side wrappers around the LSP
" commands the server advertises (Phase 19).
"
" 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:exec(command, arguments, ...) abort
if !s:has_vimlsp()
echohl ErrorMsg
echom 'nuwiki: vim-lsp not loaded; cannot run ' . a:command
echohl None
return
endif
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)
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
function! s:notify_deferred(name) abort
echohl WarningMsg
echom 'nuwiki: ' . a:name . ' is not yet implemented — see SPEC §13.1'
echohl None
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
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
call s:exec(a:cmd_name, [{ 'uri': s:buf_uri() }],
\ function('s:open_uri_from'))
endfunction
function! nuwiki#commands#diary_today() abort | call s:diary_open('nuwiki.diary.openToday') | 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
" ===== 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
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
endif
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('*lsp#rename')
LspRename
else
echohl WarningMsg
echom 'nuwiki: :LspRename unavailable; install vim-lsp'
echohl None
endif
endfunction
" ===== §13.1 deferred =====
function! nuwiki#commands#list_change_lvl() abort | call s:notify_deferred(':VimwikiListChangeLvl') | endfunction
function! nuwiki#commands#list_remove_done() abort | call s:notify_deferred(':VimwikiRemoveDone') | endfunction
function! nuwiki#commands#table_insert() abort | call s:notify_deferred(':VimwikiTable') | endfunction
function! nuwiki#commands#table_move_left() abort | call s:notify_deferred(':VimwikiTableMoveColumnLeft') | endfunction
function! nuwiki#commands#table_move_right() abort | call s:notify_deferred(':VimwikiTableMoveColumnRight') | endfunction
function! nuwiki#commands#colorize(color) abort | call s:notify_deferred(':VimwikiColorize') | endfunction
function! nuwiki#commands#paste_link() abort | call s:notify_deferred(':VimwikiPasteLink') | endfunction
function! nuwiki#commands#paste_url() abort | call s:notify_deferred(':VimwikiPasteUrl') | endfunction
+82 -3
View File
@@ -22,9 +22,88 @@ setlocal iskeyword+=-
let b:undo_ftplugin = 'setlocal commentstring< comments< formatoptions< suffixesadd< iskeyword<' let b:undo_ftplugin = 'setlocal commentstring< comments< formatoptions< suffixesadd< iskeyword<'
if !has('nvim') if !has('nvim')
" Plain Vim — the buffer-local options above are all we ship. Vim users " ===== Plain Vim path =====
" interact via the LSP client (vim-lsp / coc) which already exposes "
" `:LspDefinition`, `:LspReferences`, `:LspRename`, etc. " Define the same `:Vimwiki*` / `:Nuwiki*` surface here, dispatching
" through `autoload/nuwiki/commands.vim` (which talks to vim-lsp).
" coc.nvim users can run `:CocCommand nuwiki.x` directly and skip
" these aliases.
command! -buffer -count VimwikiIndex call nuwiki#commands#wiki_index(<count>)
command! -buffer -count VimwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
command! -buffer VimwikiDiaryIndex call nuwiki#commands#diary_index()
command! -buffer VimwikiMakeDiaryNote call nuwiki#commands#diary_today()
command! -buffer VimwikiMakeYesterdayDiaryNote call nuwiki#commands#diary_yesterday()
command! -buffer VimwikiMakeTomorrowDiaryNote call nuwiki#commands#diary_tomorrow()
command! -buffer VimwikiDiaryNextDay call nuwiki#commands#diary_next()
command! -buffer VimwikiDiaryPrevDay call nuwiki#commands#diary_prev()
command! -buffer VimwikiDiaryGenerateLinks call nuwiki#commands#diary_generate_index()
command! -buffer VimwikiFollowLink LspDefinition
command! -buffer VimwikiBacklinks LspReferences
command! -buffer VWB LspReferences
command! -buffer -nargs=1 VimwikiSearch lvimgrep /<args>/ **
command! -buffer -nargs=1 VWS lvimgrep /<args>/ **
command! -buffer -nargs=1 VimwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
command! -buffer VimwikiDeleteFile call nuwiki#commands#delete_file()
command! -buffer VimwikiRenameFile call nuwiki#commands#rename_file()
command! -buffer VimwikiNextTask call nuwiki#commands#next_task()
command! -buffer VimwikiToggleListItem call nuwiki#commands#toggle_list_item()
command! -buffer VimwikiToggleRejectedListItem call nuwiki#commands#reject_list_item()
command! -buffer VimwikiRemoveDone call nuwiki#commands#list_remove_done()
command! -buffer -nargs=? VimwikiListChangeLvl call nuwiki#commands#list_change_lvl()
command! -buffer Vimwiki2HTML call nuwiki#commands#export_current()
command! -buffer Vimwiki2HTMLBrowse call nuwiki#commands#export_browse()
command! -buffer -bang VimwikiAll2HTML if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
command! -buffer VimwikiRss call nuwiki#commands#export_rss()
command! -buffer VimwikiTOC call nuwiki#commands#toc_generate()
command! -buffer VimwikiGenerateLinks call nuwiki#commands#links_generate()
command! -buffer VimwikiCheckLinks call nuwiki#commands#check_links()
command! -buffer VimwikiFindOrphans call nuwiki#commands#find_orphans()
command! -buffer VimwikiRebuildTags call nuwiki#commands#tags_rebuild()
command! -buffer -nargs=? VimwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
command! -buffer -nargs=? VimwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
" §13.1 deferred stubs.
command! -buffer -nargs=1 VimwikiTable call nuwiki#commands#table_insert()
command! -buffer VimwikiTableMoveColumnLeft call nuwiki#commands#table_move_left()
command! -buffer VimwikiTableMoveColumnRight call nuwiki#commands#table_move_right()
command! -buffer -nargs=1 VimwikiColorize call nuwiki#commands#colorize(<q-args>)
command! -buffer VimwikiPasteLink call nuwiki#commands#paste_link()
command! -buffer VimwikiPasteUrl call nuwiki#commands#paste_url()
" :Nuwiki* canonical aliases (P10) — Vim side.
command! -buffer -count NuwikiIndex call nuwiki#commands#wiki_index(<count>)
command! -buffer NuwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
command! -buffer NuwikiDiaryIndex call nuwiki#commands#diary_index()
command! -buffer NuwikiDiaryToday call nuwiki#commands#diary_today()
command! -buffer NuwikiDiaryYesterday call nuwiki#commands#diary_yesterday()
command! -buffer NuwikiDiaryTomorrow call nuwiki#commands#diary_tomorrow()
command! -buffer NuwikiDiaryNext call nuwiki#commands#diary_next()
command! -buffer NuwikiDiaryPrev call nuwiki#commands#diary_prev()
command! -buffer NuwikiFollowLink LspDefinition
command! -buffer NuwikiBacklinks LspReferences
command! -buffer NuwikiDeleteFile call nuwiki#commands#delete_file()
command! -buffer NuwikiRenameFile call nuwiki#commands#rename_file()
command! -buffer NuwikiNextTask call nuwiki#commands#next_task()
command! -buffer NuwikiToggleListItem call nuwiki#commands#toggle_list_item()
command! -buffer NuwikiToggleRejected call nuwiki#commands#reject_list_item()
command! -buffer NuwikiExport call nuwiki#commands#export_current()
command! -buffer NuwikiExportBrowse call nuwiki#commands#export_browse()
command! -buffer -bang NuwikiExportAll if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
command! -buffer NuwikiRss call nuwiki#commands#export_rss()
command! -buffer NuwikiTOC call nuwiki#commands#toc_generate()
command! -buffer NuwikiGenerateLinks call nuwiki#commands#links_generate()
command! -buffer NuwikiCheckLinks call nuwiki#commands#check_links()
command! -buffer NuwikiFindOrphans call nuwiki#commands#find_orphans()
command! -buffer NuwikiRebuildTags call nuwiki#commands#tags_rebuild()
command! -buffer -nargs=? NuwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
command! -buffer -nargs=? NuwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
command! -buffer -nargs=1 NuwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
finish finish
endif endif