Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5375e30bda | |||
| 51e857f3c6 | |||
| 4c187fee3c | |||
| 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) =====
|
" ===== Heading navigation (pure VimL) =====
|
||||||
|
|
||||||
function! s:heading_level(line) abort
|
function! s:heading_level(line) abort
|
||||||
let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s')
|
return nuwiki#util#heading_level(a:line)
|
||||||
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)
|
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:current_heading_level() abort
|
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_*
|
" upstream g:vimwiki_list both work — and falls back to the scalar g:nuwiki_*
|
||||||
" vars (single-wiki shorthand) and finally built-in defaults.
|
" vars (single-wiki shorthand) and finally built-in defaults.
|
||||||
function! s:wiki_cfg(n) abort
|
function! s:wiki_cfg(n) abort
|
||||||
let l:wikis = nuwiki#config#wikis()
|
return nuwiki#util#wiki_cfg(a:n)
|
||||||
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
|
endfunction
|
||||||
|
|
||||||
" Build the list of configured wikis as {name, root, ext, idx, diary_rel,
|
" 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
|
" Mirrors `lua/nuwiki/commands.lua`'s `align_table_at` so plain Vim
|
||||||
" buffers get the same "tighten columns as cells grow" behaviour the
|
" buffers get the same "tighten columns as cells grow" behaviour the
|
||||||
" Neovim path enjoys, without needing a vim-lsp roundtrip. Algorithm
|
" 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
|
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
|
endfunction
|
||||||
|
|
||||||
function! s:table_cell_starts(line) abort
|
function! s:table_cell_starts(line) abort
|
||||||
" 1-based byte positions of each `|`. Cursor lands one past pipe N at
|
" 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
|
" 1-based byte (starts[N] + 1) — that's the first space inside the
|
||||||
" cell the pipe opens.
|
" cell the pipe opens.
|
||||||
let l:out = []
|
return map(s:cell_bar_positions(a:line), 'v:val + 1')
|
||||||
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
|
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:find_table_range(row) abort
|
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:body = strpart(a:line, len(l:indent))
|
||||||
let l:segments = []
|
let l:segments = []
|
||||||
let l:pos = 0
|
let l:pos = 0
|
||||||
while 1
|
for l:bar in s:cell_bar_positions(l:body)
|
||||||
let l:bar = stridx(l:body, '|', l:pos)
|
|
||||||
if l:bar < 0
|
|
||||||
call add(l:segments, strpart(l:body, l:pos))
|
|
||||||
break
|
|
||||||
endif
|
|
||||||
call add(l:segments, strpart(l:body, l:pos, l:bar - l:pos))
|
call add(l:segments, strpart(l:body, l:pos, l:bar - l:pos))
|
||||||
let l:pos = l:bar + 1
|
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
|
" Drop the empty piece before the opening `|` and the trailing piece
|
||||||
" after the closing `|`.
|
" after the closing `|`.
|
||||||
if len(l:segments) > 0 && l:segments[0] ==# ''
|
if len(l:segments) > 0 && l:segments[0] ==# ''
|
||||||
|
|||||||
@@ -106,17 +106,5 @@ endfunction
|
|||||||
" nuwiki#config#wikis() so g:nuwiki_wikis AND an upstream g:vimwiki_list both
|
" 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.
|
" work, falling back to the scalar g:nuwiki_* single-wiki shorthand.
|
||||||
function! s:wiki_cfg(n) abort
|
function! s:wiki_cfg(n) abort
|
||||||
let l:wikis = nuwiki#config#wikis()
|
return nuwiki#util#wiki_cfg(a:n)
|
||||||
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 }
|
|
||||||
endfunction
|
endfunction
|
||||||
|
|||||||
@@ -13,15 +13,7 @@
|
|||||||
" end of buffer).
|
" end of buffer).
|
||||||
|
|
||||||
function! s:heading_level(line) abort
|
function! s:heading_level(line) abort
|
||||||
let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s')
|
return nuwiki#util#heading_level(a:line)
|
||||||
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
|
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#folding#expr() abort
|
function! nuwiki#folding#expr() abort
|
||||||
|
|||||||
@@ -24,15 +24,7 @@
|
|||||||
" ===== Heading helpers =====
|
" ===== Heading helpers =====
|
||||||
|
|
||||||
function! s:heading_level(line) abort
|
function! s:heading_level(line) abort
|
||||||
let l:lead = matchstr(a:line, '^\s*\zs=\+\ze\s')
|
return nuwiki#util#heading_level(a:line)
|
||||||
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
|
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Returns `[start, end]` line numbers, or `[]` if no heading found.
|
" Returns `[start, end]` line numbers, or `[]` if no heading found.
|
||||||
@@ -130,7 +122,7 @@ endfunction
|
|||||||
" ===== Table cell / column =====
|
" ===== Table cell / column =====
|
||||||
|
|
||||||
function! s:is_table_row(line) abort
|
function! s:is_table_row(line) abort
|
||||||
return a:line =~# '^\s*|.*|\s*$'
|
return nuwiki#util#is_table_row(a:line)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Returns a list of `[start_col, end_col]` byte positions (1-based,
|
" 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/
|
└── .gitea/
|
||||||
└── workflows/
|
└── 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
|
(The Rust server's *binary* build/cross-compile workflow lives in the separate
|
||||||
[nuwiki-rs](https://code.gfran.co/gffranco/nuwiki-rs) repository.)
|
[nuwiki-rs](https://code.gfran.co/gffranco/nuwiki-rs) repository; this repo's
|
||||||
|
release just tags the plugin.)
|
||||||
|
|
||||||
## Server Component
|
## Server Component
|
||||||
|
|
||||||
@@ -200,23 +202,23 @@ To modify the LSP server itself, clone the [nuwiki-rs](https://code.gfran.co/gff
|
|||||||
## CI/CD
|
## CI/CD
|
||||||
|
|
||||||
This repo (editor client) uses Gitea Actions:
|
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:
|
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.
|
||||||
- Release (`.gitea/workflows/release.yaml`): Triggers on `v*` tag — cross-compiles for Linux targets and creates a Gitea release with downloadable binaries.
|
|
||||||
|
|
||||||
### Releasing the plugin
|
### Releasing the plugin
|
||||||
|
|
||||||
This repo has no release workflow — the plugin is distributed by plugin
|
Releases are cut from the Actions UI — **Release** workflow
|
||||||
managers cloning the git repo, so a "release" is just a git tag. There is no
|
(`.gitea/workflows/release.yaml`) → **Run workflow** → enter the version
|
||||||
automated version stamping (the old `scripts/set-version.sh` moved to
|
(e.g. `0.5.1`). It stamps `g:nuwiki_version` in `plugin/nuwiki.vim`,
|
||||||
nuwiki-rs with the Rust crates), so cut a plugin release by hand:
|
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
|
||||||
1. Bump `g:nuwiki_version` in `plugin/nuwiki.vim`.
|
no binary, so the release carries no assets by design.
|
||||||
2. Commit, then `git tag vX.Y.Z && git push --tags`.
|
|
||||||
|
|
||||||
The plugin and the `nuwiki-ls` server version independently; the binary is
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,14 @@ call s:run('cr.adds_new_table_row', {
|
|||||||
\ 'keys': "A\<CR>x\<Esc>",
|
\ 'keys': "A\<CR>x\<Esc>",
|
||||||
\ 'expect_lines': ['| a | b |', '|x | |'],
|
\ '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
|
" Insert <S-CR>: multiline list item — continue with NO marker, aligned under
|
||||||
" the text ("- " → 2 cols; "- [ ] " → 6 cols).
|
" the text ("- " → 2 cols; "- [ ] " → 6 cols).
|
||||||
call s:run('cr.shift_cr_multiline', {
|
call s:run('cr.shift_cr_multiline', {
|
||||||
|
|||||||
@@ -735,6 +735,16 @@ vim.defer_fn(function()
|
|||||||
expect_lines = { '| a | b |', '|x | |' },
|
expect_lines = { '| a | b |', '|x | |' },
|
||||||
wait_ms = 100,
|
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) =====
|
-- ===== Insert-mode table navigation (Cluster 6) =====
|
||||||
|
|
||||||
|
|||||||
+108
-96
@@ -24,13 +24,10 @@ local function find_client()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function position_params(client)
|
local function position_params(client)
|
||||||
-- Neovim 0.12+ requires `position_encoding` explicitly; older
|
-- Neovim 0.11+ requires the position encoding explicitly (the no-arg form is
|
||||||
-- versions accept the no-arg form.
|
-- deprecated); pass the current window + the client's negotiated encoding.
|
||||||
local encoding = client and client.offset_encoding or 'utf-16'
|
local encoding = client and client.offset_encoding or 'utf-16'
|
||||||
local ok, params = pcall(vim.lsp.util.make_position_params, 0, encoding)
|
return 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()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function exec(command, arguments, on_result)
|
local function exec(command, arguments, on_result)
|
||||||
@@ -51,21 +48,13 @@ local function exec(command, arguments, on_result)
|
|||||||
end
|
end
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
local payload = { command = command, arguments = arguments or {} }
|
local payload = { command = command, arguments = arguments or {} }
|
||||||
-- Neovim 0.12 deprecates `client.request(...)` in favour of the
|
-- The method form `client:request(...)` is the correct, non-deprecated call
|
||||||
-- method form `client:request(...)`. Prefer the method, fall back
|
-- on every supported Neovim (0.11+); the old `client.request(...)` function
|
||||||
-- on older Neovim where it isn't defined yet.
|
-- form was deprecated in its favour.
|
||||||
local req = client.request
|
if type(client.request) ~= 'function' then
|
||||||
if type(req) ~= 'function' then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if rawget(getmetatable(client) or {}, '__index') and client.request ~= nil then
|
client:request('workspace/executeCommand', payload, handler, bufnr)
|
||||||
-- 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
|
end
|
||||||
|
|
||||||
local function open_uri(uri, tab)
|
local function open_uri(uri, tab)
|
||||||
@@ -128,38 +117,42 @@ local function cursor_inside_wikilink()
|
|||||||
return open_at ~= nil
|
return open_at ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function wrap_cword_as_wikilink()
|
-- Find the word around 0-based cursor `col` in `line`. Returns `left` (0-based
|
||||||
local cword = vim.fn.expand('<cword>')
|
-- index of the char before the word start) and `right` (1-based index one past
|
||||||
if cword == '' then return false end
|
-- the word's last char), so `word = line:sub(left + 1, right - 1)`. A "word" is
|
||||||
-- Replace just this `<cword>` occurrence using `*` (the start of
|
-- a run of `[%w_-]`.
|
||||||
-- the last cursor match) — `ciw[[<C-r>"]]` style, but we do it
|
local function word_boundaries(line, col)
|
||||||
-- via the API so we don't depend on register state.
|
|
||||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
|
||||||
local line = vim.api.nvim_get_current_line()
|
|
||||||
-- Locate the word boundaries around cursor (0-based col).
|
|
||||||
local left = col
|
local left = col
|
||||||
while left > 0 and line:sub(left, left):match('[%w_-]') do
|
while left > 0 and line:sub(left, left):match('[%w_-]') do
|
||||||
left = left - 1
|
left = left - 1
|
||||||
end
|
end
|
||||||
-- `left` now sits one before the word start (or at -1 / col=0 if at start).
|
-- `left` sits one before the word start; nudge forward if it's not on a
|
||||||
local start_col = left
|
-- word char (cursor was already at the very start).
|
||||||
if line:sub(left + 1, left + 1):match('[%w_-]') then
|
if not line:sub(left + 1, left + 1):match('[%w_-]') then
|
||||||
start_col = left
|
left = left + 1
|
||||||
else
|
|
||||||
start_col = left + 1
|
|
||||||
end
|
end
|
||||||
local right = start_col + 1
|
local right = left + 1
|
||||||
while right <= #line and line:sub(right, right):match('[%w_-]') do
|
while right <= #line and line:sub(right, right):match('[%w_-]') do
|
||||||
right = right + 1
|
right = right + 1
|
||||||
end
|
end
|
||||||
local stop_col = right - 1
|
return left, right
|
||||||
local word = line:sub(start_col + 1, stop_col)
|
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
|
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)
|
vim.api.nvim_set_current_line(new_line)
|
||||||
-- Put cursor inside the new `[[…]]` so the LSP definition request
|
-- Put cursor inside the new `[[…]]` so the LSP definition request
|
||||||
-- lands on the wikilink.
|
-- 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
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -331,8 +324,9 @@ function M.search(args)
|
|||||||
end
|
end
|
||||||
local config = require('nuwiki.config')
|
local config = require('nuwiki.config')
|
||||||
local file = vim.fn.expand('%:p')
|
local file = vim.fn.expand('%:p')
|
||||||
|
local wikis = config.wiki_list()
|
||||||
local root, ext = '', '.wiki'
|
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')
|
local wroot = vim.fn.fnamemodify(vim.fn.expand(w.root), ':p')
|
||||||
if file:sub(1, #wroot) == wroot then
|
if file:sub(1, #wroot) == wroot then
|
||||||
root, ext = wroot, w.ext or ext
|
root, ext = wroot, w.ext or ext
|
||||||
@@ -340,7 +334,7 @@ function M.search(args)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if root == '' then
|
if root == '' then
|
||||||
local first = config.wiki_list()[1]
|
local first = wikis[1]
|
||||||
if first then
|
if first then
|
||||||
root = vim.fn.fnamemodify(vim.fn.expand(first.root), ':p')
|
root = vim.fn.fnamemodify(vim.fn.expand(first.root), ':p')
|
||||||
ext = first.ext or ext
|
ext = first.ext or ext
|
||||||
@@ -348,9 +342,16 @@ function M.search(args)
|
|||||||
end
|
end
|
||||||
if not ext:match('^%.') then ext = '.' .. ext end
|
if not ext:match('^%.') then ext = '.' .. ext end
|
||||||
local glob = root == '' and '**' or (vim.fn.fnameescape(root) .. '**/*' .. ext)
|
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
|
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
|
return
|
||||||
end
|
end
|
||||||
vim.cmd('lopen')
|
vim.cmd('lopen')
|
||||||
@@ -818,8 +819,39 @@ end
|
|||||||
-- *before* placing the cursor, all in a single `vim.schedule` tick.
|
-- *before* placing the cursor, all in a single `vim.schedule` tick.
|
||||||
-- The algorithm matches the LSP server's implementation in nuwiki-rs.
|
-- The algorithm matches the LSP server's implementation in nuwiki-rs.
|
||||||
|
|
||||||
local function is_table_row(line)
|
local is_table_row = require('nuwiki.util').is_table_row
|
||||||
return line ~= nil and line:match('^%s*|.*|%s*$') ~= nil
|
|
||||||
|
-- 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
|
end
|
||||||
|
|
||||||
local function table_cell_starts(line)
|
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[N], starts[N+1]); placing the cursor at 0-based byte column
|
||||||
-- `starts[i]` lands one position past the i-th `|` (the first space
|
-- `starts[i]` lands one position past the i-th `|` (the first space
|
||||||
-- of the cell that the `|` opens).
|
-- of the cell that the `|` opens).
|
||||||
local out = {}
|
return cell_bar_positions(line)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function find_table_range(row)
|
local function find_table_range(row)
|
||||||
@@ -857,15 +881,11 @@ local function parse_table_row(line)
|
|||||||
local body = line:sub(#indent + 1)
|
local body = line:sub(#indent + 1)
|
||||||
local segments = {}
|
local segments = {}
|
||||||
local pos = 1
|
local pos = 1
|
||||||
while true do
|
for _, bar in ipairs(cell_bar_positions(body)) do
|
||||||
local bar = body:find('|', pos, true)
|
|
||||||
if not bar then
|
|
||||||
table.insert(segments, body:sub(pos))
|
|
||||||
break
|
|
||||||
end
|
|
||||||
table.insert(segments, body:sub(pos, bar - 1))
|
table.insert(segments, body:sub(pos, bar - 1))
|
||||||
pos = bar + 1
|
pos = bar + 1
|
||||||
end
|
end
|
||||||
|
table.insert(segments, body:sub(pos))
|
||||||
-- Drop the empty piece before the opening `|` and the trailing piece
|
-- Drop the empty piece before the opening `|` and the trailing piece
|
||||||
-- after the closing `|`.
|
-- after the closing `|`.
|
||||||
if #segments > 0 and segments[1] == '' then table.remove(segments, 1) end
|
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 })
|
vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
|
||||||
end
|
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`.
|
--- Smart `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`.
|
||||||
--- Used as an `<expr>` keymap so the function is side-effect-free on
|
--- 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
|
--- the buffer (textlock is active). The table-row branch hands off to
|
||||||
@@ -1003,12 +1044,7 @@ function M.smart_return()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- List item with marker?
|
-- List item with marker?
|
||||||
local indent, marker, after = line:match('^(%s*)([%-%*%#])%s(.*)$')
|
local indent, marker, after = parse_list_marker(line)
|
||||||
if not marker then
|
|
||||||
local n
|
|
||||||
indent, n, marker, after = line:match('^(%s*)(%d+)([%.%)])%s(.*)$')
|
|
||||||
if marker then marker = n .. marker end
|
|
||||||
end
|
|
||||||
if marker then
|
if marker then
|
||||||
local cb = after:match('^%[[%sxXoO%.%-]%]%s*(.*)$')
|
local cb = after:match('^%[[%sxXoO%.%-]%]%s*(.*)$')
|
||||||
local body = cb or after
|
local body = cb or after
|
||||||
@@ -1018,14 +1054,8 @@ function M.smart_return()
|
|||||||
-- "break out of the list" behaviour on an empty bullet).
|
-- "break out of the list" behaviour on an empty bullet).
|
||||||
return esc .. '0DA' .. cr
|
return esc .. '0DA' .. cr
|
||||||
end
|
end
|
||||||
-- nvim defaults `autoindent=on`, which inserts the previous line's
|
-- Skip our own indent copy when auto-indent is active (see has_auto_indent).
|
||||||
-- indent right after the `<CR>` we return. If we then add our own
|
local effective_indent = has_auto_indent() and '' or indent
|
||||||
-- `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
|
|
||||||
return cr .. effective_indent .. marker .. ' ' .. (cb and '[ ] ' or '')
|
return cr .. effective_indent .. marker .. ' ' .. (cb and '[ ] ' or '')
|
||||||
end
|
end
|
||||||
return cr
|
return cr
|
||||||
@@ -1038,23 +1068,15 @@ function M.smart_shift_return()
|
|||||||
local line = vim.api.nvim_get_current_line()
|
local line = vim.api.nvim_get_current_line()
|
||||||
local cr = vim.api.nvim_replace_termcodes('<CR>', true, false, true)
|
local cr = vim.api.nvim_replace_termcodes('<CR>', true, false, true)
|
||||||
if vim.fn.pumvisible() == 1 then return cr end
|
if vim.fn.pumvisible() == 1 then return cr end
|
||||||
local indent, marker, after = line:match('^(%s*)([%-%*%#])%s(.*)$')
|
local indent, marker, after = parse_list_marker(line)
|
||||||
if not marker then
|
|
||||||
local n
|
|
||||||
indent, n, marker, after = line:match('^(%s*)(%d+)([%.%)])%s(.*)$')
|
|
||||||
if marker then marker = n .. marker end
|
|
||||||
end
|
|
||||||
if not marker then
|
if not marker then
|
||||||
return cr
|
return cr
|
||||||
end
|
end
|
||||||
-- Align under the text: marker width + 1 space (+ 4 for a `[ ] ` checkbox).
|
-- Align under the text: marker width + 1 space (+ 4 for a `[ ] ` checkbox).
|
||||||
local has_cb = after:match('^%[[%sxXoO%.%-]%]') ~= nil
|
local has_cb = after:match('^%[[%sxXoO%.%-]%]') ~= nil
|
||||||
local pad = #marker + 1 + (has_cb and 4 or 0)
|
local pad = #marker + 1 + (has_cb and 4 or 0)
|
||||||
-- Mirror smart_return's auto-indent handling: when an auto-indent mechanism
|
-- Mirror smart_return's auto-indent handling (see has_auto_indent).
|
||||||
-- is active, Vim already re-inserts the line's indent after `<CR>`.
|
return cr .. (has_auto_indent() and '' or indent) .. string.rep(' ', pad)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- :VimwikiReturn / :NuwikiReturn — the command form of the smart `<CR>`
|
-- :VimwikiReturn / :NuwikiReturn — the command form of the smart `<CR>`
|
||||||
@@ -1243,17 +1265,7 @@ function M.colorize(color, visual)
|
|||||||
local cword = vim.fn.expand('<cword>')
|
local cword = vim.fn.expand('<cword>')
|
||||||
if cword == '' then return end
|
if cword == '' then return end
|
||||||
local col = vim.api.nvim_win_get_cursor(0)[2]
|
local col = vim.api.nvim_win_get_cursor(0)[2]
|
||||||
local left = col
|
local left, right = word_boundaries(line, 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 before = line:sub(1, left)
|
local before = line:sub(1, left)
|
||||||
local word = line:sub(left + 1, right - 1)
|
local word = line:sub(left + 1, right - 1)
|
||||||
local after = line:sub(right)
|
local after = line:sub(right)
|
||||||
|
|||||||
+1
-10
@@ -9,16 +9,7 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
--- Heading level from one line of source. Returns 0 for non-headings.
|
--- Heading level from one line of source. Returns 0 for non-headings.
|
||||||
local function heading_level(line)
|
local heading_level = require('nuwiki.util').heading_level
|
||||||
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
|
|
||||||
|
|
||||||
--- `foldexpr` body. Returns a fold-marker string per `:help fold-expr`.
|
--- `foldexpr` body. Returns a fold-marker string per `:help fold-expr`.
|
||||||
function M.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
|
-- yet attached) degrades to the regex variant instead of spamming
|
||||||
-- the user with E5108 errors per line.
|
-- the user with E5108 errors per line.
|
||||||
local apply = function()
|
local apply = function()
|
||||||
if folding_mode == 'expr'
|
vim.opt_local.foldmethod = 'expr'
|
||||||
or not (vim.fn.has('nvim-0.11') == 1 and vim.lsp.foldexpr)
|
vim.opt_local.foldtext = 'v:lua.require("nuwiki.folding").foldtext()'
|
||||||
then
|
-- Min Neovim is 0.11, so vim.lsp.foldexpr is the only capability gate.
|
||||||
vim.opt_local.foldmethod = 'expr'
|
if folding_mode == 'expr' or not vim.lsp.foldexpr then
|
||||||
vim.opt_local.foldexpr = 'v:lua.require("nuwiki.folding").expr()'
|
vim.opt_local.foldexpr = 'v:lua.require("nuwiki.folding").expr()'
|
||||||
vim.opt_local.foldtext = 'v:lua.require("nuwiki.folding").foldtext()'
|
|
||||||
else
|
else
|
||||||
vim.opt_local.foldmethod = 'expr'
|
|
||||||
vim.opt_local.foldexpr = 'v:lua.require("nuwiki.folding").lsp_expr()'
|
vim.opt_local.foldexpr = 'v:lua.require("nuwiki.folding").lsp_expr()'
|
||||||
vim.opt_local.foldtext = 'v:lua.require("nuwiki.folding").foldtext()'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ local function build_from_source(dest)
|
|||||||
local root = plugin_root()
|
local root = plugin_root()
|
||||||
vim.notify('nuwiki: building nuwiki-ls from source (cargo build --release) …', vim.log.levels.INFO)
|
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'
|
local rs_repo = vim.fn.fnamemodify(root, ':h:h') .. '/nuwiki-rs'
|
||||||
if not vim.uv then vim.uv = vim.loop end
|
local uv = vim.uv or vim.loop
|
||||||
local rs_exists = vim.uv.fs_stat(rs_repo)
|
local rs_exists = uv.fs_stat(rs_repo)
|
||||||
if not rs_exists then
|
if not rs_exists then
|
||||||
vim.notify('nuwiki: cloning nuwiki-rs …', vim.log.levels.INFO)
|
vim.notify('nuwiki: cloning nuwiki-rs …', vim.log.levels.INFO)
|
||||||
local _ = vim.fn.system({
|
local _ = vim.fn.system({
|
||||||
|
|||||||
+11
-25
@@ -29,13 +29,7 @@ end
|
|||||||
-- vimwiki's `vimwiki#base#goto_*_header` family does, just simpler:
|
-- vimwiki's `vimwiki#base#goto_*_header` family does, just simpler:
|
||||||
-- match `^\s*=` runs as heading lines.
|
-- match `^\s*=` runs as heading lines.
|
||||||
|
|
||||||
local function heading_level(line)
|
local heading_level = require('nuwiki.util').heading_level
|
||||||
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 function jump_heading(direction, predicate)
|
local function jump_heading(direction, predicate)
|
||||||
local total = vim.api.nvim_buf_line_count(0)
|
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
|
-- approximate: when the current line starts with a list marker, insert
|
||||||
-- the marker on the new line; otherwise fall back to plain o/O.
|
-- 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 line = vim.fn.getline('.')
|
||||||
local indent, marker = line:match('^(%s*)([%-%*#])%s')
|
local indent, marker = line:match('^(%s*)([%-%*#])%s')
|
||||||
if not marker then
|
if not marker then
|
||||||
vim.api.nvim_feedkeys('o', 'n', false)
|
vim.api.nvim_feedkeys(cmd, 'n', false)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local prefix = indent .. marker .. ' '
|
local prefix = indent .. marker .. ' '
|
||||||
-- Also preserve checkbox if present
|
if cmd == 'o' and line:match('^%s*[%-%*#]%s+(%[[%sxXoO%.%-]%])') then
|
||||||
local checkbox = line:match('^%s*[%-%*#]%s+(%[[%sxXoO%.%-]%])')
|
|
||||||
if checkbox then
|
|
||||||
prefix = prefix .. '[ ] '
|
prefix = prefix .. '[ ] '
|
||||||
end
|
end
|
||||||
vim.api.nvim_feedkeys('o' .. prefix, 'n', false)
|
vim.api.nvim_feedkeys(cmd .. 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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== Public attach =====
|
-- ===== Public attach =====
|
||||||
@@ -259,8 +245,8 @@ function M.attach(bufnr, mappings)
|
|||||||
{ desc = 'nuwiki: remove checkbox (current item)' }, bufnr)
|
{ desc = 'nuwiki: remove checkbox (current item)' }, bufnr)
|
||||||
map('n', 'gL', cmd.list_remove_checkbox_in_list,
|
map('n', 'gL', cmd.list_remove_checkbox_in_list,
|
||||||
{ desc = 'nuwiki: remove checkboxes (whole list)' }, bufnr)
|
{ desc = 'nuwiki: remove checkboxes (whole list)' }, bufnr)
|
||||||
map('n', 'o', open_below_with_bullet, { desc = 'nuwiki: open below + bullet' }, bufnr)
|
map('n', 'o', function() open_with_bullet('o') end, { 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 above + bullet' }, bufnr)
|
||||||
|
|
||||||
-- Insert-mode bindings (matches upstream vimwiki). The list helpers
|
-- Insert-mode bindings (matches upstream vimwiki). The list helpers
|
||||||
-- mutate the buffer via the LSP / `nvim_buf_set_lines`, both of
|
-- 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
|
if opts.wikis then
|
||||||
local buf_norm = vim.fs.normalize(buf_file)
|
local buf_norm = vim.fs.normalize(buf_file)
|
||||||
for _, w in ipairs(opts.wikis) do
|
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
|
if root ~= '' and buf_norm:sub(1, #root) == root then
|
||||||
return root
|
return root
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ local M = {}
|
|||||||
|
|
||||||
-- ===== Heading helpers =====
|
-- ===== Heading helpers =====
|
||||||
|
|
||||||
local function heading_level(line)
|
local heading_level = require('nuwiki.util').heading_level
|
||||||
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
|
|
||||||
|
|
||||||
--- Find the start/end of the heading block at `lnum`.
|
--- Find the start/end of the heading block at `lnum`.
|
||||||
--- `with_subtree` controls whether we descend into sub-headings:
|
--- `with_subtree` controls whether we descend into sub-headings:
|
||||||
@@ -123,9 +117,7 @@ end
|
|||||||
|
|
||||||
-- ===== Table cell / column =====
|
-- ===== Table cell / column =====
|
||||||
|
|
||||||
local function is_table_row(line)
|
local is_table_row = require('nuwiki.util').is_table_row
|
||||||
return line:match('^%s*|.*|%s*$') ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Compute the byte ranges of cells on a `|`-delimited row.
|
--- Compute the byte ranges of cells on a `|`-delimited row.
|
||||||
--- Returns a list of `{start_byte, end_byte}` 1-based inclusive of the
|
--- 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
|
" their config (lazy.nvim does this automatically through `opts`). On Vim
|
||||||
" we start the LSP client when a vimwiki buffer is loaded.
|
" we start the LSP client when a vimwiki buffer is loaded.
|
||||||
"
|
"
|
||||||
" This file contains wiring only — all logic lives in
|
" This file contains wiring only. The editor layers (VimL + Lua) are thin
|
||||||
" the Rust language server.
|
" 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')
|
if exists('g:loaded_nuwiki')
|
||||||
finish
|
finish
|
||||||
@@ -15,7 +16,7 @@ let g:loaded_nuwiki = 1
|
|||||||
" Plugin version, surfaced by :VimwikiShowVersion / :NuwikiShowVersion.
|
" Plugin version, surfaced by :VimwikiShowVersion / :NuwikiShowVersion.
|
||||||
" Set before the Neovim early-return below so both clients see the global.
|
" Set before the Neovim early-return below so both clients see the global.
|
||||||
if !exists('g:nuwiki_version')
|
if !exists('g:nuwiki_version')
|
||||||
let g:nuwiki_version = '0.5.0'
|
let g:nuwiki_version = '0.5.2'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
" Resolve the plugin root NOW, while this script is being sourced and
|
" Resolve the plugin root NOW, while this script is being sourced and
|
||||||
|
|||||||
Reference in New Issue
Block a user