fix(follow-link): create missing page + wrap word on <CR>
CI / cargo fmt --check (push) Successful in 35s
CI / cargo clippy (push) Successful in 1m8s
CI / cargo test (push) Successful in 1m16s

Two missing pieces in the previous "follow creates page" fix:

1. **`wiki_root` never reached the server.** The Lua glue (and the
   Vim autoload glue) was sending the config under `settings` — that
   field is for `workspace/didChangeConfiguration` notifications, not
   for `initialize`. The server's `Config::from_init_params` reads
   `initialization_options`, so `wiki_root` arrived as `None`, the
   `Wiki` aggregate was empty, and `wiki_for_uri` returned `None` →
   `resolve_target_uri` bailed before reaching my synthesise path,
   leaving `<CR>` with "No Location Found".

   Both glues now send the same payload under both keys:
   - `lua/nuwiki/lsp.lua` → adds `init_options = init_options()`
     alongside the existing `settings = …`.
   - `autoload/nuwiki/lsp.vim` → adds `initialization_options` to
     the `lsp#register_server` call.

   The server keeps reading from `initializationOptions` at startup
   *and* honouring `didChangeConfiguration` later (Phase 11 plumbing).

2. **No-wiki fallback in the server.** Even with the glue fix above,
   users who launch nuwiki on an ad-hoc `.wiki` file outside any
   workspace folder would still get an empty `wikis` list. The
   `LinkKind::Wiki` arm now falls back to a new
   `synthesise_page_uri_next_to(source_uri, path)` which builds the
   future page next to the current file. `<CR>` on `[[NewPage]]`
   opens `./NewPage.wiki`, save creates it.

3. **`<CR>` on a plain word now wraps + follows.** Mirrors vimwiki's
   `:VimwikiFollowLink`: when the cursor isn't already inside
   `[[…]]`, the new `nuwiki.commands.follow_link_or_create` /
   `nuwiki#commands#follow_link_or_create` first wraps the word
   under cursor as `[[word]]`, places the cursor inside the link,
   then dispatches `vim.lsp.buf.definition()` /
   `:LspDefinition`. Both editor paths now bind `<CR>` /
   `<S-CR>` / `<C-CR>` / `<C-S-CR>` to this smart variant.

Verified end-to-end:
- `[[NewPage]]` with the rebuilt binary: definition returns
  `file:///tmp/wiki/NewPage.wiki` as a Location, so the editor
  opens an empty buffer for it.
- A bare `MyPage` word becomes `[[MyPage]]` in the buffer before
  the follow request fires.

