diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim index 9af000c..c32c281 100644 --- a/autoload/nuwiki/commands.vim +++ b/autoload/nuwiki/commands.vim @@ -94,6 +94,69 @@ function! s:wiki_pick(notification) abort \ function('s:open_uri_from')) endfunction +" ===== Smart : follow or wrap-then-follow ===== + +function! s:cursor_inside_wikilink() abort + let l:line = getline('.') + let l:col = col('.') + let l:i = 1 + let l:open = 0 + while l:i < strlen(l:line) + if strpart(l:line, l:i - 1, 2) ==# '[[' + let l:open = l:i + let l:i += 2 + elseif strpart(l:line, l:i - 1, 2) ==# ']]' + let l:open = 0 + let l:i += 2 + else + let l:i += 1 + endif + if l:i > l:col + break + endif + endwhile + return l:open > 0 +endfunction + +function! s:wrap_cword_as_wikilink() abort + let l:word = expand('') + if l:word ==# '' + return 0 + endif + let l:line = getline('.') + let l:col = col('.') + " Find word boundaries around cursor (1-based). + let l:left = l:col + while l:left > 1 && strpart(l:line, l:left - 2, 1) =~# '[A-Za-z0-9_-]' + let l:left -= 1 + endwhile + let l:right = l:col + while l:right <= strlen(l:line) && strpart(l:line, l:right - 1, 1) =~# '[A-Za-z0-9_-]' + let l:right += 1 + endwhile + let l:word_real = strpart(l:line, l:left - 1, l:right - l:left) + if l:word_real ==# '' + return 0 + endif + let l:new_line = strpart(l:line, 0, l:left - 1) . '[[' . l:word_real . ']]' . strpart(l:line, l:right - 1) + call setline('.', l:new_line) + call cursor(line('.'), l:left + 2) + return 1 +endfunction + +function! nuwiki#commands#follow_link_or_create() abort + if !s:cursor_inside_wikilink() + call s:wrap_cword_as_wikilink() + endif + if exists(':LspDefinition') == 2 + LspDefinition + else + echohl WarningMsg + echom 'nuwiki: :LspDefinition unavailable; install vim-lsp' + echohl None + endif +endfunction + " Pure-VimL bullet-continuation for `o` / `O` — mirrors the Lua side " so re-pressing the key keeps the list marker (and checkbox if any). function! nuwiki#commands#open_below_with_bullet() abort diff --git a/autoload/nuwiki/lsp.vim b/autoload/nuwiki/lsp.vim index f2251bc..ab09a59 100644 --- a/autoload/nuwiki/lsp.vim +++ b/autoload/nuwiki/lsp.vim @@ -36,6 +36,7 @@ function! nuwiki#lsp#start() abort \ 'name': 'nuwiki', \ 'cmd': {server_info -> [l:bin]}, \ 'allowlist': ['vimwiki'], + \ 'initialization_options': s:settings(), \ 'config': { 'settings': { 'nuwiki': s:settings() } }, \ }) " vim-lsp auto-enables servers when 'g:lsp_auto_enable' is on diff --git a/crates/nuwiki-lsp/src/lib.rs b/crates/nuwiki-lsp/src/lib.rs index fa34b18..030b7f7 100644 --- a/crates/nuwiki-lsp/src/lib.rs +++ b/crates/nuwiki-lsp/src/lib.rs @@ -247,18 +247,26 @@ impl Backend { synthesise_page_uri(&target_wiki.config, path) } LinkKind::Wiki => { - let source_wiki = self.wiki_for_uri(source_uri)?; - let source_page = - index::page_name_from_uri(source_uri, Some(source_wiki.config.root.as_path())); - if let Ok(idx) = source_wiki.index.read() { - if let Some(uri) = idx.resolve(target, &source_page).cloned() { - return Some(uri); + if let Some(source_wiki) = self.wiki_for_uri(source_uri) { + let source_page = index::page_name_from_uri( + source_uri, + Some(source_wiki.config.root.as_path()), + ); + if let Ok(idx) = source_wiki.index.read() { + if let Some(uri) = idx.resolve(target, &source_page).cloned() { + return Some(uri); + } } + let path = target.path.as_deref()?; + return synthesise_page_uri(&source_wiki.config, path); } - // Unindexed page — synthesise so `` opens (or - // creates-on-save) the missing wiki page. + // No wiki registered (no `initializationOptions`, no + // workspace folder). Fall back to the source buffer's + // parent directory so `` on `[[NewPage]]` still + // opens (and on save creates) the future page next to + // the current file. let path = target.path.as_deref()?; - synthesise_page_uri(&source_wiki.config, path) + synthesise_page_uri_next_to(source_uri, path) } _ => { let source_wiki = self.wiki_for_uri(source_uri)?; @@ -928,6 +936,30 @@ impl LanguageServer for Backend { } } +/// Same as `synthesise_page_uri` but anchored on `source_uri`'s parent +/// directory and the conventional `.wiki` extension. Used when no +/// `Wiki` is registered (server received no `initializationOptions` +/// and no `workspace_folders`) — `` on a wikilink still opens a +/// future page next to the current file. +fn synthesise_page_uri_next_to(source_uri: &Url, path: &str) -> Option { + let source_path = source_uri.to_file_path().ok()?; + let dir = source_path.parent()?; + let mut p = dir.to_path_buf(); + for segment in path.split('/') { + if !segment.is_empty() { + p.push(segment); + } + } + let stem = p + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + if !stem.is_empty() && !stem.contains('.') { + p.set_file_name(format!("{stem}.wiki")); + } + Url::from_file_path(p).ok() +} + /// Build a `/` URI for a wikilink that /// doesn't (yet) resolve to an indexed page. Used by /// `Backend::resolve_target_uri` so `` on `[[Newish]]` opens the diff --git a/ftplugin/vimwiki.vim b/ftplugin/vimwiki.vim index 98d6736..d1c3a35 100644 --- a/ftplugin/vimwiki.vim +++ b/ftplugin/vimwiki.vim @@ -122,10 +122,10 @@ if !has('nvim') nnoremap wi :call nuwiki#commands#diary_generate_index() " Links - nnoremap :LspDefinition - nnoremap :split:LspDefinition - nnoremap :vsplit:LspDefinition - nnoremap :tabnew:LspDefinition + nnoremap :call nuwiki#commands#follow_link_or_create() + nnoremap :split:call nuwiki#commands#follow_link_or_create() + nnoremap :vsplit:call nuwiki#commands#follow_link_or_create() + nnoremap :tabnew:call nuwiki#commands#follow_link_or_create() nnoremap nnoremap /\[\[:nohlsearch nnoremap ?\[\[:nohlsearch diff --git a/lua/nuwiki/commands.lua b/lua/nuwiki/commands.lua index bf65081..9db0d2f 100644 --- a/lua/nuwiki/commands.lua +++ b/lua/nuwiki/commands.lua @@ -95,6 +95,80 @@ local function uri_args() return { { uri = buf_uri() } } end +-- ===== Smart : 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 `` on `Notes` becomes `` 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('') + if cword == '' then return false end + -- Replace just this `` occurrence using `*` (the start of + -- the last cursor match) — `ciw[["]]` 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) diff --git a/lua/nuwiki/keymaps.lua b/lua/nuwiki/keymaps.lua index f9a3c33..edede8c 100644 --- a/lua/nuwiki/keymaps.lua +++ b/lua/nuwiki/keymaps.lua @@ -173,12 +173,13 @@ function M.attach(bufnr, mappings) -- ===== Links ===== if on('links') then - map('n', '', vim.lsp.buf.definition, { desc = 'nuwiki: follow link' }, bufnr) - map('n', '', function() vim.cmd('split'); vim.lsp.buf.definition() end, + map('n', '', cmd.follow_link_or_create, + { desc = 'nuwiki: follow link (or wrap word + follow)' }, bufnr) + map('n', '', function() vim.cmd('split'); cmd.follow_link_or_create() end, { desc = 'nuwiki: follow link in split' }, bufnr) - map('n', '', function() vim.cmd('vsplit'); vim.lsp.buf.definition() end, + map('n', '', function() vim.cmd('vsplit'); cmd.follow_link_or_create() end, { desc = 'nuwiki: follow link in vsplit' }, bufnr) - map('n', '', function() vim.cmd('tabnew'); vim.lsp.buf.definition() end, + map('n', '', function() vim.cmd('tabnew'); cmd.follow_link_or_create() end, { desc = 'nuwiki: follow link in new tab' }, bufnr) map('n', '', '', { desc = 'nuwiki: go back', remap = true }, bufnr) map('n', '', link.next, { desc = 'nuwiki: next wikilink' }, bufnr) diff --git a/lua/nuwiki/lsp.lua b/lua/nuwiki/lsp.lua index 53b6750..1767c9d 100644 --- a/lua/nuwiki/lsp.lua +++ b/lua/nuwiki/lsp.lua @@ -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,