Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd173f000c | |||
| 4bee216c05 | |||
| 82444f5013 | |||
| a9208db2e4 | |||
| ac1db1af1d | |||
| 101cb1cea9 |
@@ -0,0 +1,151 @@
|
||||
name: Release
|
||||
|
||||
# Cut a plugin release from the Actions UI: "Run workflow" → enter the
|
||||
# version (e.g. 0.5.1). This repo is the Vim/Neovim plugin only and ships no
|
||||
# binary (the nuwiki-ls server lives in nuwiki-rs and is downloaded at install
|
||||
# time), so a release is just: stamp g:nuwiki_version, commit, tag vX.Y.Z, and
|
||||
# publish a Gitea release entry. No build / cross-compile.
|
||||
#
|
||||
# Dispatch-only on purpose: the job creates the tag itself, so a push-tag
|
||||
# trigger would double-fire.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version, no leading v (e.g. 0.5.1)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: tag + release ${{ inputs.version }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Push the release commit + tag back with a write-capable token.
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Validate version + stamp
|
||||
id: stamp
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ver="${VERSION#v}" # tolerate a leading v
|
||||
if ! printf '%s' "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$'; then
|
||||
echo "::error::'$ver' is not a semver version (expected e.g. 0.5.1)"
|
||||
exit 1
|
||||
fi
|
||||
tag="v$ver"
|
||||
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null \
|
||||
|| git ls-remote --exit-code --tags origin "$tag" >/dev/null 2>&1; then
|
||||
echo "::error::tag $tag already exists"
|
||||
exit 1
|
||||
fi
|
||||
# g:nuwiki_version in plugin/nuwiki.vim is the plugin's only version
|
||||
# location (surfaced by :NuwikiShowVersion / health).
|
||||
sed -i -E "s/(let g:nuwiki_version = ')[^']*(')/\1$ver\2/" plugin/nuwiki.vim
|
||||
if ! grep -q "let g:nuwiki_version = '$ver'" plugin/nuwiki.vim; then
|
||||
echo "::error::failed to stamp g:nuwiki_version in plugin/nuwiki.vim"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$ver" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sanity-check the plugin sources cleanly
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends vim
|
||||
# plugin/nuwiki.vim must source without error (silent Ex mode exits
|
||||
# non-zero on any error), and expose the stamped version.
|
||||
vim -e -s -u NONE -N \
|
||||
-c 'set runtimepath+=.' \
|
||||
-c 'runtime plugin/nuwiki.vim' \
|
||||
-c "if get(g:, 'nuwiki_version', '') !=# '${{ steps.stamp.outputs.version }}' | cquit 1 | endif" \
|
||||
-c 'qall!'
|
||||
|
||||
- name: Commit, tag, push
|
||||
env:
|
||||
VER: ${{ steps.stamp.outputs.version }}
|
||||
TAG: ${{ steps.stamp.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "gitea-actions"
|
||||
git config user.email "gitea-actions@users.noreply.code.gfran.co"
|
||||
git add plugin/nuwiki.vim
|
||||
git commit -m "chore(release): $VER"
|
||||
git tag -a "$TAG" -m "nuwiki plugin $VER"
|
||||
git push origin "HEAD:${GITHUB_REF_NAME}"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Ensure jq + curl
|
||||
run: |
|
||||
command -v jq >/dev/null 2>&1 || { apt-get update && apt-get install -y --no-install-recommends jq; }
|
||||
command -v curl >/dev/null 2>&1 || { apt-get update && apt-get install -y --no-install-recommends curl; }
|
||||
|
||||
- name: Generate release notes
|
||||
id: notes
|
||||
env:
|
||||
TAG: ${{ steps.stamp.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force origin >/dev/null 2>&1 || true
|
||||
# Previous tag = newest tag that isn't the one we just cut.
|
||||
prev_tag=$(git tag --sort=-version:refname | grep -vxF "$TAG" | head -n1 || echo "")
|
||||
if [ -n "$prev_tag" ]; then
|
||||
log=$(git log --oneline "$prev_tag..$TAG")
|
||||
compare="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${prev_tag}...${TAG}"
|
||||
else
|
||||
log=$(git log --oneline "$TAG")
|
||||
compare="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commits/tag/${TAG}"
|
||||
fi
|
||||
cat > release-notes.md <<EOF
|
||||
Vim/Neovim plugin release. The \`nuwiki-ls\` server binary is downloaded
|
||||
automatically from [nuwiki-rs](${GITHUB_SERVER_URL}/gffranco/nuwiki-rs)
|
||||
at install time — no binary assets are attached here by design.
|
||||
|
||||
**Full Changelog**: ${compare}
|
||||
|
||||
**Changes**:
|
||||
${log}
|
||||
EOF
|
||||
echo "wrote release notes"
|
||||
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITEA_SERVER: ${{ github.server_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ steps.stamp.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||
echo "::error::RELEASE_TOKEN secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
# Idempotent: skip if a release for this tag already exists.
|
||||
existing=$(curl --silent -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
"$GITEA_SERVER/api/v1/repos/$REPO/releases/tags/$TAG" || true)
|
||||
if [ "$existing" = "200" ]; then
|
||||
echo "release $TAG already exists — nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
notes=$(cat release-notes.md 2>/dev/null || echo "")
|
||||
payload=$(jq -n --arg tag "$TAG" --arg name "$TAG" --arg body "$notes" \
|
||||
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
|
||||
code=$(curl --silent -o /tmp/rel.json -w "%{http_code}" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$GITEA_SERVER/api/v1/repos/$REPO/releases")
|
||||
if [ "$code" != "201" ]; then
|
||||
echo "::error::failed to create release (HTTP $code)"; cat /tmp/rel.json; exit 1
|
||||
fi
|
||||
echo "created release $TAG"
|
||||
@@ -468,15 +468,7 @@ endfunction
|
||||
" ===== Heading navigation (pure VimL) =====
|
||||
|
||||
function! s:heading_level(line) abort
|
||||
let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s')
|
||||
if l:lead ==# ''
|
||||
return 0
|
||||
endif
|
||||
let l:trail = matchstr(a:line, '\s\zs=\+\ze\s*$')
|
||||
if len(l:trail) != len(l:lead)
|
||||
return 0
|
||||
endif
|
||||
return len(l:lead)
|
||||
return nuwiki#util#heading_level(a:line)
|
||||
endfunction
|
||||
|
||||
function! s:current_heading_level() abort
|
||||
@@ -593,21 +585,7 @@ endfunction
|
||||
" upstream g:vimwiki_list both work — and falls back to the scalar g:nuwiki_*
|
||||
" vars (single-wiki shorthand) and finally built-in defaults.
|
||||
function! s:wiki_cfg(n) abort
|
||||
let l:wikis = nuwiki#config#wikis()
|
||||
let l:w = (a:n >= 0 && a:n < len(l:wikis)) ? l:wikis[a:n] : {}
|
||||
let l:root = expand(get(l:w, 'root',
|
||||
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
||||
let l:ext = get(l:w, 'file_extension',
|
||||
\ get(g:, 'nuwiki_file_extension', '.wiki'))
|
||||
if l:ext[0] !=# '.' | let l:ext = '.' . l:ext | endif
|
||||
let l:idx = get(l:w, 'index', 'index')
|
||||
let l:diary_rel = get(l:w, 'diary_rel_path',
|
||||
\ get(g:, 'nuwiki_diary_rel_path', 'diary'))
|
||||
let l:diary_idx = get(l:w, 'diary_index', 'diary')
|
||||
let l:space = get(l:w, 'links_space_char',
|
||||
\ get(g:, 'nuwiki_links_space_char', ' '))
|
||||
return { 'root': l:root, 'ext': l:ext, 'idx': l:idx,
|
||||
\ 'diary_rel': l:diary_rel, 'diary_idx': l:diary_idx, 'space': l:space }
|
||||
return nuwiki#util#wiki_cfg(a:n)
|
||||
endfunction
|
||||
|
||||
" Build the list of configured wikis as {name, root, ext, idx, diary_rel,
|
||||
@@ -1175,10 +1153,10 @@ 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 a:line =~# '^\s*|.*|\s*$'
|
||||
return nuwiki#util#is_table_row(a:line)
|
||||
endfunction
|
||||
|
||||
function! s:table_cell_starts(line) abort
|
||||
|
||||
@@ -106,17 +106,5 @@ endfunction
|
||||
" nuwiki#config#wikis() so g:nuwiki_wikis AND an upstream g:vimwiki_list both
|
||||
" work, falling back to the scalar g:nuwiki_* single-wiki shorthand.
|
||||
function! s:wiki_cfg(n) abort
|
||||
let l:wikis = nuwiki#config#wikis()
|
||||
let l:w = (a:n >= 0 && a:n < len(l:wikis)) ? l:wikis[a:n] : {}
|
||||
let l:root = expand(get(l:w, 'root',
|
||||
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
||||
let l:ext = get(l:w, 'file_extension',
|
||||
\ get(g:, 'nuwiki_file_extension', '.wiki'))
|
||||
if l:ext[0] !=# '.' | let l:ext = '.' . l:ext | endif
|
||||
let l:idx = get(l:w, 'index', 'index')
|
||||
let l:diary_rel = get(l:w, 'diary_rel_path',
|
||||
\ get(g:, 'nuwiki_diary_rel_path', 'diary'))
|
||||
let l:diary_idx = get(l:w, 'diary_index', 'diary')
|
||||
return { 'root': l:root, 'ext': l:ext, 'idx': l:idx,
|
||||
\ 'diary_rel': l:diary_rel, 'diary_idx': l:diary_idx }
|
||||
return nuwiki#util#wiki_cfg(a:n)
|
||||
endfunction
|
||||
|
||||
@@ -13,15 +13,7 @@
|
||||
" end of buffer).
|
||||
|
||||
function! s:heading_level(line) abort
|
||||
let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s')
|
||||
if empty(l:lead) | return 0 | endif
|
||||
let l:lvl = strlen(l:lead)
|
||||
if l:lvl > 6 | return 0 | endif
|
||||
let l:trail = matchstr(a:line, '\s\zs=\+\ze\s*$')
|
||||
if empty(l:trail) || strlen(l:trail) != l:lvl
|
||||
return 0
|
||||
endif
|
||||
return l:lvl
|
||||
return nuwiki#util#heading_level(a:line)
|
||||
endfunction
|
||||
|
||||
function! nuwiki#folding#expr() abort
|
||||
|
||||
@@ -24,15 +24,7 @@
|
||||
" ===== Heading helpers =====
|
||||
|
||||
function! s:heading_level(line) abort
|
||||
let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s')
|
||||
if empty(l:lead) | return 0 | endif
|
||||
let l:lvl = strlen(l:lead)
|
||||
if l:lvl > 6 | return 0 | endif
|
||||
let l:trail = matchstr(a:line, '\s\zs=\+\ze\s*$')
|
||||
if empty(l:trail) || strlen(l:trail) != l:lvl
|
||||
return 0
|
||||
endif
|
||||
return l:lvl
|
||||
return nuwiki#util#heading_level(a:line)
|
||||
endfunction
|
||||
|
||||
" Returns `[start, end]` line numbers, or `[]` if no heading found.
|
||||
@@ -130,7 +122,7 @@ endfunction
|
||||
" ===== Table cell / column =====
|
||||
|
||||
function! s:is_table_row(line) abort
|
||||
return a:line =~# '^\s*|.*|\s*$'
|
||||
return nuwiki#util#is_table_row(a:line)
|
||||
endfunction
|
||||
|
||||
" Returns a list of `[start_col, end_col]` byte positions (1-based,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
" autoload/nuwiki/util.vim — small shared helpers used across the VimL client
|
||||
" layers (commands, folding, textobjects, diary). Keeps the formerly
|
||||
" copy-pasted heading / table-row / wiki-config logic in one place. Mirrors
|
||||
" lua/nuwiki/util.lua so both clients behave identically.
|
||||
|
||||
" Heading level of `line` (1-6), or 0 when it isn't a heading. Mirrors the
|
||||
" server lexer: balanced leading/trailing runs of `=` around a non-empty
|
||||
" title, capped at level 6. Accepts both the spaced (`== H ==`) and spaceless
|
||||
" (`==H==`) forms, matching vimwiki.
|
||||
function! nuwiki#util#heading_level(line) abort
|
||||
let l:lead = matchstr(a:line, '^\s*\zs=\+')
|
||||
if empty(l:lead)
|
||||
return 0
|
||||
endif
|
||||
let l:lvl = strlen(l:lead)
|
||||
if l:lvl > 6
|
||||
return 0
|
||||
endif
|
||||
" Same-length trailing run of `=` at end of line (after optional trailing ws).
|
||||
let l:trail = matchstr(a:line, '=\+\ze\s*$')
|
||||
if empty(l:trail) || strlen(l:trail) != l:lvl
|
||||
return 0
|
||||
endif
|
||||
" Non-empty title between the runs (rules out marker-only lines like
|
||||
" `======`). `.\{-}` is non-greedy so the `=\+` runs keep their length.
|
||||
let l:body = matchstr(a:line, '^\s*=\+\zs.\{-}\ze=\+\s*$')
|
||||
if l:body =~# '^\s*$'
|
||||
return 0
|
||||
endif
|
||||
return l:lvl
|
||||
endfunction
|
||||
|
||||
" True when `line` looks like a table row (`| … |`).
|
||||
function! nuwiki#util#is_table_row(line) abort
|
||||
return a:line =~# '^\s*|.*|\s*$'
|
||||
endfunction
|
||||
|
||||
" Resolve the config dict for wiki #n (0-based), falling back to the scalar
|
||||
" g:nuwiki_* globals. Returns root/ext/idx/diary_rel/diary_idx/space.
|
||||
function! nuwiki#util#wiki_cfg(n) abort
|
||||
let l:wikis = nuwiki#config#wikis()
|
||||
let l:w = (a:n >= 0 && a:n < len(l:wikis)) ? l:wikis[a:n] : {}
|
||||
let l:root = expand(get(l:w, 'root',
|
||||
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
||||
let l:ext = get(l:w, 'file_extension',
|
||||
\ get(g:, 'nuwiki_file_extension', '.wiki'))
|
||||
if l:ext[0] !=# '.' | let l:ext = '.' . l:ext | endif
|
||||
let l:idx = get(l:w, 'index', 'index')
|
||||
let l:diary_rel = get(l:w, 'diary_rel_path',
|
||||
\ get(g:, 'nuwiki_diary_rel_path', 'diary'))
|
||||
let l:diary_idx = get(l:w, 'diary_index', 'diary')
|
||||
let l:space = get(l:w, 'links_space_char',
|
||||
\ get(g:, 'nuwiki_links_space_char', ' '))
|
||||
return { 'root': l:root, 'ext': l:ext, 'idx': l:idx,
|
||||
\ 'diary_rel': l:diary_rel, 'diary_idx': l:diary_idx, 'space': l:space }
|
||||
endfunction
|
||||
+16
-14
@@ -59,11 +59,13 @@ nuwiki/
|
||||
│
|
||||
└── .gitea/
|
||||
└── workflows/
|
||||
└── ci.yaml # Editor (Vim/Neovim) harnesses on every push/PR
|
||||
├── ci.yaml # Editor (Vim/Neovim) harnesses on every push/PR
|
||||
└── release.yaml # Dispatch → stamp version, tag, Gitea release (no build)
|
||||
```
|
||||
|
||||
(The Rust server's build/release workflow lives in the separate
|
||||
[nuwiki-rs](https://code.gfran.co/gffranco/nuwiki-rs) repository.)
|
||||
(The Rust server's *binary* build/cross-compile workflow lives in the separate
|
||||
[nuwiki-rs](https://code.gfran.co/gffranco/nuwiki-rs) repository; this repo's
|
||||
release just tags the plugin.)
|
||||
|
||||
## Server Component
|
||||
|
||||
@@ -200,23 +202,23 @@ To modify the LSP server itself, clone the [nuwiki-rs](https://code.gfran.co/gff
|
||||
## CI/CD
|
||||
|
||||
This repo (editor client) uses Gitea Actions:
|
||||
- CI (`.gitea/workflows/ci.yaml`): Runs editor tests on every push/PR.
|
||||
- CI (`.gitea/workflows/ci.yaml`): Runs the editor harnesses on every push/PR.
|
||||
- Release (`.gitea/workflows/release.yaml`): `workflow_dispatch` with a `version` input — stamps `g:nuwiki_version`, tags `vX.Y.Z`, and publishes a Gitea release. No binary build (the plugin ships none).
|
||||
|
||||
The Rust LSP server lives in [nuwiki-rs](https://code.gfran.co/gffranco/nuwiki-rs) which handles:
|
||||
- Release (`.gitea/workflows/release.yaml`): Triggers on `v*` tag — cross-compiles for Linux targets and creates a Gitea release with downloadable binaries.
|
||||
The Rust LSP server lives in [nuwiki-rs](https://code.gfran.co/gffranco/nuwiki-rs) which has its own release workflow — it cross-compiles `nuwiki-ls` for Linux targets and publishes the downloadable binaries the plugin fetches.
|
||||
|
||||
### Releasing the plugin
|
||||
|
||||
This repo has no release workflow — the plugin is distributed by plugin
|
||||
managers cloning the git repo, so a "release" is just a git tag. There is no
|
||||
automated version stamping (the old `scripts/set-version.sh` moved to
|
||||
nuwiki-rs with the Rust crates), so cut a plugin release by hand:
|
||||
|
||||
1. Bump `g:nuwiki_version` in `plugin/nuwiki.vim`.
|
||||
2. Commit, then `git tag vX.Y.Z && git push --tags`.
|
||||
Releases are cut from the Actions UI — **Release** workflow
|
||||
(`.gitea/workflows/release.yaml`) → **Run workflow** → enter the version
|
||||
(e.g. `0.5.1`). It stamps `g:nuwiki_version` in `plugin/nuwiki.vim`,
|
||||
sanity-checks that the plugin still sources, commits `chore(release): X.Y.Z`,
|
||||
pushes the `vX.Y.Z` tag, and publishes a Gitea release entry. The plugin ships
|
||||
no binary, so the release carries no assets by design.
|
||||
|
||||
The plugin and the `nuwiki-ls` server version independently; the binary is
|
||||
fetched from nuwiki-rs's `latest` release at install time.
|
||||
fetched from nuwiki-rs's `latest` release at install time. To do it by hand
|
||||
instead: bump `g:nuwiki_version`, commit, then `git tag vX.Y.Z && git push --tags`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+71
-80
@@ -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
|
||||
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('<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).
|
||||
-- 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('<cword>')
|
||||
if cword == '' then return false end
|
||||
-- Replace just this `<cword>` 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
|
||||
|
||||
@@ -331,8 +324,9 @@ function M.search(args)
|
||||
end
|
||||
local config = require('nuwiki.config')
|
||||
local file = vim.fn.expand('%:p')
|
||||
local wikis = config.wiki_list()
|
||||
local root, ext = '', '.wiki'
|
||||
for _, w in ipairs(config.wiki_list()) do
|
||||
for _, w in ipairs(wikis) do
|
||||
local wroot = vim.fn.fnamemodify(vim.fn.expand(w.root), ':p')
|
||||
if file:sub(1, #wroot) == wroot then
|
||||
root, ext = wroot, w.ext or ext
|
||||
@@ -340,7 +334,7 @@ function M.search(args)
|
||||
end
|
||||
end
|
||||
if root == '' then
|
||||
local first = config.wiki_list()[1]
|
||||
local first = wikis[1]
|
||||
if first then
|
||||
root = vim.fn.fnamemodify(vim.fn.expand(first.root), ':p')
|
||||
ext = first.ext or ext
|
||||
@@ -348,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
|
||||
-- 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')
|
||||
@@ -818,9 +819,7 @@ end
|
||||
-- *before* placing the cursor, all in a single `vim.schedule` tick.
|
||||
-- The algorithm matches the LSP server's implementation in nuwiki-rs.
|
||||
|
||||
local function is_table_row(line)
|
||||
return line ~= nil and line:match('^%s*|.*|%s*$') ~= nil
|
||||
end
|
||||
local is_table_row = require('nuwiki.util').is_table_row
|
||||
|
||||
local function table_cell_starts(line)
|
||||
-- Returns 1-based byte positions of each `|` separator. Cell N spans
|
||||
@@ -980,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 `<CR>`, 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 `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`.
|
||||
--- Used as an `<expr>` keymap so the function is side-effect-free on
|
||||
--- the buffer (textlock is active). The table-row branch hands off to
|
||||
@@ -1003,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
|
||||
@@ -1018,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 `<CR>` 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
|
||||
@@ -1038,23 +1047,15 @@ function M.smart_shift_return()
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local cr = vim.api.nvim_replace_termcodes('<CR>', 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 `<CR>`.
|
||||
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 `<CR>`
|
||||
@@ -1243,17 +1244,7 @@ function M.colorize(color, visual)
|
||||
local cword = vim.fn.expand('<cword>')
|
||||
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)
|
||||
|
||||
+1
-10
@@ -9,16 +9,7 @@
|
||||
local M = {}
|
||||
|
||||
--- Heading level from one line of source. Returns 0 for non-headings.
|
||||
local function heading_level(line)
|
||||
local lead = line:match('^%s*(=+)%s')
|
||||
if not lead then return 0 end
|
||||
local lvl = #lead
|
||||
if lvl > 6 then return 0 end
|
||||
-- Require a matching trailing run of `=`s.
|
||||
local trail = line:match('%s(=+)%s*$')
|
||||
if not trail or #trail ~= lvl then return 0 end
|
||||
return lvl
|
||||
end
|
||||
local heading_level = require('nuwiki.util').heading_level
|
||||
|
||||
--- `foldexpr` body. Returns a fold-marker string per `:help fold-expr`.
|
||||
function M.expr()
|
||||
|
||||
@@ -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.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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
+11
-25
@@ -29,13 +29,7 @@ end
|
||||
-- vimwiki's `vimwiki#base#goto_*_header` family does, just simpler:
|
||||
-- match `^\s*=` runs as heading lines.
|
||||
|
||||
local function heading_level(line)
|
||||
local lead = line and line:match('^%s*(=+)%s')
|
||||
if not lead then return 0 end
|
||||
local trail = line:match('%s(=+)%s*$')
|
||||
if not trail or #trail ~= #lead then return 0 end
|
||||
return #lead
|
||||
end
|
||||
local heading_level = require('nuwiki.util').heading_level
|
||||
|
||||
local function jump_heading(direction, predicate)
|
||||
local total = vim.api.nvim_buf_line_count(0)
|
||||
@@ -103,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 =====
|
||||
@@ -259,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
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -15,13 +15,7 @@ local M = {}
|
||||
|
||||
-- ===== Heading helpers =====
|
||||
|
||||
local function heading_level(line)
|
||||
local lead = line and line:match('^%s*(=+)%s')
|
||||
if not lead then return 0 end
|
||||
local trail = line:match('%s(=+)%s*$')
|
||||
if not trail or #trail ~= #lead then return 0 end
|
||||
return #lead
|
||||
end
|
||||
local heading_level = require('nuwiki.util').heading_level
|
||||
|
||||
--- Find the start/end of the heading block at `lnum`.
|
||||
--- `with_subtree` controls whether we descend into sub-headings:
|
||||
@@ -123,9 +117,7 @@ end
|
||||
|
||||
-- ===== Table cell / column =====
|
||||
|
||||
local function is_table_row(line)
|
||||
return line:match('^%s*|.*|%s*$') ~= nil
|
||||
end
|
||||
local is_table_row = require('nuwiki.util').is_table_row
|
||||
|
||||
--- Compute the byte ranges of cells on a `|`-delimited row.
|
||||
--- Returns a list of `{start_byte, end_byte}` 1-based inclusive of the
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
-- lua/nuwiki/util.lua — small pure-text helpers shared across the client
|
||||
-- layers (keymaps, folding, textobjects, commands). Dependency-free so any
|
||||
-- module can require it without load-order concerns.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Heading level of `line` (1-6), or 0 when it isn't a heading. Mirrors the
|
||||
-- server lexer: balanced leading/trailing runs of `=` around non-empty text,
|
||||
-- capped at level 6 (a 7+ `=` run is not a heading). Both the spaced
|
||||
-- (`== H ==`) and spaceless (`==H==`) forms are accepted, matching vimwiki.
|
||||
-- `nil`-safe.
|
||||
function M.heading_level(line)
|
||||
if not line then
|
||||
return 0
|
||||
end
|
||||
local lead = line:match('^%s*(=+)')
|
||||
if not lead then
|
||||
return 0
|
||||
end
|
||||
local lvl = #lead
|
||||
if lvl > 6 then
|
||||
return 0
|
||||
end
|
||||
-- Same-length trailing run of `=` at end of line (after optional trailing ws).
|
||||
local trail = line:match('(=+)%s*$')
|
||||
if not trail or #trail ~= lvl then
|
||||
return 0
|
||||
end
|
||||
-- Non-empty title between the two runs (rules out marker-only lines like
|
||||
-- `======`). `(.-)` is lazy, so the leading/trailing `=+` keep their runs.
|
||||
local body = line:match('^%s*=+(.-)=+%s*$')
|
||||
if not body or body:match('^%s*$') then
|
||||
return 0
|
||||
end
|
||||
return lvl
|
||||
end
|
||||
|
||||
-- True when `line` looks like a table row (`| … |`). `nil`-safe.
|
||||
function M.is_table_row(line)
|
||||
return line ~= nil and line:match('^%s*|.*|%s*$') ~= nil
|
||||
end
|
||||
|
||||
return M
|
||||
+4
-3
@@ -4,8 +4,9 @@
|
||||
" their config (lazy.nvim does this automatically through `opts`). On Vim
|
||||
" we start the LSP client when a vimwiki buffer is loaded.
|
||||
"
|
||||
" This file contains wiring only — all logic lives in
|
||||
" the Rust language server.
|
||||
" This file contains wiring only. The editor layers (VimL + Lua) are thin
|
||||
" clients; the heavy lifting is done by the nuwiki-ls language server, which
|
||||
" is built from the separate nuwiki-rs repo and downloaded at install time.
|
||||
|
||||
if exists('g:loaded_nuwiki')
|
||||
finish
|
||||
@@ -15,7 +16,7 @@ let g:loaded_nuwiki = 1
|
||||
" Plugin version, surfaced by :VimwikiShowVersion / :NuwikiShowVersion.
|
||||
" Set before the Neovim early-return below so both clients see the global.
|
||||
if !exists('g:nuwiki_version')
|
||||
let g:nuwiki_version = '0.5.0'
|
||||
let g:nuwiki_version = '0.5.1'
|
||||
endif
|
||||
|
||||
" Resolve the plugin root NOW, while this script is being sourced and
|
||||
|
||||
Reference in New Issue
Block a user