9 Commits

Author SHA1 Message Date
gffranco 5375e30bda fix(tables): don't skip escaped \| in cell scan — match the server
CI / editor tests (push) Successful in 46s
The server lexer (nuwiki-rs) splits table cells on a backslash-escaped `\|`
(it can't skip it yet without leaving a stray backslash in the output), but
the client's cell_bar_positions was skipping `\|`. That made the editor
auto-align keep `a\|b` as one cell while HTML export split it into two.

Drop the `\|` case from both clients so buffer alignment and export agree.
`[[…]]` / `{{…}}` / `` `code` `` pipes are still treated as literal. A
coordinated `\|` follow-up (server unescaping + client re-adding the skip)
is tracked in nuwiki-rs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:43:15 +00:00
gitea-actions 51e857f3c6 chore(release): 0.5.2
CI / editor tests (push) Successful in 44s
2026-07-02 12:02:20 +00:00
gffranco 4c187fee3c fix(tables): don't split a cell on a | inside a link/code/escape
CI / editor tests (push) Successful in 46s
Table auto-alignment scanned for cell separators with a naive find('|'),
so a cell containing a vimwiki link `[[url|title]]` was split at the link's
internal pipe — mangling both the link (`[[url` / `title]]`) and the table
(a phantom extra column). Same bug in both the Lua (Neovim) and VimL (Vim)
table code, in both table_cell_starts and parse_table_row.

Add a shared cell-separator scanner (cell_bar_positions) that treats `|` as
literal — not a separator — when it's inside `[[…]]` wikilinks, `{{…}}`
transclusions, `` `…` `` inline code, or backslash-escaped (`\|`), matching
vimwiki. Both scan functions in each client now use it.

Regression tests added on both clients (cr.table_link_cell_pipe_not_a_separator);
full harness green (Neovim 308, Vim 302).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:57:16 +00:00
gitea-actions cd173f000c chore(release): 0.5.1
CI / editor tests (push) Successful in 42s
2026-06-29 11:38:14 +00:00
gffranco 4bee216c05 refactor(client): self-review cleanup — dead code, dedup, parity, stale comments
CI / editor tests (push) Successful in 45s
A code-review pass over the VimL + Lua client layers (no behaviour change
beyond the noted parity fixes; full editor harness 10/10):

Lua (lua/nuwiki/):
- commands.lua: drop the dead <0.10 make_position_params pcall/no-arg fallback
  (min is 0.11; no-arg form is itself deprecated) and its stale comment.
- commands.lua: simplify the exec() client dispatch — the metatable/rawget
  dance is gone; always use the non-deprecated colon form client:request() on
  0.11+. (This is the block an external review misread as "critical".)
- commands.lua: extract parse_list_marker() + has_auto_indent() shared by
  smart_return/smart_shift_return; extract word_boundaries() shared by
  wrap_cword_as_wikilink/colorize (and unify the equivalent char classes).
- commands.lua: :Nuwiki search now surfaces real lvimgrep errors instead of
  reporting every failure as "no match" (E480 stays a WARN) — parity with the
  VimL twin's `catch /E480/`.
- keymaps.lua: collapse open_below/above_with_bullet into open_with_bullet(cmd)
  (checkbox prefix preserved for `o` only, as before).
- lsp.lua: normalize the wiki root before the buffer-path prefix compare
  (Windows separators); buf path was already normalized.
- ftplugin.lua: drop the always-true has('nvim-0.11') guard and hoist the
  identical foldmethod/foldtext out of both branches.
- install.lua: use a local `uv` instead of mutating the global vim.uv.

VimL (autoload/nuwiki/):
- commands.vim: refresh a stale comment pointing at the old in-repo
  nuwiki-lsp/src/commands.rs path → the nuwiki-rs server generally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:59:52 +00:00
gffranco 82444f5013 refactor(viml): extract shared heading_level/is_table_row/wiki_cfg; fix cap + spaceless
CI / editor tests (push) Successful in 44s
Mirrors the Lua util refactor. Pulls the copy-pasted VimL helpers into
autoload/nuwiki/util.vim and points the per-file s: stubs at it:

- heading_level (commands/folding/textobjects) — commands.vim's copy was
  missing the >6 level cap the others had (a 7+ `=` line returned 7). The
  unified version caps at 6 AND accepts spaceless headings (==Heading==),
  matching the server lexer + the Lua client (so fold fallback, text
  objects, and ]] navigation recognize spaceless headings too).
- is_table_row (commands/textobjects) — identical copies.
- wiki_cfg (commands/diary) — these had DRIFTED: commands.vim's included a
  `space` (links_space_char) key diary.vim's lacked. Unified to the superset
  so callers reading `.space` keep working; diary callers ignore the extra key.

