From db9c1c5c11fe692db17f8240485becdfee117e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Sun, 31 May 2026 17:57:03 +0000 Subject: [PATCH] feat: real range/bang/visual handling for the deferred command gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the four items previously left as "needs handler work" — all fully wired, not cosmetic. - ToggleListItem / IncrementListItem / DecrementListItem are now -range (both clients). The client loops the per-line op over [,] (toggle_list_item_range / list_cycle_symbol_range). The server's edits are version-less single-line WorkspaceEdits, so each line applies independently — no server protocol change. `:'<,'>VimwikiToggleListItem` toggles the whole selection. - VimwikiRebuildTags gains -bang: `:…RebuildTags!` passes { all: true } and the server (tags_rebuild) re-indexes every configured wiki via wikis_snapshot(), not just the current one. - VimwikiNormalizeLink is -nargs=?: with `1` (upstream's visual flag; the x-mode `+` mapping now passes it) it wraps the '< / '> selection as [[selection]] (new wrap_visual_as_wikilink in both clients). - VimwikiCheckLinks is -range: a ranged invocation filters the broken-link report to the current buffer's selected lines (client-side filter in results_to_qf / check_links). Tests: cmd.toggle_list_item_range + cmd.normalize_link_visual_wraps_selection (test-keymaps.lua), normalize.visual_wraps_selection (test-keymaps-vim.vim). README + doc/nuwiki.txt + gap doc updated. fmt/clippy clean; nvim 274, vim 266+18, all Rust tests + config-parity green. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 8 +-- autoload/nuwiki/commands.vim | 85 +++++++++++++++++++--- crates/nuwiki-lsp/src/commands.rs | 99 ++++++++++++++++---------- development/tests/test-keymaps-vim.vim | 11 +++ development/tests/test-keymaps.lua | 28 ++++++++ development/vimwiki-gap.md | 27 ++++--- doc/nuwiki.txt | 10 +-- ftplugin/vimwiki.vim | 50 ++++++------- lua/nuwiki/commands.lua | 81 +++++++++++++++++---- lua/nuwiki/keymaps.lua | 3 +- 10 files changed, 297 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 6873a57..6661221 100644 --- a/README.md +++ b/README.md @@ -429,10 +429,10 @@ available on both the Neovim and plain-Vim paths. | Nuwiki | Vimwiki | Action | |---|---|---| | `:NuwikiNextTask` | `:VimwikiNextTask` | Jump to the next unfinished task | -| `:NuwikiToggleListItem` | `:VimwikiToggleListItem` | Toggle the checkbox under the cursor | +| `:NuwikiToggleListItem` | `:VimwikiToggleListItem` | Toggle the checkbox under the cursor (`-range`: every item in the selection) | | `:NuwikiToggleRejected` | `:VimwikiToggleRejectedListItem` | Toggle the rejected marker `[-]` | | `:NuwikiListToggle` | `:VimwikiListToggle` | Toggle a checkbox on the current item (adds `[ ]` if none) | -| `:NuwikiIncrementListItem` / `:NuwikiDecrementListItem` | `:VimwikiIncrementListItem` / `:VimwikiDecrementListItem` | Cycle the list marker to the next / previous symbol | +| `:NuwikiIncrementListItem` / `:NuwikiDecrementListItem` | `:VimwikiIncrementListItem` / `:VimwikiDecrementListItem` | Cycle the list marker to the next / previous symbol (`-range`: every item in the selection) | | `:NuwikiChangeSymbol {sym}` | `:VimwikiChangeSymbolTo {sym}` (`:VimwikiListChangeSymbolI`) | Set the current item's marker to `{sym}` | | `:NuwikiChangeSymbolInList {sym}` | `:VimwikiChangeSymbolInListTo {sym}` | Set every item in the list to `{sym}` | | `:NuwikiRemoveDone[!]` | `:VimwikiRemoveDone[!]` | Remove every completed item from the current list; with `!`, from the whole buffer | @@ -451,9 +451,9 @@ available on both the Neovim and plain-Vim paths. |---|---|---| | `:NuwikiTOC` | `:VimwikiTOC` | Generate / refresh the table of contents on the current page | | `:NuwikiGenerateLinks` | `:VimwikiGenerateLinks` | Insert a flat list of every page in the wiki | -| `:NuwikiCheckLinks` | `:VimwikiCheckLinks` | Send broken links to the quickfix list | +| `:NuwikiCheckLinks` | `:VimwikiCheckLinks` | Send broken links to the quickfix list (`-range`: limit to the current buffer's selected lines) | | `:NuwikiFindOrphans` | `:VimwikiFindOrphans` | List pages with no incoming links in the quickfix list | -| `:NuwikiRebuildTags` | `:VimwikiRebuildTags` | Force a full workspace re-index | +| `:NuwikiRebuildTags[!]` | `:VimwikiRebuildTags[!]` | Re-index the current wiki; `!` re-indexes every configured wiki | | `:NuwikiSearchTags {tag}` | `:VimwikiSearchTags {tag}` | Quickfix listing every occurrence of `:tag:` | | `:NuwikiGenerateTagLinks [tag]` | `:VimwikiGenerateTagLinks [tag]` (`:VimwikiGenerateTags`) | Insert a section linking every page that has `` | | `:NuwikiColorize {color}` | `:VimwikiColorize {color}` | Wrap the word / visual selection in a coloured `` | diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim index b19d535..e672921 100644 --- a/autoload/nuwiki/commands.vim +++ b/autoload/nuwiki/commands.vim @@ -529,6 +529,26 @@ endfunction function! nuwiki#commands#toggle_list_item() abort call s:exec_pos('nuwiki.list.toggleCheckbox') endfunction + +" Apply a per-line action across [l1, l2] (visual-range commands). Each call +" sends its own request for its line; the server's edits are version-less and +" single-line, so applying them as they return never conflicts. +function! s:over_range(l1, l2, Fn) abort + let l:save = getcurpos() + for l:ln in range(a:l1, a:l2) + call cursor(l:ln, 1) + call a:Fn() + endfor + call setpos('.', l:save) +endfunction + +function! nuwiki#commands#toggle_list_item_range(l1, l2) abort + call s:over_range(a:l1, a:l2, function('nuwiki#commands#toggle_list_item')) +endfunction + +function! nuwiki#commands#list_cycle_symbol_range(direction, l1, l2) abort + call s:over_range(a:l1, a:l2, { -> nuwiki#commands#list_cycle_symbol(a:direction) }) +endfunction function! nuwiki#commands#cycle_list_item() abort call s:exec_pos('nuwiki.list.cycleCheckbox') endfunction @@ -573,9 +593,12 @@ endfunction function! nuwiki#commands#links_generate() abort call s:exec('nuwiki.links.generate', [{'uri': s:buf_uri()}]) endfunction -function! nuwiki#commands#check_links() abort +function! nuwiki#commands#check_links(...) abort + " a:1 = range count; when >0, restrict the report to the current buffer's + " [a:2, a:3] lines (`:'<,'>VimwikiCheckLinks`). No range → whole workspace. + let l:rng = (a:0 >= 3 && a:1 > 0) ? [a:2, a:3] : [] call s:exec('nuwiki.workspace.checkLinks', [{ 'uri': s:buf_uri() }], - \ {n -> s:results_to_qf(n, 'broken links')}) + \ {n -> s:results_to_qf(n, 'broken links', l:rng)}) endfunction function! nuwiki#commands#find_orphans() abort @@ -583,7 +606,10 @@ function! nuwiki#commands#find_orphans() abort \ {n -> s:results_to_qf(n, 'orphans')}) endfunction -function! s:results_to_qf(notification, title) abort +function! s:results_to_qf(notification, title, ...) abort + " Optional a:1 = [line1, line2]: keep only current-buffer rows in that range. + let l:rng = a:0 >= 1 ? a:1 : [] + let l:cur = empty(l:rng) ? '' : resolve(expand('%:p')) let l:rows = get(get(a:notification, 'response', {}), 'result', []) if type(l:rows) != type([]) || empty(l:rows) echom 'nuwiki: no ' . a:title @@ -598,9 +624,18 @@ function! s:results_to_qf(notification, title) abort let l:lnum = l:row['range']['start']['line'] + 1 let l:col = l:row['range']['start']['character'] + 1 endif + if !empty(l:rng) + if resolve(l:path) !=# l:cur || l:lnum < l:rng[0] || l:lnum > l:rng[1] + continue + endif + 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 + if empty(l:items) + echom 'nuwiki: no ' . a:title + return + endif call setqflist([], ' ', { 'title': 'nuwiki ' . a:title, 'items': l:items }) copen endfunction @@ -624,8 +659,11 @@ function! nuwiki#commands#tags_generate_links(tag) abort 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() }]) +function! nuwiki#commands#tags_rebuild(...) abort + " a:1 (the command's ) → re-index every configured wiki. + let l:all = a:0 >= 1 && a:1 + call s:exec('nuwiki.tags.rebuild', + \ [{ 'uri': s:buf_uri(), 'all': l:all ? v:true : v:false }]) endfunction " ===== HTML export ===== @@ -1205,15 +1243,46 @@ function! nuwiki#commands#smart_shift_tab() abort return "\call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\" endfunction -" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]` -" without following. Pure-VimL. -function! nuwiki#commands#normalize_link() abort +" `:VimwikiNormalizeLink [1]` — wrap text as `[[…]]` without following. +" With a:1 (upstream's visual `1` arg, also the `x`-mode mapping) wrap the +" `'<`/`'>` selection; otherwise the word under the cursor. Pure-VimL. +function! nuwiki#commands#normalize_link(...) abort + let l:visual = a:0 >= 1 && a:1 + if l:visual + call s:wrap_visual_as_wikilink() + return + endif if s:cursor_inside_wikilink() return endif call s:wrap_cword_as_wikilink() endfunction +" Wrap the single-line `'<`/`'>` visual selection as `[[selection]]`. +function! s:wrap_visual_as_wikilink() abort + let l:sp = getpos("'<") + let l:ep = getpos("'>") + if l:sp[1] <= 0 || l:sp[1] != l:ep[1] + return + endif + let l:line = getline(l:sp[1]) + let l:sc = l:sp[2] + let l:ec = l:ep[2] + if l:ec > strlen(l:line) + let l:ec = strlen(l:line) + endif + if l:ec < l:sc + return + endif + let l:sel = strpart(l:line, l:sc - 1, l:ec - l:sc + 1) + " Don't double-wrap if the selection is already a bracketed link. + if l:sel =~# '^\[\[.*\]\]$' + return + endif + let l:new = strpart(l:line, 0, l:sc - 1) . '[[' . l:sel . ']]' . strpart(l:line, l:ec) + call setline(l:sp[1], l:new) +endfunction + function! s:echo_url_from(response, ...) abort let l:result = get(get(a:response, 'response', {}), 'result', '') if type(l:result) == type('') && l:result !=# '' diff --git a/crates/nuwiki-lsp/src/commands.rs b/crates/nuwiki-lsp/src/commands.rs index 96131f9..7db8e97 100644 --- a/crates/nuwiki-lsp/src/commands.rs +++ b/crates/nuwiki-lsp/src/commands.rs @@ -619,56 +619,77 @@ fn tags_generate_links( } fn tags_rebuild(backend: &Backend, args: Vec) -> Result, String> { + // `:VimwikiRebuildTags!` passes `{ all: true }` to re-index every + // configured wiki (upstream's bang variant); otherwise just the current. + let all = args + .first() + .and_then(|v| v.get("all")) + .and_then(Value::as_bool) + .unwrap_or(false); let p = parse_optional_uri_arg(args)?; - let Some(wiki) = resolve_diary_wiki(backend, p.uri.as_ref()) else { - return Ok(None); - }; let registry = std::sync::Arc::clone(&backend.registry); - let root = wiki.config.root.clone(); - if root.as_os_str().is_empty() { - return Ok(Some( - serde_json::json!({ "pages": 0, "reason": "no wiki root configured" }), - )); - } let Some(plugin) = registry.get("vimwiki") else { return Err("vimwiki syntax plugin missing".to_string()); }; - let files = crate::index::walk_wiki_files(&root); - let total = files.len(); + let targets: Vec = if all { + backend.wikis_snapshot() + } else { + match resolve_diary_wiki(backend, p.uri.as_ref()) { + Some(w) => vec![w], + None => return Ok(None), + } + }; + let mut indexed = 0usize; - let mut idx = wiki - .index - .write() - .map_err(|_| "index lock poisoned".to_string())?; - // Drop pages whose files no longer exist. Collect first to avoid mutating - // during iteration. - let stale: Vec = idx - .pages_by_uri - .keys() - .filter(|u| match u.to_file_path() { - Ok(p) => !p.exists(), - Err(_) => true, - }) - .cloned() - .collect(); - for u in &stale { - idx.remove(u); - } - for path in files { - let Ok(text) = std::fs::read_to_string(&path) else { + let mut total = 0usize; + let mut stale_removed = 0usize; + let mut wikis_done = 0usize; + for wiki in &targets { + let root = wiki.config.root.clone(); + if root.as_os_str().is_empty() { continue; - }; - let ast = plugin.parse(&text); - let Ok(uri) = Url::from_file_path(&path) else { - continue; - }; - idx.upsert(uri, &ast); - indexed += 1; + } + let files = crate::index::walk_wiki_files(&root); + total += files.len(); + let mut idx = wiki + .index + .write() + .map_err(|_| "index lock poisoned".to_string())?; + // Drop pages whose files no longer exist. Collect first to avoid + // mutating during iteration. + let stale: Vec = idx + .pages_by_uri + .keys() + .filter(|u| match u.to_file_path() { + Ok(p) => !p.exists(), + Err(_) => true, + }) + .cloned() + .collect(); + for u in &stale { + idx.remove(u); + } + stale_removed += stale.len(); + for path in files { + let Ok(text) = std::fs::read_to_string(&path) else { + continue; + }; + let ast = plugin.parse(&text); + let Ok(uri) = Url::from_file_path(&path) else { + continue; + }; + idx.upsert(uri, &ast); + indexed += 1; + } + wikis_done += 1; } + Ok(Some(serde_json::json!({ "pages": indexed, "files_seen": total, - "stale_removed": stale.len(), + "stale_removed": stale_removed, + "wikis": wikis_done, + "all": all, }))) } diff --git a/development/tests/test-keymaps-vim.vim b/development/tests/test-keymaps-vim.vim index dd34fad..338b095 100644 --- a/development/tests/test-keymaps-vim.vim +++ b/development/tests/test-keymaps-vim.vim @@ -577,6 +577,17 @@ call s:record( \ 'err=' . s:cz_e . ' real=' . synconcealed(3, 1)[0] \ . ' rgb=' . synconcealed(2, stridx(getline(2), '` selection. +call s:set_buf(['one two three']) +call cursor(1, 5) +call feedkeys("viw\", 'tx') +call nuwiki#commands#normalize_link(1) +call s:record( + \ getline(1) ==# 'one [[two]] three' ? 1 : 0, + \ 'normalize.visual_wraps_selection', + \ 'got ' . string(getline(1))) + " ===== Follow-link resolves + opens in the current window (regression) ===== " follow_link_or_create() must resolve the definition itself and `:edit` the " target in the CURRENT window — including a page that does not exist yet diff --git a/development/tests/test-keymaps.lua b/development/tests/test-keymaps.lua index 0d0a2f3..03de89c 100644 --- a/development/tests/test-keymaps.lua +++ b/development/tests/test-keymaps.lua @@ -792,6 +792,34 @@ vim.defer_fn(function() end end) + -- ===== Visual-range list commands + normalize-visual (regression) ===== + -- :N,MVimwikiToggleListItem toggles every checkbox in the range (server, + -- one request per line; wait for the edits to land). + tobj_case('cmd.toggle_list_item_range', function() + set_buf({ '- [ ] a', '- [ ] b', '- [ ] c' }) + vim.cmd('1,3VimwikiToggleListItem') + local done = vim.wait(2000, function() + for _, l in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do + if not l:match('%[[xX]%]') then return false end + end + return true + end, 30) + if not done then + error('range toggle incomplete: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false))) + end + end) + -- `:VimwikiNormalizeLink 1` (the arg is upstream's visual flag; the command + -- is -nargs=? not -range, so no `'<,'>`) wraps the last visual selection. + tobj_case('cmd.normalize_link_visual_wraps_selection', function() + set_buf({ 'one two three' }) + vim.cmd('normal! 0wviw\27') -- select 'two', leave visual (sets '<,'> marks) + vim.cmd('VimwikiNormalizeLink 1') + local got = vim.api.nvim_get_current_line() + if got ~= 'one [[two]] three' then + error('visual normalize did not wrap selection: ' .. got) + end + end) + -- ===== Wiki picker (config-driven, no LSP) ===== -- The picker must build its list from config so it works from any buffer -- before the server attaches. diff --git a/development/vimwiki-gap.md b/development/vimwiki-gap.md index e721c53..5546541 100644 --- a/development/vimwiki-gap.md +++ b/development/vimwiki-gap.md @@ -132,7 +132,10 @@ fix site. (align) vs `gww` (align w/o resize) distinction lost. - [ ] `VimwikiTable` is `-nargs=1` vs upstream `-nargs=*` (can't pass cols+rows). - [ ] `VimwikiListChangeLvl` is `-nargs=?` vs upstream `-range -nargs=+`. -- [ ] `VimwikiRebuildTags` lacks `-bang` ("rebuild all" variant). +- [x] `VimwikiRebuildTags` `-bang` ("rebuild all") — `:…RebuildTags!` now passes + `{ all: true }`; the server (`tags_rebuild`) re-indexes **every** configured + wiki (via `wikis_snapshot()`) instead of just the current one. `-bang` added to + all four command defs. - [ ] `:VimwikiSearch` / `VWS` uses `lvimgrep`, not vimwiki's search engine. - [ ] `VimwikiIndex` family is buffer-local in nuwiki (upstream defines globally in `plugin/`); only `:…UISelect` is a global entry point. @@ -175,9 +178,12 @@ fix site. also switched both Neovim `Vimwiki/NuwikiTabIndex` from `vim.v.count` (always 0 in command context) to ``, so a `:NNuwikiTabIndex` count is now actually honored. -- [ ] **`VimwikiToggleListItem` / `Increment` / `DecrementListItem` lack - `-range`** (both branches) vs upstream's `-range` (`:350`) — no visual-range - checkbox toggle / symbol cycle. +- [x] **`VimwikiToggleListItem` / `Increment` / `DecrementListItem` `-range`** — + all three (both branches) are now `-range`; the client loops the per-line op + over `[, ]` (`toggle_list_item_range` / `list_cycle_symbol_range`). + The server edits are version-less single-line, so each line's edit applies + independently — no server protocol change needed. Covered by + `cmd.toggle_list_item_range` in `test-keymaps.lua`. - [ ] **Diary-note family lacks `-count`** _(new, 2026-05-31 re-audit)_ — upstream's `:Vimwiki{Make,TabMake,MakeYesterday,MakeTomorrow}DiaryNote` carry `-count=0` (the count selects the wiki number); nuwiki's diary-note commands declare no @@ -189,11 +195,14 @@ fix site. (colours), and the tag commands. nuwiki defines none, so `` completion of page / tag / colour names at the `:` line is unavailable. _Fix:_ `ftplugin/vimwiki.vim` (both branches) — VimL completers or LSP-backed. -- [ ] **Minor command-attribute gaps** _(new)_ — `VimwikiNormalizeLink` lacks - upstream's `-nargs=?`; `VimwikiCheckLinks` lacks `-range`. (`VimwikiColorize` - `-nargs=1`→`-nargs=*` **done** — all four defs now match upstream, so a bare - `:VimwikiColorize` prompts for the colour.) The remaining two would be - accepted-but-ignored without handler work, so left open. +- [x] **Minor command-attribute gaps** _(new)_ — all now real, not cosmetic: + `VimwikiNormalizeLink` is `-nargs=?` and with `1` wraps the `'<`/`'>` selection + (`wrap_visual_as_wikilink`, both clients; the `x`-mode `+` passes it too); + `VimwikiCheckLinks` is `-range` and a ranged invocation filters the report to + the current buffer's selected lines; `VimwikiColorize` `-nargs=1`→`-nargs=*` + (bare `:VimwikiColorize` prompts). Covered by + `normalize.visual_wraps_selection` (vim) / + `cmd.normalize_link_visual_wraps_selection` (nvim). ### Config - [ ] `auto_header` — auto H1-from-filename on new page (server-side). diff --git a/doc/nuwiki.txt b/doc/nuwiki.txt index 148970d..a6c1d2b 100644 --- a/doc/nuwiki.txt +++ b/doc/nuwiki.txt @@ -376,8 +376,10 @@ Page generation ~ Insert a flat list of every page in the wiki under the cursor. *:NuwikiCheckLinks* -:NuwikiCheckLinks - Send every broken link in the workspace to the |quickfix| list. +:[range]NuwikiCheckLinks + Send every broken link in the workspace to the |quickfix| list. With a + range (e.g. `:'<,'>NuwikiCheckLinks`) the report is limited to the current + buffer's selected lines. Tags ~ @@ -391,8 +393,8 @@ Tags ~ argument, generates a section per tag found in the workspace. *:NuwikiRebuildTags* -:NuwikiRebuildTags - Force a full workspace re-index. +:NuwikiRebuildTags[!] + Re-index the current wiki. With `!`, re-index every configured wiki. HTML export ~ diff --git a/ftplugin/vimwiki.vim b/ftplugin/vimwiki.vim index 2389310..90c1ab5 100644 --- a/ftplugin/vimwiki.vim +++ b/ftplugin/vimwiki.vim @@ -76,11 +76,11 @@ if !has('nvim') command! -buffer VimwikiRenameFile call nuwiki#commands#rename_file() command! -buffer VimwikiRenameLink call nuwiki#commands#rename_file() command! -buffer VimwikiNextTask call nuwiki#commands#next_task() - command! -buffer VimwikiToggleListItem call nuwiki#commands#toggle_list_item() + command! -buffer -range VimwikiToggleListItem call nuwiki#commands#toggle_list_item_range(, ) command! -buffer VimwikiToggleRejectedListItem call nuwiki#commands#reject_list_item() command! -buffer VimwikiListToggle call nuwiki#commands#list_toggle_or_add_checkbox() - command! -buffer VimwikiIncrementListItem call nuwiki#commands#list_cycle_symbol(1) - command! -buffer VimwikiDecrementListItem call nuwiki#commands#list_cycle_symbol(-1) + command! -buffer -range VimwikiIncrementListItem call nuwiki#commands#list_cycle_symbol_range(1, , ) + command! -buffer -range VimwikiDecrementListItem call nuwiki#commands#list_cycle_symbol_range(-1, , ) command! -buffer -nargs=1 VimwikiChangeSymbolTo call nuwiki#commands#list_change_symbol(, 0) command! -buffer -nargs=1 VimwikiListChangeSymbolI call nuwiki#commands#list_change_symbol(, 0) command! -buffer -nargs=1 VimwikiChangeSymbolInListTo call nuwiki#commands#list_change_symbol(, 1) @@ -88,7 +88,7 @@ if !has('nvim') command! -buffer -range VimwikiRemoveSingleCB call nuwiki#commands#list_remove_checkbox() command! -buffer VimwikiRemoveCBInList call nuwiki#commands#list_remove_checkbox_in_list() command! -buffer -nargs=? VimwikiListChangeLvl call nuwiki#commands#list_change_lvl() - command! -buffer VimwikiNormalizeLink call nuwiki#commands#normalize_link() + command! -buffer -nargs=? VimwikiNormalizeLink call nuwiki#commands#normalize_link() command! -buffer VimwikiRenumberList call nuwiki#commands#list_renumber() command! -buffer VimwikiRenumberAllLists call nuwiki#commands#list_renumber_all() command! -buffer VimwikiTableAlignQ call nuwiki#commands#table_align() @@ -101,10 +101,10 @@ if !has('nvim') 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 -range VimwikiCheckLinks call nuwiki#commands#check_links(, , ) command! -buffer VimwikiFindOrphans call nuwiki#commands#find_orphans() - command! -buffer VimwikiRebuildTags call nuwiki#commands#tags_rebuild() + command! -buffer -bang VimwikiRebuildTags call nuwiki#commands#tags_rebuild(0) command! -buffer -nargs=? VimwikiSearchTags call nuwiki#commands#tags_search() command! -buffer -nargs=? VimwikiGenerateTagLinks call nuwiki#commands#tags_generate_links() command! -buffer -nargs=? VimwikiGenerateTags call nuwiki#commands#tags_generate_links() @@ -137,7 +137,7 @@ if !has('nvim') 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 -range NuwikiToggleListItem call nuwiki#commands#toggle_list_item_range(, ) 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() @@ -145,9 +145,9 @@ if !has('nvim') 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 -range NuwikiCheckLinks call nuwiki#commands#check_links(, , ) command! -buffer NuwikiFindOrphans call nuwiki#commands#find_orphans() - command! -buffer NuwikiRebuildTags call nuwiki#commands#tags_rebuild() + command! -buffer -bang NuwikiRebuildTags call nuwiki#commands#tags_rebuild(0) 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() @@ -173,11 +173,11 @@ if !has('nvim') command! -buffer NuwikiRemoveCheckboxInList call nuwiki#commands#list_remove_checkbox_in_list() command! -buffer -nargs=? NuwikiListChangeLvl call nuwiki#commands#list_change_lvl() command! -buffer NuwikiListToggle call nuwiki#commands#list_toggle_or_add_checkbox() - command! -buffer NuwikiIncrementListItem call nuwiki#commands#list_cycle_symbol(1) - command! -buffer NuwikiDecrementListItem call nuwiki#commands#list_cycle_symbol(-1) + command! -buffer -range NuwikiIncrementListItem call nuwiki#commands#list_cycle_symbol_range(1, , ) + command! -buffer -range NuwikiDecrementListItem call nuwiki#commands#list_cycle_symbol_range(-1, , ) command! -buffer -nargs=1 NuwikiChangeSymbol call nuwiki#commands#list_change_symbol(, 0) command! -buffer -nargs=1 NuwikiChangeSymbolInList call nuwiki#commands#list_change_symbol(, 1) - command! -buffer NuwikiNormalizeLink call nuwiki#commands#normalize_link() + command! -buffer -nargs=? NuwikiNormalizeLink call nuwiki#commands#normalize_link() command! -buffer NuwikiRenumberList call nuwiki#commands#list_renumber() command! -buffer NuwikiRenumberAllLists call nuwiki#commands#list_renumber_all() command! -buffer NuwikiTableAlign call nuwiki#commands#table_align() @@ -223,7 +223,7 @@ if !has('nvim') nnoremap /\[\[:nohlsearch nnoremap ?\[\[:nohlsearch nnoremap + :call nuwiki#commands#normalize_link() - xnoremap + :call nuwiki#commands#normalize_link() + xnoremap + :call nuwiki#commands#normalize_link(1) nnoremap wn :call nuwiki#commands#wiki_goto_page('') nnoremap wd :call nuwiki#commands#delete_file() nnoremap wr :call nuwiki#commands#rename_file() @@ -417,11 +417,11 @@ command! -buffer VimwikiRenameFile lua require('nuwiki.commands').rename_ command! -buffer VimwikiRenameLink lua require('nuwiki.commands').rename_file() command! -buffer VimwikiNextTask lua require('nuwiki.commands').next_task() -command! -buffer VimwikiToggleListItem lua require('nuwiki.commands').toggle_list_item() +command! -buffer -range VimwikiToggleListItem lua require('nuwiki.commands').toggle_list_item_range(, ) command! -buffer VimwikiToggleRejectedListItem lua require('nuwiki.commands').reject_list_item() command! -buffer VimwikiListToggle lua require('nuwiki.commands').list_toggle_or_add_checkbox() -command! -buffer VimwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol(1) -command! -buffer VimwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol(-1) +command! -buffer -range VimwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(1, , ) +command! -buffer -range VimwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(-1, , ) command! -buffer -nargs=1 VimwikiChangeSymbolTo lua require('nuwiki.commands').list_change_symbol(, false) command! -buffer -nargs=1 VimwikiListChangeSymbolI lua require('nuwiki.commands').list_change_symbol(, false) command! -buffer -nargs=1 VimwikiChangeSymbolInListTo lua require('nuwiki.commands').list_change_symbol(, true) @@ -429,7 +429,7 @@ command! -buffer -bang VimwikiRemoveDone lua require('nuwiki.command command! -buffer -range VimwikiRemoveSingleCB lua require('nuwiki.commands').list_remove_checkbox() command! -buffer VimwikiRemoveCBInList lua require('nuwiki.commands').list_remove_checkbox_in_list() command! -buffer -nargs=? VimwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl() -command! -buffer VimwikiNormalizeLink lua require('nuwiki.commands').normalize_link() +command! -buffer -nargs=? VimwikiNormalizeLink lua require('nuwiki.commands').normalize_link() command! -buffer VimwikiRenumberList lua require('nuwiki.commands').list_renumber() command! -buffer VimwikiRenumberAllLists lua require('nuwiki.commands').list_renumber_all() command! -buffer VimwikiTableAlignQ lua require('nuwiki.commands').table_align() @@ -442,10 +442,10 @@ command! -buffer VimwikiRss lua require('nuwiki.commands').export command! -buffer VimwikiTOC lua require('nuwiki.commands').toc_generate() command! -buffer VimwikiGenerateLinks lua require('nuwiki.commands').links_generate() -command! -buffer VimwikiCheckLinks lua require('nuwiki.commands').check_links() +command! -buffer -range VimwikiCheckLinks lua require('nuwiki.commands').check_links(, , ) command! -buffer VimwikiFindOrphans lua require('nuwiki.commands').find_orphans() -command! -buffer VimwikiRebuildTags lua require('nuwiki.commands').tags_rebuild() +command! -buffer -bang VimwikiRebuildTags lua require('nuwiki.commands').tags_rebuild('' == '!') command! -buffer -nargs=? VimwikiSearchTags lua require('nuwiki.commands').tags_search() command! -buffer -nargs=? VimwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links() command! -buffer -nargs=? VimwikiGenerateTags lua require('nuwiki.commands').tags_generate_links() @@ -475,7 +475,7 @@ command! -buffer NuwikiBacklinks lua vim.lsp.buf.references() command! -buffer NuwikiDeleteFile lua require('nuwiki.commands').delete_file() command! -buffer NuwikiRenameFile lua require('nuwiki.commands').rename_file() command! -buffer NuwikiNextTask lua require('nuwiki.commands').next_task() -command! -buffer NuwikiToggleListItem lua require('nuwiki.commands').toggle_list_item() +command! -buffer -range NuwikiToggleListItem lua require('nuwiki.commands').toggle_list_item_range(, ) command! -buffer NuwikiToggleRejected lua require('nuwiki.commands').reject_list_item() command! -buffer NuwikiExport lua require('nuwiki.commands').export_current() command! -buffer NuwikiExportBrowse lua require('nuwiki.commands').export_browse() @@ -483,9 +483,9 @@ command! -buffer -bang NuwikiExportAll execute (0 ? "lua require command! -buffer NuwikiRss lua require('nuwiki.commands').export_rss() command! -buffer NuwikiTOC lua require('nuwiki.commands').toc_generate() command! -buffer NuwikiGenerateLinks lua require('nuwiki.commands').links_generate() -command! -buffer NuwikiCheckLinks lua require('nuwiki.commands').check_links() +command! -buffer -range NuwikiCheckLinks lua require('nuwiki.commands').check_links(, , ) command! -buffer NuwikiFindOrphans lua require('nuwiki.commands').find_orphans() -command! -buffer NuwikiRebuildTags lua require('nuwiki.commands').tags_rebuild() +command! -buffer -bang NuwikiRebuildTags lua require('nuwiki.commands').tags_rebuild('' == '!') command! -buffer -nargs=? NuwikiSearchTags lua require('nuwiki.commands').tags_search() command! -buffer -nargs=? NuwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links() command! -buffer -nargs=1 NuwikiGoto lua require('nuwiki.commands').wiki_goto_page() @@ -515,11 +515,11 @@ command! -buffer -range NuwikiRemoveCheckbox lua require('nuwiki.command command! -buffer NuwikiRemoveCheckboxInList lua require('nuwiki.commands').list_remove_checkbox_in_list() command! -buffer -nargs=? NuwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl() command! -buffer NuwikiListToggle lua require('nuwiki.commands').list_toggle_or_add_checkbox() -command! -buffer NuwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol(1) -command! -buffer NuwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol(-1) +command! -buffer -range NuwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(1, , ) +command! -buffer -range NuwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(-1, , ) command! -buffer -nargs=1 NuwikiChangeSymbol lua require('nuwiki.commands').list_change_symbol(, false) command! -buffer -nargs=1 NuwikiChangeSymbolInList lua require('nuwiki.commands').list_change_symbol(, true) -command! -buffer NuwikiNormalizeLink lua require('nuwiki.commands').normalize_link() +command! -buffer -nargs=? NuwikiNormalizeLink lua require('nuwiki.commands').normalize_link() command! -buffer NuwikiRenumberList lua require('nuwiki.commands').list_renumber() command! -buffer NuwikiRenumberAllLists lua require('nuwiki.commands').list_renumber_all() command! -buffer NuwikiTableAlign lua require('nuwiki.commands').table_align() diff --git a/lua/nuwiki/commands.lua b/lua/nuwiki/commands.lua index 14c3eb5..78237d5 100644 --- a/lua/nuwiki/commands.lua +++ b/lua/nuwiki/commands.lua @@ -361,6 +361,27 @@ function M.cycle_list_item_back() exec('nuwiki.list.cycleCheckbox', a) end M.reject_list_item = _exec_pos('nuwiki.list.rejectCheckbox') + +-- Apply a per-line action across [l1, l2] (visual-range commands). Each call +-- targets its own line; the server's version-less single-line edits apply +-- independently as they return, so no conflict. +local function over_range(l1, l2, fn) + local save = vim.api.nvim_win_get_cursor(0) + for ln = l1, l2 do + vim.api.nvim_win_set_cursor(0, { ln, 0 }) + fn() + end + pcall(vim.api.nvim_win_set_cursor, 0, save) +end + +function M.toggle_list_item_range(l1, l2) + over_range(l1, l2, M.toggle_list_item) +end + +function M.list_cycle_symbol_range(direction, l1, l2) + over_range(l1, l2, function() M.list_cycle_symbol(direction) end) +end + M.heading_add_level = _exec_pos('nuwiki.heading.addLevel') M.heading_remove_level = _exec_pos('nuwiki.heading.removeLevel') @@ -380,7 +401,11 @@ end M.toc_generate = function() exec('nuwiki.toc.generate', uri_args()) end M.links_generate = function() exec('nuwiki.links.generate', uri_args()) end -function M.check_links() +-- range>0 restricts the report to the current buffer's [l1, l2] lines +-- (`:'<,'>VimwikiCheckLinks`); otherwise the whole workspace. +function M.check_links(range, l1, l2) + local filter = type(range) == 'number' and range > 0 + local cur = filter and vim.api.nvim_buf_get_name(0) or nil exec('nuwiki.workspace.checkLinks', uri_args(), function(r) if type(r) ~= 'table' or #r == 0 then vim.notify('nuwiki: no broken links') @@ -388,12 +413,20 @@ function M.check_links() end local items = {} for _, b in ipairs(r) do - table.insert(items, { - filename = vim.uri_to_fname(b.uri), - lnum = (b.range and b.range.start and b.range.start.line or 0) + 1, - col = (b.range and b.range.start and b.range.start.character or 0) + 1, - text = (b.kind or '?') .. ': ' .. (b.message or ''), - }) + local fname = vim.uri_to_fname(b.uri) + local lnum = (b.range and b.range.start and b.range.start.line or 0) + 1 + if not filter or (fname == cur and lnum >= l1 and lnum <= l2) then + table.insert(items, { + filename = fname, + lnum = lnum, + col = (b.range and b.range.start and b.range.start.character or 0) + 1, + text = (b.kind or '?') .. ': ' .. (b.message or ''), + }) + end + end + if #items == 0 then + vim.notify('nuwiki: no broken links') + return end vim.fn.setqflist({}, ' ', { title = 'nuwiki broken links', items = items }) vim.cmd('copen') @@ -451,10 +484,14 @@ function M.tags_generate_links(tag) exec('nuwiki.tags.generateLinks', { args }) end -function M.tags_rebuild() - exec('nuwiki.tags.rebuild', uri_args(), function(r) +function M.tags_rebuild(all) + local a = uri_args() + a[1].all = all and true or false + exec('nuwiki.tags.rebuild', a, function(r) if r and r.pages then - vim.notify(string.format('nuwiki: re-indexed %d page(s)', r.pages)) + local scope = (r.all and (r.wikis or 0) ~= 1) + and string.format(' across %d wikis', r.wikis or 0) or '' + vim.notify(string.format('nuwiki: re-indexed %d page(s)%s', r.pages, scope)) end end) end @@ -1027,11 +1064,25 @@ function M.paste_url() exec('nuwiki.link.pasteUrl', pos_args()) end ---- Vimwiki `:VimwikiNormalizeLink` — wrap the word at cursor as ---- `[[word]]` without following. The `` two-step does the same ---- thing under the hood; expose it as a standalone command + keymap ---- for users who want to bind it to `+` per vimwiki defaults. -function M.normalize_link() +-- Wrap the single-line `'<`/`'>` visual selection as `[[selection]]`. +local function wrap_visual_as_wikilink() + local sl, sc, el, ec = visual_selection_range() + if not (sl and sl == el) then return end + local line = vim.api.nvim_buf_get_lines(0, sl - 1, sl, false)[1] or '' + local sel = line:sub(sc + 1, ec + 1) + if sel == '' or sel:match('^%[%[.*%]%]$') then return end + local new = line:sub(1, sc) .. '[[' .. sel .. ']]' .. line:sub(ec + 2) + vim.api.nvim_buf_set_lines(0, sl - 1, sl, false, { new }) +end + +--- Vimwiki `:VimwikiNormalizeLink [1]` — wrap text as `[[…]]` without +--- following. With `visual` (upstream's `1` arg / the `x`-mode `+` mapping) +--- wrap the selection; otherwise the word at the cursor. +function M.normalize_link(visual) + if visual then + wrap_visual_as_wikilink() + return + end if cursor_inside_wikilink() then return end wrap_cword_as_wikilink() end diff --git a/lua/nuwiki/keymaps.lua b/lua/nuwiki/keymaps.lua index 80b6bf0..7084ab5 100644 --- a/lua/nuwiki/keymaps.lua +++ b/lua/nuwiki/keymaps.lua @@ -174,7 +174,8 @@ function M.attach(bufnr, mappings) map('n', '', link.next, { desc = 'nuwiki: next wikilink' }, bufnr) map('n', '', link.prev, { desc = 'nuwiki: prev wikilink' }, bufnr) map('n', '+', cmd.normalize_link, { desc = 'nuwiki: wrap word as wikilink' }, bufnr) - map('x', '+', cmd.normalize_link, { desc = 'nuwiki: wrap word as wikilink' }, bufnr) + map('x', '+', function() cmd.normalize_link(true) end, + { desc = 'nuwiki: wrap selection as wikilink' }, bufnr) map('n', 'wn', cmd.wiki_goto_page, { desc = 'nuwiki: goto page' }, bufnr) map('n', 'wd', cmd.delete_file, { desc = 'nuwiki: delete page' }, bufnr) map('n', 'wr', cmd.rename_file, { desc = 'nuwiki: rename page' }, bufnr)