From 04df170791606e56598e2000d4665b0a59ca1b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Mon, 11 May 2026 22:23:06 +0000 Subject: [PATCH] feat(vim): :Vimwiki* / :Nuwiki* commands for plain Vim via vim-lsp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- autoload/nuwiki/commands.vim | 250 +++++++++++++++++++++++++++++++++++ ftplugin/vimwiki.vim | 85 +++++++++++- 2 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 autoload/nuwiki/commands.vim diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim new file mode 100644 index 0000000..be2ebc5 --- /dev/null +++ b/autoload/nuwiki/commands.vim @@ -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 diff --git a/ftplugin/vimwiki.vim b/ftplugin/vimwiki.vim index d8d514d..fd36736 100644 --- a/ftplugin/vimwiki.vim +++ b/ftplugin/vimwiki.vim @@ -22,9 +22,88 @@ setlocal iskeyword+=- let b:undo_ftplugin = 'setlocal commentstring< comments< formatoptions< suffixesadd< iskeyword<' if !has('nvim') - " Plain Vim — the buffer-local options above are all we ship. Vim users - " interact via the LSP client (vim-lsp / coc) which already exposes - " `:LspDefinition`, `:LspReferences`, `:LspRename`, etc. + " ===== Plain Vim path ===== + " + " 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() + command! -buffer -count VimwikiTabIndex call nuwiki#commands#wiki_tab_index() + 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 // ** + command! -buffer -nargs=1 VWS lvimgrep // ** + command! -buffer -nargs=1 VimwikiGoto call nuwiki#commands#wiki_goto_page() + + 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 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() + command! -buffer -nargs=? VimwikiGenerateTagLinks call nuwiki#commands#tags_generate_links() + + " §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() + 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() + command! -buffer NuwikiTabIndex call nuwiki#commands#wiki_tab_index() + 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 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() + command! -buffer -nargs=? NuwikiGenerateTagLinks call nuwiki#commands#tags_generate_links() + command! -buffer -nargs=1 NuwikiGoto call nuwiki#commands#wiki_goto_page() + finish endif