Total 381 tests still pass; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 01:44:26 +00:00
parent 9d86218b13
commit 347e2f02f7
7 changed files with 200 additions and 17 deletions
+74
View File
@@ -95,6 +95,80 @@ local function uri_args()
return { { uri = buf_uri() } }
end
-- ===== Smart <CR>: follow or create a link =====
--
-- Vimwiki's `:VimwikiFollowLink` is smart: when the cursor sits on a
-- bare word (not inside `[[…]]`), it wraps the word into a wikilink
-- before following — so `<CR>` on `Notes` becomes `<CR>` on
-- `[[Notes]]` and opens a fresh buffer for the page. Match that.
local function cursor_inside_wikilink()
local line = vim.api.nvim_get_current_line()
local col = vim.api.nvim_win_get_cursor(0)[2] + 1 -- 1-based
-- Find the most recent `[[` at or before the cursor, then check
-- that no `]]` closes it before the cursor.
local open_at
local i = 1
while i < #line do
if line:sub(i, i + 1) == '[[' then
open_at = i
i = i + 2
elseif line:sub(i, i + 1) == ']]' then
open_at = nil
i = i + 2
else
i = i + 1
end
if i > col then break end
end
return open_at ~= nil
end
local function wrap_cword_as_wikilink()
local cword = vim.fn.expand('<cword>')
if cword == '' then return false end
-- Replace just this `<cword>` occurrence using `*` (the start of
-- the last cursor match) — `ciw[[<C-r>"]]` style, but we do it
-- via the API so we don't depend on register state.
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local line = vim.api.nvim_get_current_line()
-- Locate the word boundaries around cursor (0-based col).
local left = col
while left > 0 and line:sub(left, left):match('[%w_-]') do
left = left - 1
end
-- `left` now sits one before the word start (or at -1 / col=0 if at start).
local start_col = left
if line:sub(left + 1, left + 1):match('[%w_-]') then
start_col = left
else
start_col = left + 1
end
local right = start_col + 1
while right <= #line and line:sub(right, right):match('[%w_-]') do
right = right + 1
end
local stop_col = right - 1
local word = line:sub(start_col + 1, stop_col)
if word == '' then return false end
local new_line = line:sub(1, start_col) .. '[[' .. word .. ']]' .. line:sub(stop_col + 1)
vim.api.nvim_set_current_line(new_line)
-- Put cursor inside the new `[[…]]` so the LSP definition request
-- lands on the wikilink.
vim.api.nvim_win_set_cursor(0, { row, start_col + 2 })
return true
end
function M.follow_link_or_create()
if not cursor_inside_wikilink() then
if not wrap_cword_as_wikilink() then
-- Not on a word either — fall through to plain definition; the
-- LSP will simply do nothing if there's no target.
end
end
vim.lsp.buf.definition()
end
-- ===== Wiki picker =====
function M.wiki_index(count)
+5 -4
View File
@@ -173,12 +173,13 @@ function M.attach(bufnr, mappings)
-- ===== Links =====
if on('links') then
map('n', '<CR>', vim.lsp.buf.definition, { desc = 'nuwiki: follow link' }, bufnr)
map('n', '<S-CR>', function() vim.cmd('split'); vim.lsp.buf.definition() end,
map('n', '<CR>', cmd.follow_link_or_create,
{ desc = 'nuwiki: follow link (or wrap word + follow)' }, bufnr)
map('n', '<S-CR>', function() vim.cmd('split'); cmd.follow_link_or_create() end,
{ desc = 'nuwiki: follow link in split' }, bufnr)
map('n', '<C-CR>', function() vim.cmd('vsplit'); vim.lsp.buf.definition() end,
map('n', '<C-CR>', function() vim.cmd('vsplit'); cmd.follow_link_or_create() end,
{ desc = 'nuwiki: follow link in vsplit' }, bufnr)
map('n', '<C-S-CR>', function() vim.cmd('tabnew'); vim.lsp.buf.definition() end,
map('n', '<C-S-CR>', function() vim.cmd('tabnew'); cmd.follow_link_or_create() end,
{ desc = 'nuwiki: follow link in new tab' }, bufnr)
map('n', '<BS>', '<C-o>', { desc = 'nuwiki: go back', remap = true }, bufnr)
map('n', '<Tab>', link.next, { desc = 'nuwiki: next wikilink' }, bufnr)
+12
View File
@@ -14,6 +14,16 @@ local function command()
return { install.expected_path() }
end
--- `initializationOptions` payload: the server reads it once at
--- `initialize` time via `Config::from_init_params`. This is what
--- delivers `wiki_root`/`wikis`/HTML/diary config to Rust.
local function init_options()
return config.options
end
--- `settings` payload: the same shape, but sent via
--- `workspace/didChangeConfiguration` after startup. Lets the server
--- pick up config changes without a restart.
local function server_settings()
return {
nuwiki = config.options,
@@ -40,6 +50,7 @@ function M.register()
cmd = command(),
filetypes = { 'vimwiki' },
root_markers = { '.git', '.nuwiki' },
init_options = init_options(),
settings = server_settings(),
})
vim.lsp.enable('nuwiki')
@@ -55,6 +66,7 @@ function M.register()
name = 'nuwiki',
cmd = command(),
root_dir = root_dir_for(ev.file),
init_options = init_options(),
settings = server_settings(),
})
end,