From 6983daa6375c3eb8bd76b1a77466ab94e8ab07f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Sun, 31 May 2026 15:00:59 +0000 Subject: [PATCH] fix(follow-link): resolve+open definitions ourselves on the Vim clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 , `tab drop` for . :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) --- README.md | 22 ++- autoload/nuwiki/commands.vim | 115 +++++++++------- autoload/nuwiki/lsp.vim | 8 +- development/start-vim-coc.sh | 178 +++++++++++++++++++++++++ development/tests/test-keymaps-vim.vim | 44 ++++++ development/vimwiki-gap.md | 57 ++++---- 6 files changed, 345 insertions(+), 79 deletions(-) create mode 100755 development/start-vim-coc.sh diff --git a/README.md b/README.md index 19392e7..f3a595d 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,12 @@ on your behalf, so add the entry once yourself (open it with "languageserver": { "nuwiki": { "command": "~/.vim/pack/gffranco/start/nuwiki/bin/nuwiki-ls", - "filetypes": ["vimwiki"] + "filetypes": ["vimwiki"], + "initializationOptions": { + "wiki_root": "~/vimwiki", + "file_extension": ".wiki", + "syntax": "vimwiki" + } } } } @@ -163,9 +168,18 @@ on your behalf, so add the entry once yourself (open it with Point `command` at the installed binary — by default it lives in the plugin's `bin/` directory (adjust the path for your plugin manager), or -set `g:nuwiki_binary_path` and reuse that value. With coc you can also -invoke server commands directly via `:CocCommand nuwiki.` -instead of the `:Vimwiki*` / `:Nuwiki*` aliases. +set `g:nuwiki_binary_path` and reuse that value. + +`initializationOptions` is **required** — it carries `wiki_root` (and the +other [config keys](#configuration)) to the server, exactly as the vim-lsp +registration does automatically. Without it the server resolves links +against its default root, so existing pages show up as broken links and +following a link to a new page creates it in the wrong directory. (Run +`:NuwikiInstall`-time guidance, or `:call nuwiki#lsp#start()` once, to have +nuwiki print this snippet with your configured paths already filled in.) +With coc you can also invoke server commands directly via +`:CocCommand nuwiki.` instead of the `:Vimwiki*` / `:Nuwiki*` +aliases. ### Try it without touching your config diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim index 4122acf..881f42d 100644 --- a/autoload/nuwiki/commands.vim +++ b/autoload/nuwiki/commands.vim @@ -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 : 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 diff --git a/autoload/nuwiki/lsp.vim b/autoload/nuwiki/lsp.vim index d56f837..2e6a9f3 100644 --- a/autoload/nuwiki/lsp.vim +++ b/autoload/nuwiki/lsp.vim @@ -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 diff --git a/development/start-vim-coc.sh b/development/start-vim-coc.sh new file mode 100755 index 0000000..5ee04bc --- /dev/null +++ b/development/start-vim-coc.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# +# start-vim-coc.sh — launch Vim with a minimal config that loads nuwiki +# through coc.nvim (instead of vim-lsp). +# +# The plugin's Vim path (autoload/nuwiki/lsp.vim) prefers `vim-lsp` and falls +# back to `coc.nvim`. start-vim.sh exercises the vim-lsp path; this script +# exercises the coc.nvim path. vim-lsp is deliberately kept OFF the runtimepath +# so `:LspDefinition` doesn't exist and nuwiki's coc branch is the one that runs +# (s:jump_definition() → CocActionAsync('jumpDefinition', 'edit')). +# +# This doubles as a reproducer for the " opens a new tab" regression: +# coc-settings.json sets coc.preferences.jumpCommand to `tabe`, so without the +# fix `` on a link opens a new tab. With the fix nuwiki passes 'edit' +# explicitly and the target opens in the *current* window regardless. +# +# It clones coc.nvim's prebuilt `release` branch into the dev cache on first +# run (no yarn/build needed — it just needs Node.js at runtime). +# +# Usage: ./development/start-vim-coc.sh [extra args...] +# ./development/start-vim-coc.sh # open the scratch wiki's index +# ./development/start-vim-coc.sh path/to/note.wiki # open a specific file +# NUWIKI_DEV_WIKI=/tmp/foo ./development/start-vim-coc.sh +# NUWIKI_DEV_NO_SEED=1 ./development/start-vim-coc.sh # use WIKI_DIR as-is (no scratch seeding) +# NUWIKI_DEV_SKIP_BUILD=1 ./development/start-vim-coc.sh # reuse the cached binary +# NUWIKI_DEV_COC_JUMP=edit ./development/start-vim-coc.sh # change coc.preferences.jumpCommand +# +# The release binary is rebuilt on every launch so source changes always reach +# the running LSP. `cargo build --release` is incremental, so this is a fast +# no-op when nothing changed. Set NUWIKI_DEV_SKIP_BUILD=1 to skip the build. +# +# Tested with Vim 9.1+ and Node 18+. State (viminfo/sessions, coc data) lives +# under $XDG_CACHE_HOME/nuwiki-dev/ so your real Vim install stays untouched. + +set -euo pipefail + +# Shared paths + helpers (log, ensure_binary, write_sample_wiki, seed_wiki). +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +VIMRC="$DEV_DIR/vimrc-coc" +VIM_STATE="$DEV_DIR/vim-coc-state" +COC_DIR="$DEV_DIR/coc.nvim" +COC_DATA="$DEV_DIR/coc-data" +COC_SETTINGS="$DEV_DIR/coc-settings.json" +COC_JUMP="${NUWIKI_DEV_COC_JUMP:-tabe}" + +ensure_node() { + if ! command -v node >/dev/null 2>&1; then + log 'node not found — coc.nvim needs Node.js at runtime; skipping' + exit 0 + fi +} + +ensure_coc() { + mkdir -p "$DEV_DIR" + if [[ -d "$COC_DIR/.git" ]]; then + return + fi + if ! command -v git >/dev/null 2>&1; then + log 'git not found — cannot clone coc.nvim; skipping' + exit 0 + fi + log "cloning coc.nvim (release branch) → $COC_DIR" + git clone --depth 1 --branch release \ + https://github.com/neoclide/coc.nvim.git "$COC_DIR" >/dev/null 2>&1 +} + +write_coc_settings() { + mkdir -p "$DEV_DIR" + # Mirror what the vim-lsp client sends (autoload/nuwiki/lsp.vim s:settings()): + # both `initializationOptions` (sent on initialize) and `settings.nuwiki` + # (served via workspace/configuration). Without these the server falls back + # to its own default wiki_root (~/vimwiki) instead of $WIKI_DIR, so it can't + # resolve links (e.g. Notes shows as broken) or create-on-follow correctly. + cat > "$COC_SETTINGS" < "$VIMRC" <