Also refreshes the stale plugin/nuwiki.vim header ("all logic lives in the
Rust language server" → thin client; server is nuwiki-rs, downloaded).

Verified: 18-case heading battery in headless vim + full editor harness 10/10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:38:04 +00:00
gffranco a9208db2e4 fix(lua): recognize spaceless headings (==Heading==) in client helpers
CI / editor tests (push) Successful in 44s
The shared heading_level helper still required spaces around the title, so
the client-side regex paths — fold fallback, text objects (ah/ih), and
heading navigation (]] etc.) — skipped spaceless headings, even though the
server (and HTML export) accept them since the #7 fix. Rewrite to match the
lexer: balanced leading/trailing `=` runs (cap 6) around a non-empty title,
spaced or spaceless. Marker-only (`======`), unbalanced (`==a==b`, `==> x`)
and 7+ runs still return 0.

Verified against an 18-case battery + full editor harness (10/10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:28:37 +00:00
gffranco ac1db1af1d refactor(lua): extract shared heading_level/is_table_row; dedup wiki_list in search
CI / editor tests (push) Successful in 48s
Pulls the duplicated heading_level (keymaps/folding/textobjects) and
is_table_row (commands/textobjects) helpers into lua/nuwiki/util.lua. The
copies had drifted — only folding capped level >6 and only commands
nil-guarded is_table_row; the unified versions keep both (cap + nil-safe),
which also aligns the keymap/textobject paths with the server's level cap.
search() now resolves config.wiki_list() once instead of twice.

No behaviour change beyond the >6 cap now applying everywhere. Full editor
harness green (10/10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:19:21 +00:00
gffranco 101cb1cea9 ci(release): automate plugin releases via workflow_dispatch
CI / editor tests (push) Successful in 41s
Adds a dispatch-triggered Release workflow for the plugin: enter a version
in the Actions UI and it stamps g:nuwiki_version in plugin/nuwiki.vim,
sanity-checks the plugin still sources (+ exposes the stamped version),
commits chore(release): X.Y.Z, pushes the vX.Y.Z tag, and publishes a Gitea
release with notes. No binary build — the plugin ships none; nuwiki-ls is
fetched from nuwiki-rs at install time. Dispatch-only so the self-created
tag doesn't double-fire.

Updates ONBOARDING (tree, CI/CD, "Releasing the plugin") to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:26:28 +00:00
18 changed files with 465 additions and 242 deletions
+151
View File
@@ -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"
+42 -42
View File
@@ -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,25 +1153,51 @@ 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
" 0-based byte offsets of the cell-separator `|` in `s`. Pipes that are literal
" cell content are skipped: those inside `[[…]]` wikilinks, `{{…}}`
" transclusions, and `` `…` `` inline code. Byte-based (strpart/stridx) to match
" the rest of the table code. Keeps a `[[url|title]]` link in one cell instead
" of splitting on its internal pipe.
"
" Backslash-escaped `\|` is intentionally NOT skipped, to match the server
" lexer (nuwiki-rs). See the nuwiki-rs issue for the coordinated `\|` follow-up.
function! s:cell_bar_positions(s) abort
let l:out = []
let l:i = 0
let l:n = strlen(a:s)
while l:i < l:n
let l:c = strpart(a:s, l:i, 1)
let l:two = strpart(a:s, l:i, 2)
if l:c ==# '`'
let l:close = stridx(a:s, '`', l:i + 1)
let l:i = l:close < 0 ? l:n : l:close + 1
elseif l:two ==# '[['
let l:close = stridx(a:s, ']]', l:i + 2)
let l:i = l:close < 0 ? l:n : l:close + 2
elseif l:two ==# '{{'
let l:close = stridx(a:s, '}}', l:i + 2)
let l:i = l:close < 0 ? l:n : l:close + 2
elseif l:c ==# '|'
call add(l:out, l:i)
let l:i += 1
else
let l:i += 1
endif
endwhile
return l:out
endfunction
function! s:table_cell_starts(line) abort
" 1-based byte positions of each `|`. Cursor lands one past pipe N at
" 1-based byte (starts[N] + 1) — that's the first space inside the
" cell the pipe opens.
let l:out = []
let l:pos = 0
while 1
let l:bar = stridx(a:line, '|', l:pos)
if l:bar < 0 | break | endif
call add(l:out, l:bar + 1)
let l:pos = l:bar + 1
endwhile
return l:out
return map(s:cell_bar_positions(a:line), 'v:val + 1')
endfunction
function! s:find_table_range(row) abort
@@ -1214,15 +1218,11 @@ function! s:parse_table_row(line) abort
let l:body = strpart(a:line, len(l:indent))
let l:segments = []
let l:pos = 0
while 1
let l:bar = stridx(l:body, '|', l:pos)
if l:bar < 0
call add(l:segments, strpart(l:body, l:pos))
break
endif
for l:bar in s:cell_bar_positions(l:body)
call add(l:segments, strpart(l:body, l:pos, l:bar - l:pos))
let l:pos = l:bar + 1
endwhile
endfor
call add(l:segments, strpart(l:body, l:pos))
" Drop the empty piece before the opening `|` and the trailing piece
" after the closing `|`.
if len(l:segments) > 0 && l:segments[0] ==# ''
+1 -13
View File
@@ -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
+1 -9
View File
@@ -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
+2 -10
View File
@@ -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,
+56
View File
@@ -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
View File
@@ -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
+8
View File
@@ -336,6 +336,14 @@ call s:run('cr.adds_new_table_row', {
\ 'keys': "A\<CR>x\<Esc>",
\ 'expect_lines': ['| a | b |', '|x | |'],
\ })
" Regression: a `|` inside `[[u|t]]` must not split the cell — the link stays
" intact and the row keeps 2 columns (fresh row has 2 cells, not 3).
call s:run('cr.table_link_cell_pipe_not_a_separator', {
\ 'lines': ['| a | [[u|t]] |'],
\ 'cursor': [1, 4],
\ 'keys': "A\<CR>x\<Esc>",
\ 'expect_lines': ['| a | [[u|t]] |', '|x | |'],
\ })
" Insert <S-CR>: multiline list item — continue with NO marker, aligned under
" the text ("- " → 2 cols; "- [ ] " → 6 cols).
call s:run('cr.shift_cr_multiline', {
+10
View File
@@ -735,6 +735,16 @@ vim.defer_fn(function()
expect_lines = { '| a | b |', '|x | |' },
wait_ms = 100,
})
run('cr.table_link_cell_pipe_not_a_separator', {
-- Regression: the `|` inside `[[u|t]]` must NOT split the cell, so the
-- link stays intact and the row keeps its 2 columns (a 2-col fresh row,
-- not the mangled 3-col one the old naive `|` scan produced).
lines = { '| a | [[u|t]] |' },
cursor = { 1, 4 },
keys = 'A<CR>x<Esc>',
expect_lines = { '| a | [[u|t]] |', '|x | |' },
wait_ms = 100,
})
-- ===== Insert-mode table navigation (Cluster 6) =====
+108 -96
View File
@@ -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('<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
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')
@@ -818,8 +819,39 @@ 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
local is_table_row = require('nuwiki.util').is_table_row
-- 1-based byte positions of the cell-separator `|` characters in `s`. Pipes
-- that are literal cell content are skipped: those inside `[[…]]` wikilinks,
-- `{{…}}` transclusions, and `` `…` `` inline code. This keeps a
-- `[[url|title]]` link in one cell instead of splitting on its internal pipe.
--
-- Backslash-escaped `\|` is intentionally NOT skipped, to match the server
-- lexer (nuwiki-rs), which can't skip it yet without leaving a stray backslash
-- in the output. See nuwiki-rs issue for the coordinated `\|` follow-up.
local function cell_bar_positions(s)
local out = {}
local i = 1
local n = #s
while i <= n do
local c = s:sub(i, i)
if c == '`' then
local close = s:find('`', i + 1, true)
i = (close or n) + 1
elseif s:sub(i, i + 1) == '[[' then
local close = s:find(']]', i + 2, true)
i = close and close + 2 or n + 1
elseif s:sub(i, i + 1) == '{{' then
local close = s:find('}}', i + 2, true)
i = close and close + 2 or n + 1
elseif c == '|' then
table.insert(out, i)
i = i + 1
else
i = i + 1
end
end
return out
end
local function table_cell_starts(line)
@@ -827,15 +859,7 @@ local function table_cell_starts(line)
-- (starts[N], starts[N+1]); placing the cursor at 0-based byte column
-- `starts[i]` lands one position past the i-th `|` (the first space
-- of the cell that the `|` opens).
local out = {}
local pos = 0
while true do
local bar = line:find('|', pos + 1, true)
if not bar then break end
table.insert(out, bar)
pos = bar
end
return out
return cell_bar_positions(line)
end
local function find_table_range(row)
@@ -857,15 +881,11 @@ local function parse_table_row(line)
local body = line:sub(#indent + 1)
local segments = {}
local pos = 1
while true do
local bar = body:find('|', pos, true)
if not bar then
table.insert(segments, body:sub(pos))
break
end
for _, bar in ipairs(cell_bar_positions(body)) do
table.insert(segments, body:sub(pos, bar - 1))
pos = bar + 1
end
table.insert(segments, body:sub(pos))
-- Drop the empty piece before the opening `|` and the trailing piece
-- after the closing `|`.
if #segments > 0 and segments[1] == '' then table.remove(segments, 1) end
@@ -980,6 +1000,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 +1044,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 +1054,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 +1068,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 +1265,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
View File
@@ -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()
+4 -7
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -10
View File
@@ -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
+43
View File
@@ -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
View File
@@ -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.2'
endif
" Resolve the plugin root NOW, while this script is being sourced and