diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim index ad9e529..265c5c6 100644 --- a/autoload/nuwiki/commands.vim +++ b/autoload/nuwiki/commands.vim @@ -1153,7 +1153,7 @@ endfunction " Mirrors `lua/nuwiki/commands.lua`'s `align_table_at` so plain Vim " buffers get the same "tighten columns as cells grow" behaviour the " Neovim path enjoys, without needing a vim-lsp roundtrip. Algorithm -" matches `nuwiki-lsp/src/commands.rs::ops::render_aligned_table` (nuwiki-rs repo). +" matches the table-alignment logic in the nuwiki-rs server. function! s:is_table_row(line) abort return nuwiki#util#is_table_row(a:line) diff --git a/lua/nuwiki/commands.lua b/lua/nuwiki/commands.lua index db05b00..e4bb585 100644 --- a/lua/nuwiki/commands.lua +++ b/lua/nuwiki/commands.lua @@ -24,13 +24,10 @@ local function find_client() end local function position_params(client) - -- Neovim 0.12+ requires `position_encoding` explicitly; older - -- versions accept the no-arg form. + -- Neovim 0.11+ requires the position encoding explicitly (the no-arg form is + -- deprecated); pass the current window + the client's negotiated encoding. local encoding = client and client.offset_encoding or 'utf-16' - local ok, params = pcall(vim.lsp.util.make_position_params, 0, encoding) - if ok then return params end - -- Fallback for Neovim < 0.10 where the second arg wasn't a thing. - return vim.lsp.util.make_position_params() + return vim.lsp.util.make_position_params(0, encoding) end local function exec(command, arguments, on_result) @@ -51,21 +48,13 @@ local function exec(command, arguments, on_result) end local bufnr = vim.api.nvim_get_current_buf() local payload = { command = command, arguments = arguments or {} } - -- Neovim 0.12 deprecates `client.request(...)` in favour of the - -- method form `client:request(...)`. Prefer the method, fall back - -- on older Neovim where it isn't defined yet. - local req = client.request - if type(req) ~= 'function' then + -- The method form `client:request(...)` is the correct, non-deprecated call + -- on every supported Neovim (0.11+); the old `client.request(...)` function + -- form was deprecated in its favour. + if type(client.request) ~= 'function' then return end - if rawget(getmetatable(client) or {}, '__index') and client.request ~= nil then - -- Method-style works for both colon and dot calls on Neovim's - -- Client metatable, but pass `self` explicitly for the older - -- function-style signature compatibility. - client:request('workspace/executeCommand', payload, handler, bufnr) - else - client.request('workspace/executeCommand', payload, handler, bufnr) - end + client:request('workspace/executeCommand', payload, handler, bufnr) end local function open_uri(uri, tab) @@ -128,38 +117,42 @@ local function cursor_inside_wikilink() 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). +-- Find the word around 0-based cursor `col` in `line`. Returns `left` (0-based +-- index of the char before the word start) and `right` (1-based index one past +-- the word's last char), so `word = line:sub(left + 1, right - 1)`. A "word" is +-- a run of `[%w_-]`. +local function word_boundaries(line, 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 + -- `left` sits one before the word start; nudge forward if it's not on a + -- word char (cursor was already at the very start). + if not line:sub(left + 1, left + 1):match('[%w_-]') then + left = left + 1 end - local right = start_col + 1 + local right = left + 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) + return left, right +end + +local function wrap_cword_as_wikilink() + local cword = vim.fn.expand('') + if cword == '' then return false end + -- Replace just this `` occurrence 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() + local left, right = word_boundaries(line, col) + local word = line:sub(left + 1, right - 1) if word == '' then return false end - local new_line = line:sub(1, start_col) .. '[[' .. word .. ']]' .. line:sub(stop_col + 1) + local new_line = line:sub(1, left) .. '[[' .. word .. ']]' .. line:sub(right) 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 }) + vim.api.nvim_win_set_cursor(0, { row, left + 2 }) return true end @@ -349,9 +342,16 @@ function M.search(args) end if not ext:match('^%.') then ext = '.' .. ext end local glob = root == '' and '**' or (vim.fn.fnameescape(root) .. '**/*' .. ext) - local ok = pcall(vim.cmd, 'lvimgrep /' .. vim.fn.escape(pat, '/') .. '/j ' .. glob) + local ok, err = pcall(vim.cmd, 'lvimgrep /' .. vim.fn.escape(pat, '/') .. '/j ' .. glob) if not ok then - vim.notify('nuwiki: no match for ' .. pat, vim.log.levels.WARN) + -- E480 = no match: a normal "nothing found", not an error. Anything else + -- (bad pattern, unreadable dir) is surfaced verbatim. Mirrors the VimL + -- twin's `catch /E480/`. + if type(err) == 'string' and err:match('E480') then + vim.notify('nuwiki: no match for ' .. pat, vim.log.levels.WARN) + else + vim.notify('nuwiki: ' .. tostring(err), vim.log.levels.ERROR) + end return end vim.cmd('lopen') @@ -979,6 +979,27 @@ function M._table_insert_row_below(row) vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 }) end +-- Parse a list-item line into indent / marker / trailing text. Handles +-- `-`/`*`/`#` bullets and numbered `N.`/`N)` markers (the marker includes the +-- number). `marker` is nil when `line` isn't a list item. +local function parse_list_marker(line) + local indent, marker, after = line:match('^(%s*)([%-%*%#])%s(.*)$') + if not marker then + local n + indent, n, marker, after = line:match('^(%s*)(%d+)([%.%)])%s(.*)$') + if marker then marker = n .. marker end + end + return indent, marker, after +end + +-- True when any auto-indent mechanism is active. nvim then re-inserts the +-- previous line's indent after a returned ``, so we must not also add our +-- own copy (which would push the continuation one level too deep). +local function has_auto_indent() + return vim.bo.autoindent or vim.bo.smartindent or vim.bo.cindent + or (vim.bo.indentexpr ~= nil and vim.bo.indentexpr ~= '') +end + --- Smart `` for insert mode — vimwiki parity for `:VimwikiReturn`. --- Used as an `` keymap so the function is side-effect-free on --- the buffer (textlock is active). The table-row branch hands off to @@ -1002,12 +1023,7 @@ function M.smart_return() end -- List item with marker? - local indent, marker, after = line:match('^(%s*)([%-%*%#])%s(.*)$') - if not marker then - local n - indent, n, marker, after = line:match('^(%s*)(%d+)([%.%)])%s(.*)$') - if marker then marker = n .. marker end - end + local indent, marker, after = parse_list_marker(line) if marker then local cb = after:match('^%[[%sxXoO%.%-]%]%s*(.*)$') local body = cb or after @@ -1017,14 +1033,8 @@ function M.smart_return() -- "break out of the list" behaviour on an empty bullet). return esc .. '0DA' .. cr end - -- nvim defaults `autoindent=on`, which inserts the previous line's - -- indent right after the `` we return. If we then add our own - -- `indent`, the new bullet is pushed one level deeper than the one - -- the user is continuing. Skip our copy whenever any auto-indent - -- mechanism is active and let Vim/Nvim insert the indent for us. - local auto = vim.bo.autoindent or vim.bo.smartindent or vim.bo.cindent - or (vim.bo.indentexpr ~= nil and vim.bo.indentexpr ~= '') - local effective_indent = auto and '' or indent + -- Skip our own indent copy when auto-indent is active (see has_auto_indent). + local effective_indent = has_auto_indent() and '' or indent return cr .. effective_indent .. marker .. ' ' .. (cb and '[ ] ' or '') end return cr @@ -1037,23 +1047,15 @@ function M.smart_shift_return() local line = vim.api.nvim_get_current_line() local cr = vim.api.nvim_replace_termcodes('', true, false, true) if vim.fn.pumvisible() == 1 then return cr end - local indent, marker, after = line:match('^(%s*)([%-%*%#])%s(.*)$') - if not marker then - local n - indent, n, marker, after = line:match('^(%s*)(%d+)([%.%)])%s(.*)$') - if marker then marker = n .. marker end - end + local indent, marker, after = parse_list_marker(line) if not marker then return cr end -- Align under the text: marker width + 1 space (+ 4 for a `[ ] ` checkbox). local has_cb = after:match('^%[[%sxXoO%.%-]%]') ~= nil local pad = #marker + 1 + (has_cb and 4 or 0) - -- Mirror smart_return's auto-indent handling: when an auto-indent mechanism - -- is active, Vim already re-inserts the line's indent after ``. - local auto = vim.bo.autoindent or vim.bo.smartindent or vim.bo.cindent - or (vim.bo.indentexpr ~= nil and vim.bo.indentexpr ~= '') - return cr .. (auto and '' or indent) .. string.rep(' ', pad) + -- Mirror smart_return's auto-indent handling (see has_auto_indent). + return cr .. (has_auto_indent() and '' or indent) .. string.rep(' ', pad) end -- :VimwikiReturn / :NuwikiReturn — the command form of the smart `` @@ -1242,17 +1244,7 @@ function M.colorize(color, visual) local cword = vim.fn.expand('') if cword == '' then return end local col = vim.api.nvim_win_get_cursor(0)[2] - local left = col - while left > 0 and line:sub(left, left):match('[%w_%-]') do - left = left - 1 - end - if not line:sub(left + 1, left + 1):match('[%w_%-]') then - left = left + 1 - end - local right = left + 1 - while right <= #line and line:sub(right, right):match('[%w_%-]') do - right = right + 1 - end + local left, right = word_boundaries(line, col) local before = line:sub(1, left) local word = line:sub(left + 1, right - 1) local after = line:sub(right) diff --git a/lua/nuwiki/ftplugin.lua b/lua/nuwiki/ftplugin.lua index b6dbfbd..d89103a 100644 --- a/lua/nuwiki/ftplugin.lua +++ b/lua/nuwiki/ftplugin.lua @@ -18,16 +18,13 @@ local function setup_folding(bufnr, folding_mode) -- yet attached) degrades to the regex variant instead of spamming -- the user with E5108 errors per line. local apply = function() - if folding_mode == 'expr' - or not (vim.fn.has('nvim-0.11') == 1 and vim.lsp.foldexpr) - then - vim.opt_local.foldmethod = 'expr' + vim.opt_local.foldmethod = 'expr' + vim.opt_local.foldtext = 'v:lua.require("nuwiki.folding").foldtext()' + -- Min Neovim is 0.11, so vim.lsp.foldexpr is the only capability gate. + if folding_mode == 'expr' or not vim.lsp.foldexpr then vim.opt_local.foldexpr = 'v:lua.require("nuwiki.folding").expr()' - vim.opt_local.foldtext = 'v:lua.require("nuwiki.folding").foldtext()' else - vim.opt_local.foldmethod = 'expr' vim.opt_local.foldexpr = 'v:lua.require("nuwiki.folding").lsp_expr()' - vim.opt_local.foldtext = 'v:lua.require("nuwiki.folding").foldtext()' end end diff --git a/lua/nuwiki/install.lua b/lua/nuwiki/install.lua index 6b2e256..88c0f00 100644 --- a/lua/nuwiki/install.lua +++ b/lua/nuwiki/install.lua @@ -53,8 +53,8 @@ local function build_from_source(dest) local root = plugin_root() vim.notify('nuwiki: building nuwiki-ls from source (cargo build --release) …', vim.log.levels.INFO) local rs_repo = vim.fn.fnamemodify(root, ':h:h') .. '/nuwiki-rs' - if not vim.uv then vim.uv = vim.loop end - local rs_exists = vim.uv.fs_stat(rs_repo) + local uv = vim.uv or vim.loop + local rs_exists = uv.fs_stat(rs_repo) if not rs_exists then vim.notify('nuwiki: cloning nuwiki-rs …', vim.log.levels.INFO) local _ = vim.fn.system({ diff --git a/lua/nuwiki/keymaps.lua b/lua/nuwiki/keymaps.lua index 5c80ab5..9e7fdd8 100644 --- a/lua/nuwiki/keymaps.lua +++ b/lua/nuwiki/keymaps.lua @@ -97,30 +97,22 @@ function link.prev() jump_to_pattern([[\[\[]], -1) end -- approximate: when the current line starts with a list marker, insert -- the marker on the new line; otherwise fall back to plain o/O. -local function open_below_with_bullet() +-- `cmd` is 'o' (open below) or 'O' (open above). Off a list item it's a plain +-- o/O. The below ('o') variant also re-adds a `[ ] ` checkbox prefix when the +-- source item has one, so the continuation is a fresh unchecked item; the +-- above ('O') variant inserts a bare bullet (preserves prior behaviour). +local function open_with_bullet(cmd) local line = vim.fn.getline('.') local indent, marker = line:match('^(%s*)([%-%*#])%s') if not marker then - vim.api.nvim_feedkeys('o', 'n', false) + vim.api.nvim_feedkeys(cmd, 'n', false) return end local prefix = indent .. marker .. ' ' - -- Also preserve checkbox if present - local checkbox = line:match('^%s*[%-%*#]%s+(%[[%sxXoO%.%-]%])') - if checkbox then + if cmd == 'o' and line:match('^%s*[%-%*#]%s+(%[[%sxXoO%.%-]%])') then prefix = prefix .. '[ ] ' end - vim.api.nvim_feedkeys('o' .. prefix, 'n', false) -end -local function open_above_with_bullet() - local line = vim.fn.getline('.') - local indent, marker = line:match('^(%s*)([%-%*#])%s') - if not marker then - vim.api.nvim_feedkeys('O', 'n', false) - return - end - local prefix = indent .. marker .. ' ' - vim.api.nvim_feedkeys('O' .. prefix, 'n', false) + vim.api.nvim_feedkeys(cmd .. prefix, 'n', false) end -- ===== Public attach ===== @@ -253,8 +245,8 @@ function M.attach(bufnr, mappings) { desc = 'nuwiki: remove checkbox (current item)' }, bufnr) map('n', 'gL', cmd.list_remove_checkbox_in_list, { desc = 'nuwiki: remove checkboxes (whole list)' }, bufnr) - map('n', 'o', open_below_with_bullet, { desc = 'nuwiki: open below + bullet' }, bufnr) - map('n', 'O', open_above_with_bullet, { desc = 'nuwiki: open above + bullet' }, bufnr) + map('n', 'o', function() open_with_bullet('o') end, { desc = 'nuwiki: open below + bullet' }, bufnr) + map('n', 'O', function() open_with_bullet('O') end, { desc = 'nuwiki: open above + bullet' }, bufnr) -- Insert-mode bindings (matches upstream vimwiki). The list helpers -- mutate the buffer via the LSP / `nvim_buf_set_lines`, both of diff --git a/lua/nuwiki/lsp.lua b/lua/nuwiki/lsp.lua index 4cdef38..5a07b4e 100644 --- a/lua/nuwiki/lsp.lua +++ b/lua/nuwiki/lsp.lua @@ -64,7 +64,9 @@ local function root_dir_for(buf_file) if opts.wikis then local buf_norm = vim.fs.normalize(buf_file) for _, w in ipairs(opts.wikis) do - local root = vim.fn.expand(w.root or '') + -- Normalize both sides so the prefix compare survives mixed path + -- separators (e.g. on Windows); buf_norm is already normalized. + local root = vim.fs.normalize(vim.fn.expand(w.root or '')) if root ~= '' and buf_norm:sub(1, #root) == root then return root end