fix(follow-link): resolve+open definitions ourselves on the Vim clients
The Vim follow-link path delegated to the LSP client's jump UI
(:LspDefinition / coc jumpDefinition), which broke two ways for wiki
link-following:
1. **No create-on-follow (vim-lsp + coc).** vim-lsp's location handler
readfile()s the target to build a quickfix entry; for a not-yet-created
page that throws E484 and aborts the jump, so <CR> on [[NewPage]] did
nothing. The server was correct throughout — textDocument/definition
returns a *synthesised* location for missing pages (verified over
JSON-RPC).
2. **Wrong window placement (coc).** Without an explicit open command coc
fell back to coc.preferences.jumpCommand (e.g. a tab command).
Stop delegating: nuwiki#commands#follow_link_or_create() /
follow_link_drop() now resolve textDocument/definition directly and open
the URI themselves via a shared s:open_definition(open_cmd) helper — :edit
for <CR>, `tab drop` for <C-S-CR>. :edit opens a buffer for a missing path
(save creates it, matching vimwiki) and gives exact current-window
placement regardless of the user's coc jumpCommand. Removes
s:jump_definition / s:drop_from_response.
Also fix the coc action name: CocAction('getDefinition') does not exist
(E605 "Action getDefinition does not exist") — the registered action is
'definitions'. Fixed in s:open_definition and badd_link.
coc config delivery: the setup guidance shipped without
initializationOptions, so coc users never sent wiki_root and the server
resolved links against its default root (existing pages flagged broken,
follow-to-create landed in the cwd). The printed snippet
(autoload/nuwiki/lsp.vim) now emits initializationOptions populated with
the resolved settings, and the README coc snippet documents it as required.
Add development/start-vim-coc.sh — a coc.nvim dev launcher (sibling of
start-vim.sh) that wires coc's prebuilt release branch + a generated
coc-settings.json (with initializationOptions), deliberately omitting
vim-lsp so the coc path is exercised; doubles as a reproducer.
Tests: new cr.follow_opens_missing_page_in_current_window case in
test-keymaps-vim.vim (stubs CocAction to a missing page, asserts
current-window :edit). Keymap suite 254+18 green; config-parity (Vim +
Neovim) green. Neovim path unchanged and re-confirmed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,16 +69,66 @@ function! s:exec(command, arguments, ...) abort
|
||||
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
|
||||
" Resolve the wikilink under the cursor via textDocument/definition and open
|
||||
" the target with `a:open_cmd` ('edit', 'split', 'vsplit', or 'tab drop').
|
||||
"
|
||||
" We resolve + open the URI ourselves rather than delegating to
|
||||
" `:LspDefinition` / coc's `jumpDefinition`. Those drive a jump UI that first
|
||||
" fetches the target's text — vim-lsp `readfile()`s it for the quickfix entry
|
||||
" (autoload/lsp/utils/location.vim), which throws E484 for a page that does
|
||||
" not exist yet and aborts the whole jump. That silently broke "follow link
|
||||
" to create the page". `:edit` on a missing path just opens an empty buffer
|
||||
" (saving creates the file), matching vimwiki, and gives us exact control over
|
||||
" window placement regardless of the user's coc.preferences.jumpCommand.
|
||||
function! s:open_definition(open_cmd) abort
|
||||
if s:has_vimlsp()
|
||||
call lsp#send_request(s:server, {
|
||||
\ 'method': 'textDocument/definition',
|
||||
\ 'params': s:cursor_position(),
|
||||
\ 'on_notification': function('s:open_from_response', [a:open_cmd]),
|
||||
\ })
|
||||
return
|
||||
endif
|
||||
if s:has_coc()
|
||||
let l:locs = CocAction('definitions')
|
||||
if type(l:locs) == type([]) && !empty(l:locs)
|
||||
call s:open_location(a:open_cmd,
|
||||
\ get(l:locs[0], 'uri', get(l:locs[0], 'targetUri', '')),
|
||||
\ get(l:locs[0], 'range', get(l:locs[0], 'targetSelectionRange', {})))
|
||||
endif
|
||||
return
|
||||
endif
|
||||
echohl WarningMsg
|
||||
echom 'nuwiki: no LSP client available for jump-to-definition'
|
||||
echohl None
|
||||
endfunction
|
||||
|
||||
" vim-lsp on_notification callback: pull the URI + range out of the
|
||||
" textDocument/definition response (Location or Location[]) and open it.
|
||||
function! s:open_from_response(open_cmd, notification) abort
|
||||
let l:result = get(get(a:notification, 'response', {}), 'result', {})
|
||||
let l:uri = ''
|
||||
let l:range = {}
|
||||
if type(l:result) == type({})
|
||||
let l:uri = get(l:result, 'uri', get(l:result, 'targetUri', ''))
|
||||
let l:range = get(l:result, 'range', get(l:result, 'targetSelectionRange', {}))
|
||||
elseif type(l:result) == type([]) && !empty(l:result)
|
||||
let l:uri = get(l:result[0], 'uri', get(l:result[0], 'targetUri', ''))
|
||||
let l:range = get(l:result[0], 'range', get(l:result[0], 'targetSelectionRange', {}))
|
||||
endif
|
||||
call s:open_location(a:open_cmd, l:uri, l:range)
|
||||
endfunction
|
||||
|
||||
" Open a resolved definition URI with `a:open_cmd`, then place the cursor at
|
||||
" the response range start (when present).
|
||||
function! s:open_location(open_cmd, uri, range) abort
|
||||
if type(a:uri) != type('') || a:uri ==# ''
|
||||
return
|
||||
endif
|
||||
let l:path = substitute(a:uri, '^file://', '', '')
|
||||
execute a:open_cmd . ' ' . fnameescape(l:path)
|
||||
if type(a:range) == type({}) && has_key(a:range, 'start')
|
||||
call cursor(a:range.start.line + 1, a:range.start.character + 1)
|
||||
endif
|
||||
endfunction
|
||||
|
||||
@@ -158,7 +208,7 @@ function! nuwiki#commands#badd_link() abort
|
||||
return
|
||||
endif
|
||||
if s:has_coc()
|
||||
let l:locs = CocAction('getDefinition')
|
||||
let l:locs = CocAction('definitions')
|
||||
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)
|
||||
@@ -189,44 +239,9 @@ function! s:badd_from_response(notification) abort
|
||||
endfunction
|
||||
|
||||
" `:VimwikiTabDropLink` — open the link target in a tab, reusing an
|
||||
" existing tab/window if the file is already shown (`:tab drop`). Routes
|
||||
" through vim-lsp's textDocument/definition like `badd_link`.
|
||||
" existing tab/window if the file is already shown (`:tab drop`).
|
||||
function! nuwiki#commands#follow_link_drop() abort
|
||||
if s:has_vimlsp()
|
||||
call lsp#send_request(s:server, {
|
||||
\ 'method': 'textDocument/definition',
|
||||
\ 'params': s:cursor_position(),
|
||||
\ 'on_notification': function('s:drop_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 'tab drop ' . fnameescape(l:path)
|
||||
endif
|
||||
endif
|
||||
return
|
||||
endif
|
||||
echohl ErrorMsg | echom 'nuwiki: no supported LSP client for follow_link_drop' | echohl None
|
||||
endfunction
|
||||
|
||||
function! s:drop_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 'tab drop ' . fnameescape(l:path)
|
||||
call s:open_definition('tab drop')
|
||||
endfunction
|
||||
|
||||
" ===== Smart <CR>: follow or wrap-then-follow =====
|
||||
@@ -285,7 +300,7 @@ endfunction
|
||||
" 3. Press inside an existing `[[…]]` follows immediately.
|
||||
function! nuwiki#commands#follow_link_or_create() abort
|
||||
if s:cursor_inside_wikilink()
|
||||
call s:jump_definition()
|
||||
call s:open_definition('edit')
|
||||
return
|
||||
endif
|
||||
if s:wrap_cword_as_wikilink()
|
||||
@@ -293,7 +308,7 @@ function! nuwiki#commands#follow_link_or_create() abort
|
||||
return
|
||||
endif
|
||||
" Nothing to wrap — fall through to plain definition.
|
||||
call s:jump_definition()
|
||||
call s:open_definition('edit')
|
||||
endfunction
|
||||
|
||||
" Pure-VimL bullet-continuation for `o` / `O` — mirrors the Lua side
|
||||
|
||||
@@ -47,13 +47,17 @@ function! nuwiki#lsp#start() abort
|
||||
if exists(':CocConfig') == 2 || exists('*coc#config')
|
||||
" coc.nvim — coc reads its server list from coc-settings.json.
|
||||
" We can't inject it dynamically without owning the file, so just point
|
||||
" the user at the snippet.
|
||||
" the user at the snippet. `initializationOptions` carries wiki_root etc.
|
||||
" to the server exactly like the vim-lsp registration above — without it
|
||||
" the server resolves links against its default root, so existing pages
|
||||
" look broken and follow-to-create lands in the wrong directory.
|
||||
echohl WarningMsg
|
||||
echom 'nuwiki: coc.nvim detected. Add this to your coc-settings.json:'
|
||||
echom ' "languageserver": {'
|
||||
echom ' "nuwiki": {'
|
||||
echom ' "command": "' . l:bin . '",'
|
||||
echom ' "filetypes": ["vimwiki"]'
|
||||
echom ' "filetypes": ["vimwiki"],'
|
||||
echom ' "initializationOptions": ' . json_encode(s:settings())
|
||||
echom ' }'
|
||||
echom ' }'
|
||||
echohl None
|
||||
|
||||
Reference in New Issue
Block a user