Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df72e52ea | |||
| 4a717bcb31 | |||
| f1494dd2fc | |||
| a653903dba | |||
| e0f806d307 | |||
| 5dd02e7b9e | |||
| 8fdfa9e256 | |||
| 59494e9a86 | |||
| e1d734a434 | |||
| fcce5621a0 | |||
| 3e8c534859 | |||
| 0d1a73a3ba | |||
| e6f400b370 | |||
| 96d53dddf3 | |||
| c144cbbb55 | |||
| 3c1ed48a2f | |||
| 34a0607e7a | |||
| daee0f1902 | |||
| f0c51fbfcc | |||
| 94cb58064d | |||
| 9d6d28a1b6 | |||
| 346e8b0de6 | |||
| ac14cdd838 | |||
| a4643bdacb | |||
| 9d89e01ed8 | |||
| fc0d74bfbe | |||
| f3d8af5f23 | |||
| bd729d513d | |||
| 5a936e4f96 | |||
| 32cf4508de | |||
| 7e757c6a7f | |||
| a5a07768d6 | |||
| 08ebdc6f9b | |||
| b77f4b1ced | |||
| c9d75aeb1f | |||
| cd4bfffef9 | |||
| 0de2bd9d68 | |||
| e530b3d623 | |||
| a2ca49a64c | |||
| 5090ab1be0 | |||
| 68f10971f7 | |||
| ddb9d0b916 | |||
| 255e24d1e0 | |||
| 38fed6dd9e | |||
| 30cccd88bd | |||
| 3b92b11948 | |||
| e319b4a935 | |||
| e6e3f70dd2 | |||
| 1246c99c3c | |||
| f5420bd1da | |||
| a63d3dd7cf | |||
| 5cc47b0a42 | |||
| 4a19f180f2 | |||
| 5d1ac09245 | |||
| dcb5d89c0c | |||
| 87d14310fc | |||
| b2f2fc88bd | |||
| 03005d0931 | |||
| 724712121d | |||
| 305324c6e9 | |||
| 6deba47106 | |||
| a11b742fc1 | |||
| 3865b8bf0e | |||
| c161f9d21a | |||
| 9441fe918c | |||
| 079e7246ac | |||
| d8d1e9e39e | |||
| 79b502fa5c | |||
| 88114a65a4 | |||
| 2da2168d88 | |||
| 874bdd0c02 | |||
| f0c07f0c28 | |||
| f616915581 | |||
| 773bbdb6a6 | |||
| 23f5a74086 | |||
| 2b3bc48b75 | |||
| 3b3d8ca782 | |||
| efea225240 | |||
| acaaf107b4 | |||
| 4710ba03c8 | |||
| fe116088bb | |||
| a7f7e8e61f | |||
| 724c12ac16 | |||
| 8dc4050b76 | |||
| 3e82f59f12 | |||
| 2a8e2b9ea9 | |||
| feedd30c4d | |||
| 23aec3e6c7 | |||
| 09b5a2bd46 | |||
| 3fd04b0efa | |||
| 545367c3f6 | |||
| 2d3db458fb | |||
| 624bccbe50 | |||
| db9c1c5c11 | |||
| 212b300fb4 | |||
| b8586537f8 | |||
| 28d5caf581 | |||
| d8565fbe67 | |||
| 086455e19b | |||
| 9de8543385 | |||
| 15ba774a28 | |||
| 2facb40265 | |||
| 279dacff7a | |||
| 66bfd61a63 |
@@ -2,27 +2,40 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: "1"
|
||||||
RUSTFLAGS: -D warnings
|
RUSTFLAGS: -D warnings
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt:
|
fmt:
|
||||||
name: cargo fmt --check
|
name: cargo fmt --check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83
|
- uses: dtolnay/rust-toolchain@1.83
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-fmt-${{ hashFiles('**/Cargo.lock', '**/Cargo.toml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-fmt-
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: cargo clippy
|
name: cargo clippy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83
|
- uses: dtolnay/rust-toolchain@1.83
|
||||||
@@ -42,6 +55,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: cargo test
|
name: cargo test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83
|
- uses: dtolnay/rust-toolchain@1.83
|
||||||
@@ -76,13 +90,13 @@ jobs:
|
|||||||
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock', '**/Cargo.toml') }}
|
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock', '**/Cargo.toml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cargo-test-
|
${{ runner.os }}-cargo-test-
|
||||||
- name: install Neovim 0.11 + Vim
|
- name: install Neovim + Vim
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Apt's neovim is too old (Ubuntu LTS ships 0.6/0.7), missing
|
# Test the minimum supported Neovim version (0.11.0) so we
|
||||||
# `vim.lsp.get_clients` and rejecting `@group.modifier`
|
# catch API regressions at the bottom of the supported range.
|
||||||
# highlight names. Pull the official static tarball instead.
|
# Pinning to a specific patch version; bump when 0.12 ships.
|
||||||
NVIM_VER=v0.11.3
|
NVIM_VER=v0.11.0
|
||||||
curl -fsSL -o /tmp/nvim.tar.gz \
|
curl -fsSL -o /tmp/nvim.tar.gz \
|
||||||
"https://github.com/neovim/neovim/releases/download/${NVIM_VER}/nvim-linux-x86_64.tar.gz"
|
"https://github.com/neovim/neovim/releases/download/${NVIM_VER}/nvim-linux-x86_64.tar.gz"
|
||||||
sudo tar -xzf /tmp/nvim.tar.gz -C /opt
|
sudo tar -xzf /tmp/nvim.tar.gz -C /opt
|
||||||
@@ -104,3 +118,11 @@ jobs:
|
|||||||
run: ./development/tests/test-calendar.sh
|
run: ./development/tests/test-calendar.sh
|
||||||
- name: run Vim calendar integration harness
|
- name: run Vim calendar integration harness
|
||||||
run: ./development/tests/test-calendar-vim.sh
|
run: ./development/tests/test-calendar-vim.sh
|
||||||
|
- name: run Vim vars-shim harness
|
||||||
|
run: ./development/tests/test-vars-vim.sh
|
||||||
|
- name: run Vim vimwiki-config compat harness
|
||||||
|
run: ./development/tests/test-vimwiki-compat-vim.sh
|
||||||
|
- name: run Neovim global-shorthand harness
|
||||||
|
run: ./development/tests/test-global-shorthand.sh
|
||||||
|
- name: run Vim coc-registration harness
|
||||||
|
run: ./development/tests/test-coc-register-vim.sh
|
||||||
|
|||||||
+150
-30
@@ -1,9 +1,17 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
|
# Releases are cut from the Actions UI: "Run workflow" → enter the version
|
||||||
|
# (e.g. 0.4.0). The `prepare` job stamps that version into every hardcoded
|
||||||
|
# spot (scripts/set-version.sh), commits "chore(release): X.Y.Z", and pushes
|
||||||
|
# the vX.Y.Z tag. The build matrix + release job then run off that tag. No
|
||||||
|
# more hand-editing versions across Cargo.toml/plugin/nuwiki.vim.
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
- 'v*'
|
version:
|
||||||
|
description: 'Release version, no leading v (e.g. 0.4.0)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -11,10 +19,80 @@ permissions:
|
|||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
prepare:
|
||||||
|
name: bump + tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.stamp.outputs.version }}
|
||||||
|
tag: ${{ steps.stamp.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# Push the release commit + tag back with a write-capable token.
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@1.83
|
||||||
|
|
||||||
|
- name: Cache cargo state
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-prepare-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-prepare-
|
||||||
|
|
||||||
|
- 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.4.0)"
|
||||||
|
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
|
||||||
|
bash scripts/set-version.sh "$ver"
|
||||||
|
echo "version=$ver" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Gate: never tag code that doesn't build/test. If this fails nothing
|
||||||
|
# is committed or pushed, so the release simply doesn't happen.
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --workspace
|
||||||
|
|
||||||
|
- 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 Cargo.toml Cargo.lock \
|
||||||
|
crates/nuwiki-lsp/Cargo.toml crates/nuwiki-ls/Cargo.toml \
|
||||||
|
plugin/nuwiki.vim
|
||||||
|
git commit -m "chore(release): $VER"
|
||||||
|
git tag -a "$TAG" -m "nuwiki $VER"
|
||||||
|
git push origin "HEAD:${GITHUB_REF_NAME}"
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: build ${{ matrix.target }}
|
name: build ${{ matrix.target }}
|
||||||
|
needs: prepare
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -22,21 +100,24 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
apt: ""
|
apt: ""
|
||||||
rustflags: ""
|
rustflags: "-D warnings"
|
||||||
- target: aarch64-unknown-linux-gnu
|
- target: aarch64-unknown-linux-gnu
|
||||||
# gcc-aarch64-linux-gnu ships the cross compiler/binutils
|
# gcc-aarch64-linux-gnu ships the cross compiler/binutils
|
||||||
# but no target libc; libc6-dev-arm64-cross adds Scrt1.o,
|
# but no target libc; libc6-dev-arm64-cross adds Scrt1.o,
|
||||||
# crti.o, and friends needed at link time.
|
# crti.o, and friends needed at link time.
|
||||||
apt: "gcc-aarch64-linux-gnu libc6-dev-arm64-cross"
|
apt: "gcc-aarch64-linux-gnu libc6-dev-arm64-cross"
|
||||||
rustflags: ""
|
rustflags: "-D warnings"
|
||||||
- target: x86_64-unknown-linux-musl
|
- target: x86_64-unknown-linux-musl
|
||||||
apt: "musl-tools"
|
apt: "musl-tools"
|
||||||
rustflags: ""
|
rustflags: "-D warnings"
|
||||||
- target: aarch64-unknown-linux-musl
|
- target: aarch64-unknown-linux-musl
|
||||||
apt: ""
|
apt: ""
|
||||||
rustflags: "-C linker=rust-lld -C link-self-contained=yes"
|
rustflags: "-D warnings -C linker=rust-lld -C link-self-contained=yes"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Build the just-tagged commit, not the branch tip.
|
||||||
|
ref: ${{ needs.prepare.outputs.tag }}
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@1.83
|
- uses: dtolnay/rust-toolchain@1.83
|
||||||
with:
|
with:
|
||||||
@@ -71,33 +152,37 @@ jobs:
|
|||||||
|
|
||||||
- name: Package archive
|
- name: Package archive
|
||||||
id: package
|
id: package
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version="${GITHUB_REF_NAME#v}"
|
archive="nuwiki-ls-${VERSION}-${{ matrix.target }}.tar.gz"
|
||||||
archive="nuwiki-ls-${version}-${{ matrix.target }}.tar.gz"
|
|
||||||
tar -czf "$archive" -C "target/${{ matrix.target }}/release" nuwiki-ls
|
tar -czf "$archive" -C "target/${{ matrix.target }}/release" nuwiki-ls
|
||||||
echo "archive=$archive" >> "$GITHUB_OUTPUT"
|
# Use a stable name without version so /releases/latest/download/nuwiki-ls-{target}.tar.gz always resolves.
|
||||||
|
stable="nuwiki-ls-${{ matrix.target }}.tar.gz"
|
||||||
|
mv "$archive" "$stable"
|
||||||
|
echo "archive=$stable" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
# Gitea Actions doesn't support upload-artifact v4 (uses a
|
|
||||||
# GitHub-only backend API); pin to v3.
|
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: nuwiki-ls-${{ matrix.target }}
|
name: nuwiki-ls-${{ matrix.target }}
|
||||||
path: ${{ steps.package.outputs.archive }}
|
path: ${{ steps.package.outputs.archive }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: gitea release
|
name: gitea release
|
||||||
needs: build
|
needs: [prepare, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.prepare.outputs.tag }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download all build artifacts
|
- name: Download all build artifacts
|
||||||
# Paired with upload-artifact@v3 above. v3 has no merge-multiple
|
# download-artifact@v3 nests each artifact under its own dir
|
||||||
# option and nests each artifact under its own subdirectory.
|
# (artifacts/<artifact-name>/<file>), so recurse.
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: ./artifacts
|
path: ./artifacts
|
||||||
@@ -111,12 +196,34 @@ jobs:
|
|||||||
apt-get update && apt-get install -y --no-install-recommends curl
|
apt-get update && apt-get install -y --no-install-recommends curl
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: notes
|
||||||
|
env:
|
||||||
|
TAG: ${{ needs.prepare.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")
|
||||||
|
else
|
||||||
|
log=$(git log --oneline "$TAG")
|
||||||
|
fi
|
||||||
|
cat > release-notes.txt <<EOF
|
||||||
|
**Full Changelog**: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${prev_tag:-$(git rev-list --max-parents=0 HEAD)}...${TAG}
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
$log
|
||||||
|
EOF
|
||||||
|
echo "Generated release notes"
|
||||||
|
|
||||||
- name: Create Gitea release + upload assets
|
- name: Create Gitea release + upload assets
|
||||||
env:
|
env:
|
||||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
GITEA_SERVER: ${{ github.server_url }}
|
GITEA_SERVER: ${{ github.server_url }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ needs.prepare.outputs.tag }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||||
@@ -125,21 +232,34 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Releasing $TAG to $GITEA_SERVER/$REPO"
|
echo "Releasing $TAG to $GITEA_SERVER/$REPO"
|
||||||
|
|
||||||
payload=$(jq -n --arg tag "$TAG" --arg name "$TAG" \
|
# Check if a release for this tag already exists (idempotency).
|
||||||
'{tag_name: $tag, name: $name, draft: false, prerelease: false}')
|
release_id=""
|
||||||
|
existing=$(curl --fail --silent --show-error -o /dev/null -w "%{http_code}" \
|
||||||
release_id=$(curl --fail --silent --show-error \
|
|
||||||
-H "Authorization: token $RELEASE_TOKEN" \
|
-H "Authorization: token $RELEASE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
"$GITEA_SERVER/api/v1/repos/$REPO/releases/tags/$TAG" \
|
||||||
-d "$payload" \
|
2>/dev/null || true)
|
||||||
"$GITEA_SERVER/api/v1/repos/$REPO/releases" \
|
if [ "$existing" = "200" ]; then
|
||||||
| jq -r '.id')
|
echo "Release for $TAG already exists — fetching existing id"
|
||||||
|
release_id=$(curl --fail --silent --show-error \
|
||||||
if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then
|
-H "Authorization: token $RELEASE_TOKEN" \
|
||||||
echo "::error::failed to create release"
|
"$GITEA_SERVER/api/v1/repos/$REPO/releases/tags/$TAG" \
|
||||||
exit 1
|
| jq -r '.id')
|
||||||
|
else
|
||||||
|
notes=$(cat release-notes.txt 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}')
|
||||||
|
release_id=$(curl --fail --silent --show-error \
|
||||||
|
-H "Authorization: token $RELEASE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"$GITEA_SERVER/api/v1/repos/$REPO/releases" \
|
||||||
|
| jq -r '.id')
|
||||||
|
if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then
|
||||||
|
echo "::error::failed to create release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Created release id=$release_id"
|
||||||
fi
|
fi
|
||||||
echo "Created release id=$release_id"
|
|
||||||
|
|
||||||
# download-artifact@v3 nests each artifact under its own dir
|
# download-artifact@v3 nests each artifact under its own dir
|
||||||
# (artifacts/<artifact-name>/<file>), so recurse.
|
# (artifacts/<artifact-name>/<file>), so recurse.
|
||||||
|
|||||||
@@ -14,3 +14,6 @@
|
|||||||
|
|
||||||
# Vim help tags (regenerated by :helptags)
|
# Vim help tags (regenerated by :helptags)
|
||||||
doc/tags
|
doc/tags
|
||||||
|
|
||||||
|
# Claude Code local session artifacts
|
||||||
|
.claude/
|
||||||
|
|||||||
Generated
+3
-3
@@ -376,11 +376,11 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nuwiki-core"
|
name = "nuwiki-core"
|
||||||
version = "0.1.0"
|
version = "0.4.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nuwiki-ls"
|
name = "nuwiki-ls"
|
||||||
version = "0.1.0"
|
version = "0.4.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nuwiki-lsp",
|
"nuwiki-lsp",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -388,7 +388,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nuwiki-lsp"
|
name = "nuwiki-lsp"
|
||||||
version = "0.1.0"
|
version = "0.4.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dashmap 6.1.0",
|
"dashmap 6.1.0",
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.4.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.83"
|
rust-version = "1.83"
|
||||||
authors = ["Gabriel Fróes Franco <gffranco@gmail.com>"]
|
authors = ["Gabriel Fróes Franco <gffranco@gmail.com>"]
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ results.
|
|||||||
|
|
||||||
### Diary
|
### Diary
|
||||||
|
|
||||||
- Daily diary out of the box (`<Leader>ww`, `<C-Down>` / `<C-Up>`).
|
- Daily diary out of the box (`<Leader>w<Leader>w`, `<C-Down>` / `<C-Up>`).
|
||||||
- Configurable cadence: set `diary_frequency = 'weekly'` (or `'monthly'`
|
- Configurable cadence: set `diary_frequency = 'weekly'` (or `'monthly'`
|
||||||
/ `'yearly'`) per wiki and the commands address `2026-W19.wiki`,
|
/ `'yearly'`) per wiki and the commands address
|
||||||
`2026-05.wiki`, `2026.wiki` instead. Navigation stays at the same
|
`2026-W19.wiki` / `2026-05.wiki` / `2026.wiki` instead. Navigation
|
||||||
cadence as the entry under the cursor.
|
stays at the same cadence as the entry under the cursor.
|
||||||
- Diary index page generation (`:VimwikiDiaryGenerateLinks`).
|
- Diary index page generation (`:VimwikiDiaryGenerateLinks`).
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
@@ -82,9 +82,9 @@ Neovim) instead of relying on a plugin-manager build hook.
|
|||||||
Older releases fall back to a `vim.lsp.start` autocmd but aren't
|
Older releases fall back to a `vim.lsp.start` autocmd but aren't
|
||||||
officially supported.
|
officially supported.
|
||||||
- **Vim 9+** — needs a third-party LSP client:
|
- **Vim 9+** — needs a third-party LSP client:
|
||||||
[`vim-lsp`](https://github.com/prabirshrestha/vim-lsp) (recommended,
|
[`vim-lsp`](https://github.com/prabirshrestha/vim-lsp) or
|
||||||
auto-registered) or [`coc.nvim`](https://github.com/neoclide/coc.nvim)
|
[`coc.nvim`](https://github.com/neoclide/coc.nvim) — both
|
||||||
(one-time `coc-settings.json` entry — see [Vim LSP client
|
auto-registered, no manual config (see [Vim LSP client
|
||||||
setup](#vim-lsp-client-setup)).
|
setup](#vim-lsp-client-setup)).
|
||||||
- **Rust toolchain (1.83+ stable)** — only needed when building the
|
- **Rust toolchain (1.83+ stable)** — only needed when building the
|
||||||
binary from source; pre-built release downloads skip this.
|
binary from source; pre-built release downloads skip this.
|
||||||
@@ -127,7 +127,7 @@ call dein#add('gffranco/nuwiki', {
|
|||||||
```sh
|
```sh
|
||||||
git clone https://code.gfran.co/gffranco/nuwiki ~/.vim/pack/gffranco/start/nuwiki
|
git clone https://code.gfran.co/gffranco/nuwiki ~/.vim/pack/gffranco/start/nuwiki
|
||||||
cd ~/.vim/pack/gffranco/start/nuwiki && cargo build --release -p nuwiki-ls
|
cd ~/.vim/pack/gffranco/start/nuwiki && cargo build --release -p nuwiki-ls
|
||||||
mkdir -p bin && ln -s ../target/release/nuwiki-ls bin/nuwiki-ls
|
mkdir -p bin && cp target/release/nuwiki-ls bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vim LSP client setup
|
### Vim LSP client setup
|
||||||
@@ -145,10 +145,18 @@ auto-enables it. Your wiki settings (`g:nuwiki_wiki_root`,
|
|||||||
`g:nuwiki_wikis`, …) are forwarded as the server's initialization
|
`g:nuwiki_wikis`, …) are forwarded as the server's initialization
|
||||||
options.
|
options.
|
||||||
|
|
||||||
**coc.nvim** — [`coc.nvim`](https://github.com/neoclide/coc.nvim) reads
|
**coc.nvim** — also zero configuration. nuwiki registers itself with
|
||||||
its server list from `coc-settings.json`, which the plugin can't edit
|
[`coc.nvim`](https://github.com/neoclide/coc.nvim) programmatically
|
||||||
on your behalf, so add the entry once yourself (open it with
|
(`coc#config('languageserver.nuwiki', …)`) when the first `.wiki` buffer
|
||||||
`:CocConfig`):
|
loads, forwarding the same wiki settings (`g:nuwiki_wiki_root`,
|
||||||
|
`g:nuwiki_wikis`, your `g:vimwiki_*` config, the global shorthand, …) as
|
||||||
|
the server's initialization options. **No `coc-settings.json`
|
||||||
|
`languageserver` entry needed** — change your wiki config and reload; the
|
||||||
|
new settings are picked up.
|
||||||
|
|
||||||
|
To manage the entry yourself instead, set `let g:nuwiki_no_coc_register = 1`
|
||||||
|
and add it to `coc-settings.json` manually (nuwiki then just prints the
|
||||||
|
snippet on first load):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -219,6 +227,8 @@ require('nuwiki').setup({
|
|||||||
diary_rel_path = 'diary',
|
diary_rel_path = 'diary',
|
||||||
diary_index = 'diary',
|
diary_index = 'diary',
|
||||||
diary_frequency = 'daily', -- 'daily' | 'weekly' | 'monthly' | 'yearly'
|
diary_frequency = 'daily', -- 'daily' | 'weekly' | 'monthly' | 'yearly'
|
||||||
|
diary_weekly_style = 'iso', -- 'iso' (YYYY-Www) | 'date' (week-start YYYY-MM-DD, vimwiki)
|
||||||
|
diary_start_week_day = 'monday', -- week start for diary_weekly_style='date'
|
||||||
diary_sort = 'desc', -- 'desc' | 'asc'
|
diary_sort = 'desc', -- 'desc' | 'asc'
|
||||||
diary_header = 'Diary',
|
diary_header = 'Diary',
|
||||||
-- HTML export
|
-- HTML export
|
||||||
@@ -227,11 +237,21 @@ require('nuwiki').setup({
|
|||||||
template_default = 'default',
|
template_default = 'default',
|
||||||
template_ext = '.tpl',
|
template_ext = '.tpl',
|
||||||
css_name = 'style.css',
|
css_name = 'style.css',
|
||||||
|
custom_wiki2html = '', -- external converter; empty = built-in renderer
|
||||||
|
custom_wiki2html_args = '', -- extra args appended to the converter call
|
||||||
|
base_url = '', -- public URL prefix for RSS item links
|
||||||
auto_export = false, -- export on save
|
auto_export = false, -- export on save
|
||||||
auto_toc = false, -- auto-refresh TOC on save
|
auto_toc = false, -- auto-refresh TOC on save
|
||||||
|
auto_generate_links = false, -- regen Generated Links on save
|
||||||
|
auto_generate_tags = false, -- regen Generated Tags index on save
|
||||||
|
auto_diary_index = false, -- regen diary index on diary-entry save
|
||||||
|
toc_header = 'Contents', -- :VimwikiTOC caption (+ toc_header_level = 1)
|
||||||
|
links_header = 'Generated Links',
|
||||||
|
tags_header = 'Generated Tags',
|
||||||
exclude_files = {}, -- glob patterns
|
exclude_files = {}, -- glob patterns
|
||||||
-- Checkbox progression characters: " .oOX" by default.
|
-- Checkbox progression characters: " .oOX" by default.
|
||||||
listsyms = ' .oOX',
|
listsyms = ' .oOX',
|
||||||
|
listsym_rejected = '-', -- glyph for a cancelled checkbox [-]
|
||||||
listsyms_propagate = true,
|
listsyms_propagate = true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -239,20 +259,25 @@ require('nuwiki').setup({
|
|||||||
-- Keymap groups. Flip subgroups off independently. Defaults all on.
|
-- Keymap groups. Flip subgroups off independently. Defaults all on.
|
||||||
mappings = {
|
mappings = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
wiki_prefix = true, -- <Leader>w*
|
wiki_prefix = true, -- <Leader>w{w,t,s,i}, <Leader>w<Leader>{w,y,t,m,i}
|
||||||
links = true, -- <CR>, <S-CR>, <Tab>, <BS>, +, …
|
links = true, -- <CR>, <S-CR>, <C-CR>, <C-S-CR>, <D-CR>, <M-CR>, <BS>, <Tab>, <S-Tab>, + (n/x), <Leader>w{n,d,r}, <Leader>wc (n/x)
|
||||||
lists = true, -- <C-Space>, gln/glp/glx, gl/gL, o/O, …
|
lists = true, -- <C-Space>, gnt, gln, glp, glx, glh, gll, gLh, gLl (aliases gLH/gLL), glr, gLr (alias gLR), gl{-,*,#,1,i,I,a,A}, gL{…}, gl, gL, o, O; insert <C-D>, <C-T>, <C-L><C-J/K/M>, <CR>
|
||||||
headers = true, -- =, -, ]], [[, ]=, [=, ]u, [u
|
headers = true, -- =, -, ]], [[, ]=, [=, ]u, [u
|
||||||
table_editing = true, -- gqq, <A-Left>, <A-Right>, <Tab> in insert mode
|
table_editing = true, -- gqq, gq1, gww, gw1, <A-Left>, <A-Right>; insert <Tab>, <S-Tab>
|
||||||
diary = true, -- <C-Down>, <C-Up>
|
diary = true, -- <C-Down>, <C-Up>
|
||||||
html_export = true, -- <Leader>wh, <Leader>whh
|
html_export = true, -- <Leader>wh, <Leader>whh, <Leader>wha
|
||||||
text_objects = true, -- ah, ih, aH, iH, al, il, a\, i\, ac, ic
|
text_objects = true, -- ah, ih, aH, iH, al, il, a\, i\, ac, ic
|
||||||
mouse = false, -- <2-LeftMouse>, <RightMouse>, … (opt-in)
|
mouse = false, -- <2-LeftMouse>, <S-2-LeftMouse>, <C-2-LeftMouse>, <MiddleMouse>, <RightMouse> (opt-in)
|
||||||
},
|
},
|
||||||
|
|
||||||
-- 'lsp' (server-driven foldingRange, default) | 'expr' (regex) | 'off'.
|
-- 'lsp' (server-driven foldingRange, default) | 'expr' (regex) | 'off'.
|
||||||
folding = 'lsp',
|
folding = 'lsp',
|
||||||
|
|
||||||
|
-- Mirror Vim's &autowriteall while in a wiki buffer (auto-save on switch).
|
||||||
|
autowriteall = true,
|
||||||
|
-- Re-align the table under the cursor on InsertLeave.
|
||||||
|
table_auto_fmt = true,
|
||||||
|
|
||||||
-- Broken-link diagnostic severity: 'off' | 'hint' | 'warn' | 'error'.
|
-- Broken-link diagnostic severity: 'off' | 'hint' | 'warn' | 'error'.
|
||||||
diagnostic = {
|
diagnostic = {
|
||||||
link_severity = 'warn',
|
link_severity = 'warn',
|
||||||
@@ -269,7 +294,10 @@ let g:nuwiki_file_extension = '.wiki'
|
|||||||
let g:nuwiki_log_level = 'warn'
|
let g:nuwiki_log_level = 'warn'
|
||||||
let g:nuwiki_link_severity = 'warn' " 'off'|'hint'|'warn'|'error'
|
let g:nuwiki_link_severity = 'warn' " 'off'|'hint'|'warn'|'error'
|
||||||
let g:nuwiki_no_default_mappings = 0 " set to 1 to skip the keymap layer
|
let g:nuwiki_no_default_mappings = 0 " set to 1 to skip the keymap layer
|
||||||
|
let g:nuwiki_map_prefix = '<Leader>w' " wiki command prefix
|
||||||
let g:nuwiki_no_folding = 0 " set to 1 to skip foldexpr setup
|
let g:nuwiki_no_folding = 0 " set to 1 to skip foldexpr setup
|
||||||
|
let g:nuwiki_autowriteall = 1 " set to 0 to not touch &autowriteall
|
||||||
|
let g:nuwiki_table_auto_fmt = 1 " set to 0 to skip InsertLeave table re-align
|
||||||
let g:nuwiki_mouse_mappings = 0 " set to 1 to enable mouse maps
|
let g:nuwiki_mouse_mappings = 0 " set to 1 to enable mouse maps
|
||||||
" Drop individual keymap subgroups (mirror the Lua `mappings.<group>` toggles):
|
" Drop individual keymap subgroups (mirror the Lua `mappings.<group>` toggles):
|
||||||
" g:nuwiki_no_{wiki_prefix,links,lists,headers,table_editing,diary,html_export,text_objects}_mappings
|
" g:nuwiki_no_{wiki_prefix,links,lists,headers,table_editing,diary,html_export,text_objects}_mappings
|
||||||
@@ -285,7 +313,13 @@ let g:nuwiki_mouse_mappings = 0 " set to 1 to enable mouse maps
|
|||||||
| `file_extension` | string | `'.wiki'` | any extension, leading `.` |
|
| `file_extension` | string | `'.wiki'` | any extension, leading `.` |
|
||||||
| `syntax` | string | `'vimwiki'` | `'vimwiki'` (`'markdown'` planned) |
|
| `syntax` | string | `'vimwiki'` | `'vimwiki'` (`'markdown'` planned) |
|
||||||
| `log_level` | string | `'warn'` | `'error'` \| `'warn'` \| `'info'` \| `'debug'` |
|
| `log_level` | string | `'warn'` | `'error'` \| `'warn'` \| `'info'` \| `'debug'` |
|
||||||
|
| `map_prefix` | string | `'<Leader>w'` | prefix for the wiki command family (`<prefix>w`, `<prefix>t`, `<prefix><Leader>w`, …); mirrors vimwiki's `g:vimwiki_map_prefix`. Vim users set `g:nuwiki_map_prefix` |
|
||||||
| `folding` | string | `'lsp'` | `'lsp'` \| `'expr'` \| `'off'` |
|
| `folding` | string | `'lsp'` | `'lsp'` \| `'expr'` \| `'off'` |
|
||||||
|
| `autowriteall` | bool | `true` | mirror Vim's `'autowriteall'` while in a wiki buffer (vimwiki `autowriteall`); Vim users set `g:nuwiki_autowriteall` |
|
||||||
|
| `table_auto_fmt` | bool | `true` | re-align the table under the cursor on `InsertLeave` (vimwiki `table_auto_fmt`); Vim users set `g:nuwiki_table_auto_fmt` |
|
||||||
|
| `auto_chdir` | bool | `false` | `:lcd` into the owning wiki's root when a wiki buffer becomes current (vimwiki `auto_chdir`); Vim users set `g:nuwiki_auto_chdir` |
|
||||||
|
| `auto_header` | bool | `false` | insert a level-1 header from the filename on new wiki pages (vimwiki `auto_header`); honours `links_space_char`. Vim users set `g:nuwiki_auto_header` |
|
||||||
|
| `use_calendar` | bool | `true` | wire up calendar-vim integration when it's on the runtimepath (`g:calendar_action`/`g:calendar_sign`); set `false` (or `g:nuwiki_no_calendar`) to opt out |
|
||||||
| `wikis` | list of tables | `nil` | per-wiki tables (see below); wins over the single-wiki shorthand |
|
| `wikis` | list of tables | `nil` | per-wiki tables (see below); wins over the single-wiki shorthand |
|
||||||
| `mappings` | table | all on | keymap subgroups (see below) |
|
| `mappings` | table | all on | keymap subgroups (see below) |
|
||||||
| `diagnostic` | table | `{ link_severity = 'warn' }` | `link_severity`: `'off'` \| `'hint'` \| `'warn'` \| `'error'` |
|
| `diagnostic` | table | `{ link_severity = 'warn' }` | `link_severity`: `'off'` \| `'hint'` \| `'warn'` \| `'error'` |
|
||||||
@@ -294,21 +328,31 @@ let g:nuwiki_mouse_mappings = 0 " set to 1 to enable mouse maps
|
|||||||
|
|
||||||
Each is a boolean. All default to `true` except `mouse`.
|
Each is a boolean. All default to `true` except `mouse`.
|
||||||
|
|
||||||
| Key | Default | Controls |
|
The `<Leader>w` prefix in the `wiki_prefix`, `links`, and `html_export` rows is the default `map_prefix` and moves with it.
|
||||||
|
|
||||||
|
| Key | Default | Controls (every binding in the group) |
|
||||||
|-----|---------|----------|
|
|-----|---------|----------|
|
||||||
| `enabled` | `true` | master switch for the whole keymap layer |
|
| `enabled` | `true` | master switch for the whole keymap layer |
|
||||||
| `wiki_prefix` | `true` | `<Leader>w*` |
|
| `wiki_prefix` | `true` | `<Leader>ww`, `<Leader>wt`, `<Leader>ws`, `<Leader>wi`, `<Leader>w<Leader>w`, `<Leader>w<Leader>y`, `<Leader>w<Leader>t`, `<Leader>w<Leader>m`, `<Leader>w<Leader>i` |
|
||||||
| `links` | `true` | `<CR>`, `<S-CR>`, `<Tab>`, `<BS>`, `+`, … |
|
| `links` | `true` | `<CR>`, `<S-CR>`, `<C-CR>`, `<C-S-CR>`, `<D-CR>`, `<M-CR>`, `<BS>`, `<Tab>`, `<S-Tab>`, `+` (normal + visual), `<CR>` (visual: normalize selection), `<Leader>wn`, `<Leader>wd`, `<Leader>wr`, `<Leader>wc` (normal + visual) |
|
||||||
| `lists` | `true` | `<C-Space>`, `gln`/`glp`/`glx`, `gl`/`gL`, `o`/`O`, … |
|
| `lists` | `true` | `<C-Space>` (also `<C-@>`/`<Nul>`), `gnt`, `gln`, `glp`, `glx`, `glh`, `gll`, `gLh`, `gLl`, `gLH`, `gLL`, `glr`, `gLr`, `gLR`, `gl-`, `gl*`, `gl#`, `gl1`, `gli`, `glI`, `gla`, `glA`, `gL-`, `gL*`, `gL#`, `gL1`, `gLi`, `gLI`, `gLa`, `gLA`, `gl`, `gL`, `o`, `O`; insert: `<C-D>`, `<C-T>`, `<C-L><C-J>`, `<C-L><C-K>`, `<C-L><C-M>`, `<CR>`, `<S-CR>` (multiline item) |
|
||||||
| `headers` | `true` | `=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u` |
|
| `headers` | `true` | `=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u` |
|
||||||
| `table_editing` | `true` | `gqq`, `<A-Left>`, `<A-Right>`, `<Tab>` (insert) |
|
| `table_editing` | `true` | `gqq`, `gq1`, `gww`, `gw1`, `<A-Left>`, `<A-Right>`; insert: `<Tab>`, `<S-Tab>` |
|
||||||
| `diary` | `true` | `<C-Down>`, `<C-Up>` |
|
| `diary` | `true` | `<C-Down>`, `<C-Up>` |
|
||||||
| `html_export` | `true` | `<Leader>wh`, `<Leader>whh` |
|
| `html_export` | `true` | `<Leader>wh`, `<Leader>whh`, `<Leader>wha` |
|
||||||
| `text_objects` | `true` | `ah`, `ih`, `aH`, `iH`, `al`, `il`, `a\`, `i\`, `ac`, `ic` |
|
| `text_objects` | `true` | `ah`, `ih`, `aH`, `iH`, `al`, `il`, `a\`, `i\`, `ac`, `ic` |
|
||||||
| `mouse` | `false` | `<2-LeftMouse>`, `<RightMouse>`, … (opt-in) |
|
| `mouse` | `false` | `<2-LeftMouse>`, `<S-2-LeftMouse>`, `<C-2-LeftMouse>`, `<MiddleMouse>`, `<RightMouse>` (opt-in) |
|
||||||
|
|
||||||
#### Per-wiki keys (`wikis[]`)
|
#### Per-wiki keys (`wikis[]`)
|
||||||
|
|
||||||
|
Most display/generation keys below can also be set **once at the top level**
|
||||||
|
as a default for every wiki (vimwiki-style global shorthand) — via `setup({ … })`
|
||||||
|
or `g:nuwiki_<key>` — and a per-wiki value overrides it. The keys that honour
|
||||||
|
this are `toc_header`, `toc_header_level`, `toc_link_format`, `links_header`,
|
||||||
|
`links_header_level`, `tags_header`, `tags_header_level`,
|
||||||
|
`html_header_numbering`, `html_header_numbering_sym`, `links_space_char`,
|
||||||
|
`list_margin`, `listsyms`, `listsym_rejected`, and the `auto_*` toggles.
|
||||||
|
|
||||||
| Key | Type | Default | Accepted values |
|
| Key | Type | Default | Accepted values |
|
||||||
|-----|------|---------|-----------------|
|
|-----|------|---------|-----------------|
|
||||||
| `name` | string | — | any label |
|
| `name` | string | — | any label |
|
||||||
@@ -319,14 +363,31 @@ Each is a boolean. All default to `true` except `mouse`.
|
|||||||
| `diary_rel_path` | string | `'diary'` | path relative to `root` |
|
| `diary_rel_path` | string | `'diary'` | path relative to `root` |
|
||||||
| `diary_index` | string | `'diary'` | diary index page stem |
|
| `diary_index` | string | `'diary'` | diary index page stem |
|
||||||
| `diary_frequency` | string | `'daily'` | `'daily'` \| `'weekly'` \| `'monthly'` \| `'yearly'` |
|
| `diary_frequency` | string | `'daily'` | `'daily'` \| `'weekly'` \| `'monthly'` \| `'yearly'` |
|
||||||
| `diary_caption_level` | int | `1` | `1`–`6` (level of the index caption) |
|
| `diary_weekly_style` | string | `'iso'` | `'iso'` (`YYYY-Www`) \| `'date'`/`'vimwiki'` (week-start `YYYY-MM-DD`). Weekly only |
|
||||||
|
| `diary_start_week_day` | string | `'monday'` | `'monday'`–`'sunday'`; week start when `diary_weekly_style = 'date'` |
|
||||||
|
| `diary_caption_level` | int | `0` | `0`–`6` (level of the index caption) |
|
||||||
| `diary_sort` | string | `'desc'` | `'desc'` \| `'asc'` |
|
| `diary_sort` | string | `'desc'` | `'desc'` \| `'asc'` |
|
||||||
| `diary_header` | string | `'Diary'` | any heading text |
|
| `diary_header` | string | `'Diary'` | any heading text |
|
||||||
|
| `diary_months` | list | English month names | month-number → display name used in the diary index |
|
||||||
|
| `create_link` | bool | `true` | create the target page when following a link to a missing page; `false` makes the follow a no-op |
|
||||||
|
| `dir_link` | string | `''` | index stem opened when following a link to a directory (e.g. `'index'`); empty opens the directory itself |
|
||||||
|
| `bullet_types` | list | `['-', '*', '#']` | unordered-bullet glyphs for this wiki; drives `cycle_bullets` |
|
||||||
|
| `cycle_bullets` | bool | `false` | rotate an unordered item's glyph through `bullet_types` as it is indented/dedented |
|
||||||
|
| `generated_links_caption` | bool | `false` | emit `[[page\|Heading]]` (first heading as caption) from `:VimwikiGenerateLinks` |
|
||||||
|
| `toc_link_format` | int | `0` | `0` = `[[#anchor\|title]]` (with description); `1` = `[[#anchor]]` (anchor only) |
|
||||||
|
| `table_reduce_last_col` | bool | `false` | don't pad the last table column to fill — keep it at its minimum width |
|
||||||
| `listsyms` | string | `' .oOX'` | checkbox progression chars |
|
| `listsyms` | string | `' .oOX'` | checkbox progression chars |
|
||||||
|
| `listsym_rejected` | string | `'-'` | glyph for a cancelled checkbox (`[-]`); first char used |
|
||||||
| `listsyms_propagate` | bool | `true` | `true` \| `false` |
|
| `listsyms_propagate` | bool | `true` | `true` \| `false` |
|
||||||
| `list_margin` | int | `-1` | `-1` (auto) or `≥ 0` |
|
| `list_margin` | int | `-1` | `-1` (auto) or `≥ 0` |
|
||||||
| `links_space_char` | string | `' '` | char that replaces spaces in synthesised link paths |
|
| `links_space_char` | string | `' '` | char that replaces spaces in synthesised link paths |
|
||||||
| `auto_toc` | bool | `false` | `true` \| `false` |
|
| `auto_toc` | bool | `false` | `true` \| `false` |
|
||||||
|
| `auto_generate_links` | bool | `false` | regenerate an existing Generated Links section on save |
|
||||||
|
| `auto_generate_tags` | bool | `false` | regenerate an existing Generated Tags index on save |
|
||||||
|
| `auto_diary_index` | bool | `false` | regenerate the diary index when a diary entry is saved |
|
||||||
|
| `toc_header` / `toc_header_level` | string / int | `'Contents'` / `1` | `:VimwikiTOC` caption + heading level |
|
||||||
|
| `links_header` / `links_header_level` | string / int | `'Generated Links'` / `1` | `:VimwikiGenerateLinks` caption + level |
|
||||||
|
| `tags_header` / `tags_header_level` | string / int | `'Generated Tags'` / `1` | `:VimwikiGenerateTagLinks` index caption + level |
|
||||||
|
|
||||||
#### HTML export keys (per-wiki)
|
#### HTML export keys (per-wiki)
|
||||||
|
|
||||||
@@ -340,8 +401,19 @@ Each is a boolean. All default to `true` except `mouse`.
|
|||||||
| `css_name` | string | `'style.css'` | stylesheet filename |
|
| `css_name` | string | `'style.css'` | stylesheet filename |
|
||||||
| `auto_export` | bool | `false` | `true` \| `false` (export on save) |
|
| `auto_export` | bool | `false` | `true` \| `false` (export on save) |
|
||||||
| `html_filename_parameterization` | bool | `false` | `true` \| `false` |
|
| `html_filename_parameterization` | bool | `false` | `true` \| `false` |
|
||||||
|
| `custom_wiki2html` | string | `''` | external converter command; empty = built-in renderer. Called as `<cmd> <force> <syntax> <ext> <out_dir> <in_file> <css> <tpl_path> <tpl_default> <tpl_ext> <root_path> <args>` (`-` for empty optionals), matching vimwiki |
|
||||||
|
| `custom_wiki2html_args` | string | `''` | extra args appended to the `custom_wiki2html` call |
|
||||||
|
| `base_url` | string | `''` | public URL prefix; when set, diary RSS item links become `<base_url><diary_rel_path>/<date>.html` instead of `file://` |
|
||||||
| `exclude_files` | list | `{}` | glob patterns |
|
| `exclude_files` | list | `{}` | glob patterns |
|
||||||
| `color_dic` | table | `{}` | `{ name = 'color', … }` |
|
| `color_dic` | table | `{}` | `{ name = 'color', … }` colour-tag name → CSS value |
|
||||||
|
| `color_tag_template` | string | `'<span style="__STYLE__">__CONTENT__</span>'` | template for a `color_dic` colour span; `__STYLE__` → `color:<css>`, `__CONTENT__` → inner HTML |
|
||||||
|
| `valid_html_tags` | string | `'b,i,s,u,sub,sup,kbd,br,hr,div,center,strong,em'` | comma-separated inline HTML tags passed through export unescaped (`span` is always allowed) |
|
||||||
|
| `emoji_enable` | bool | `true` | substitute `:alias:` emoji shortcodes with their glyph during export |
|
||||||
|
| `text_ignore_newline` | bool | `true` | a single newline inside a paragraph is a space in HTML; `false` → `<br>` |
|
||||||
|
| `list_ignore_newline` | bool | `true` | a single newline inside a list item is a space in HTML; `false` → `<br>` |
|
||||||
|
| `rss_name` | string | `'rss.xml'` | filename of the generated diary RSS feed, relative to `html_path` |
|
||||||
|
| `rss_max_items` | int | `10` | cap on the number of diary items in the RSS feed |
|
||||||
|
| `user_htmls` | list | `{}` | basenames of HTML files with no wiki source that `:VimwikiAll2HTML` must not prune |
|
||||||
|
|
||||||
#### Vim globals (`g:nuwiki_*`)
|
#### Vim globals (`g:nuwiki_*`)
|
||||||
|
|
||||||
@@ -351,19 +423,28 @@ For users configuring without Lua.
|
|||||||
|--------|---------|---------|
|
|--------|---------|---------|
|
||||||
| `g:nuwiki_wiki_root` | `'~/vimwiki'` | single-wiki root |
|
| `g:nuwiki_wiki_root` | `'~/vimwiki'` | single-wiki root |
|
||||||
| `g:nuwiki_file_extension` | `'.wiki'` | wiki file extension |
|
| `g:nuwiki_file_extension` | `'.wiki'` | wiki file extension |
|
||||||
|
| `g:nuwiki_syntax` | `'vimwiki'` | wiki syntax |
|
||||||
|
| `g:nuwiki_diary_rel_path` | `'diary'` | diary subdirectory (relative to the wiki root) |
|
||||||
|
| `g:nuwiki_wikis` | _(unset)_ | list of per-wiki dicts for multi-wiki setups; wins over the scalar globals |
|
||||||
| `g:nuwiki_log_level` | `'warn'` | `error` \| `warn` \| `info` \| `debug` |
|
| `g:nuwiki_log_level` | `'warn'` | `error` \| `warn` \| `info` \| `debug` |
|
||||||
| `g:nuwiki_link_severity` | `'warn'` | broken-link severity: `off` \| `hint` \| `warn` \| `error` |
|
| `g:nuwiki_link_severity` | `'warn'` | broken-link severity: `off` \| `hint` \| `warn` \| `error` |
|
||||||
|
| `g:nuwiki_map_prefix` | `'<Leader>w'` | prefix for the wiki command family |
|
||||||
| `g:nuwiki_no_default_mappings` | `0` | `1` skips the whole keymap layer |
|
| `g:nuwiki_no_default_mappings` | `0` | `1` skips the whole keymap layer |
|
||||||
| `g:nuwiki_no_wiki_prefix_mappings` | `0` | `1` skips the `<Leader>w*` group |
|
| `g:nuwiki_no_wiki_prefix_mappings` | `0` | `1` skips the `wiki_prefix` group (`<Leader>ww`, `<Leader>wt`, `<Leader>ws`, `<Leader>wi`, `<Leader>w<Leader>{w,y,t,m,i}`) |
|
||||||
| `g:nuwiki_no_links_mappings` | `0` | `1` skips the link group (`<CR>`, `+`, …) |
|
| `g:nuwiki_no_links_mappings` | `0` | `1` skips the `links` group (`<CR>`, `<S-CR>`, `<C-CR>`, `<C-S-CR>`, `<D-CR>`, `<M-CR>`, `<BS>`, `<Tab>`, `<S-Tab>`, `+`, `<Leader>w{n,d,r,c}`) |
|
||||||
| `g:nuwiki_no_lists_mappings` | `0` | `1` skips the list group (`<C-Space>`, `gl*`, `o`/`O`, …) |
|
| `g:nuwiki_no_lists_mappings` | `0` | `1` skips the `lists` group (`<C-Space>`, `gnt`, `gln`, `glp`, `glx`, `glh`, `gll`, `gLh`, `gLl` (aliases `gLH`/`gLL`), `glr`, `gLr` (alias `gLR`), `gl{-,*,#,1,i,I,a,A}`, `gL{…}`, `gl`, `gL`, `o`, `O`; insert `<C-D>`, `<C-T>`, `<C-L><C-J/K/M>`, `<CR>`) |
|
||||||
| `g:nuwiki_no_headers_mappings` | `0` | `1` skips the header group (`=`, `-`, `]]`, …) |
|
| `g:nuwiki_no_headers_mappings` | `0` | `1` skips the `headers` group (`=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u`) |
|
||||||
| `g:nuwiki_no_table_editing_mappings` | `0` | `1` skips the table group (`gqq`, `<A-Left>`, insert `<Tab>`) |
|
| `g:nuwiki_no_table_editing_mappings` | `0` | `1` skips the `table_editing` group (`gqq`, `gq1`, `gww`, `gw1`, `<A-Left>`, `<A-Right>`; insert `<Tab>`, `<S-Tab>`) |
|
||||||
| `g:nuwiki_no_diary_mappings` | `0` | `1` skips the diary nav group (`<C-Down>`, `<C-Up>`) |
|
| `g:nuwiki_no_diary_mappings` | `0` | `1` skips the `diary` nav group (`<C-Down>`, `<C-Up>`) |
|
||||||
| `g:nuwiki_no_html_export_mappings` | `0` | `1` skips the HTML export group (`<Leader>wh*`) |
|
| `g:nuwiki_no_html_export_mappings` | `0` | `1` skips the `html_export` group (`<Leader>wh`, `<Leader>whh`, `<Leader>wha`) |
|
||||||
| `g:nuwiki_no_text_objects_mappings` | `0` | `1` skips the text-object group (`ah`, `il`, …) |
|
| `g:nuwiki_no_text_objects_mappings` | `0` | `1` skips the `text_objects` group (`ah`, `ih`, `aH`, `iH`, `al`, `il`, `a\`, `i\`, `ac`, `ic`) |
|
||||||
| `g:nuwiki_no_folding` | `0` | `1` skips foldexpr setup |
|
| `g:nuwiki_no_folding` | `0` | `1` skips foldexpr setup |
|
||||||
| `g:nuwiki_mouse_mappings` | `0` | `1` enables mouse maps |
|
| `g:nuwiki_mouse_mappings` | `0` | `1` enables mouse maps |
|
||||||
|
| `g:nuwiki_binary_path` | _(auto)_ | path to a prebuilt `nuwiki-ls` binary, bypassing the bundled one |
|
||||||
|
| `g:nuwiki_build_from_source` | `0` | `1` builds the binary with `cargo` instead of downloading a release |
|
||||||
|
| `g:nuwiki_no_calendar` | `0` | `1` opts out of calendar-vim integration |
|
||||||
|
| `g:nuwiki_auto_chdir` | `0` | `1` `:lcd` into the wiki root when a wiki buffer becomes current |
|
||||||
|
| `g:nuwiki_auto_header` | `0` | `1` insert a level-1 header from the filename on new wiki pages |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -399,7 +480,10 @@ available on both the Neovim and plain-Vim paths.
|
|||||||
| `:NuwikiNextLink` / `:NuwikiPrevLink` | `:VimwikiNextLink` / `:VimwikiPrevLink` | Jump to the next / previous wikilink |
|
| `:NuwikiNextLink` / `:NuwikiPrevLink` | `:VimwikiNextLink` / `:VimwikiPrevLink` | Jump to the next / previous wikilink |
|
||||||
| `:NuwikiBaddLink` | `:VimwikiBaddLink` | Add the link target under the cursor to the buffer list |
|
| `:NuwikiBaddLink` | `:VimwikiBaddLink` | Add the link target under the cursor to the buffer list |
|
||||||
| `:NuwikiNormalizeLink` | `:VimwikiNormalizeLink` | Wrap the word / visual selection under the cursor as a wikilink |
|
| `:NuwikiNormalizeLink` | `:VimwikiNormalizeLink` | Wrap the word / visual selection under the cursor as a wikilink |
|
||||||
| — | `:VimwikiSearch {pat}` (`:VWS`) | `:lvimgrep` `{pat}` across every page |
|
| `:NuwikiSearch {pat}` | `:VimwikiSearch {pat}` (`:VWS`) | `:lvimgrep` `{pat}` across the current wiki's pages (empty `{pat}` reuses the last search) |
|
||||||
|
| `:NuwikiVar` | `:VimwikiVar [key [val]]` | Get/set nuwiki client config |
|
||||||
|
| `:NuwikiReturn` | `:VimwikiReturn` | Command form of the smart `<CR>` list/table continuation |
|
||||||
|
| `:NuwikiShowVersion` | `:VimwikiShowVersion` | Print the nuwiki version + host editor |
|
||||||
|
|
||||||
**Diary**
|
**Diary**
|
||||||
|
|
||||||
@@ -425,10 +509,10 @@ available on both the Neovim and plain-Vim paths.
|
|||||||
| Nuwiki | Vimwiki | Action |
|
| Nuwiki | Vimwiki | Action |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `:NuwikiNextTask` | `:VimwikiNextTask` | Jump to the next unfinished task |
|
| `:NuwikiNextTask` | `:VimwikiNextTask` | Jump to the next unfinished task |
|
||||||
| `:NuwikiToggleListItem` | `:VimwikiToggleListItem` | Toggle the checkbox under the cursor |
|
| `:NuwikiToggleListItem` | `:VimwikiToggleListItem` | Toggle the checkbox under the cursor (`-range`: every item in the selection) |
|
||||||
| `:NuwikiToggleRejected` | `:VimwikiToggleRejectedListItem` | Toggle the rejected marker `[-]` |
|
| `:NuwikiToggleRejected` | `:VimwikiToggleRejectedListItem` | Toggle the rejected marker `[-]` |
|
||||||
| `:NuwikiListToggle` | `:VimwikiListToggle` | Toggle a checkbox on the current item (adds `[ ]` if none) |
|
| `:NuwikiListToggle` | `:VimwikiListToggle` | Toggle a checkbox on the current item (adds `[ ]` if none) |
|
||||||
| `:NuwikiIncrementListItem` / `:NuwikiDecrementListItem` | `:VimwikiIncrementListItem` / `:VimwikiDecrementListItem` | Cycle the list marker to the next / previous symbol |
|
| `:NuwikiIncrementListItem` / `:NuwikiDecrementListItem` | `:VimwikiIncrementListItem` / `:VimwikiDecrementListItem` | Cycle the list marker to the next / previous symbol (`-range`: every item in the selection) |
|
||||||
| `:NuwikiChangeSymbol {sym}` | `:VimwikiChangeSymbolTo {sym}` (`:VimwikiListChangeSymbolI`) | Set the current item's marker to `{sym}` |
|
| `:NuwikiChangeSymbol {sym}` | `:VimwikiChangeSymbolTo {sym}` (`:VimwikiListChangeSymbolI`) | Set the current item's marker to `{sym}` |
|
||||||
| `:NuwikiChangeSymbolInList {sym}` | `:VimwikiChangeSymbolInListTo {sym}` | Set every item in the list to `{sym}` |
|
| `:NuwikiChangeSymbolInList {sym}` | `:VimwikiChangeSymbolInListTo {sym}` | Set every item in the list to `{sym}` |
|
||||||
| `:NuwikiRemoveDone[!]` | `:VimwikiRemoveDone[!]` | Remove every completed item from the current list; with `!`, from the whole buffer |
|
| `:NuwikiRemoveDone[!]` | `:VimwikiRemoveDone[!]` | Remove every completed item from the current list; with `!`, from the whole buffer |
|
||||||
@@ -447,9 +531,9 @@ available on both the Neovim and plain-Vim paths.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `:NuwikiTOC` | `:VimwikiTOC` | Generate / refresh the table of contents on the current page |
|
| `:NuwikiTOC` | `:VimwikiTOC` | Generate / refresh the table of contents on the current page |
|
||||||
| `:NuwikiGenerateLinks` | `:VimwikiGenerateLinks` | Insert a flat list of every page in the wiki |
|
| `:NuwikiGenerateLinks` | `:VimwikiGenerateLinks` | Insert a flat list of every page in the wiki |
|
||||||
| `:NuwikiCheckLinks` | `:VimwikiCheckLinks` | Send broken links to the quickfix list |
|
| `:NuwikiCheckLinks` | `:VimwikiCheckLinks` | Send broken links to the quickfix list (`-range`: limit to the current buffer's selected lines) |
|
||||||
| `:NuwikiFindOrphans` | `:VimwikiFindOrphans` | List pages with no incoming links in the quickfix list |
|
| `:NuwikiFindOrphans` | `:VimwikiFindOrphans` | List pages with no incoming links in the quickfix list |
|
||||||
| `:NuwikiRebuildTags` | `:VimwikiRebuildTags` | Force a full workspace re-index |
|
| `:NuwikiRebuildTags[!]` | `:VimwikiRebuildTags[!]` | Re-index the current wiki; `!` re-indexes every configured wiki |
|
||||||
| `:NuwikiSearchTags {tag}` | `:VimwikiSearchTags {tag}` | Quickfix listing every occurrence of `:tag:` |
|
| `:NuwikiSearchTags {tag}` | `:VimwikiSearchTags {tag}` | Quickfix listing every occurrence of `:tag:` |
|
||||||
| `:NuwikiGenerateTagLinks [tag]` | `:VimwikiGenerateTagLinks [tag]` (`:VimwikiGenerateTags`) | Insert a section linking every page that has `<tag>` |
|
| `:NuwikiGenerateTagLinks [tag]` | `:VimwikiGenerateTagLinks [tag]` (`:VimwikiGenerateTags`) | Insert a section linking every page that has `<tag>` |
|
||||||
| `:NuwikiColorize {color}` | `:VimwikiColorize {color}` | Wrap the word / visual selection in a coloured `<span>` |
|
| `:NuwikiColorize {color}` | `:VimwikiColorize {color}` | Wrap the word / visual selection in a coloured `<span>` |
|
||||||
@@ -479,7 +563,8 @@ via `mappings.<group> = false` in Neovim, or
|
|||||||
| `<CR>` | Smart follow — inside `[[…]]` jumps; on a bare word wraps as `[[word]]` (second `<CR>` follows) |
|
| `<CR>` | Smart follow — inside `[[…]]` jumps; on a bare word wraps as `[[word]]` (second `<CR>` follows) |
|
||||||
| `<S-CR>` | Follow in horizontal split |
|
| `<S-CR>` | Follow in horizontal split |
|
||||||
| `<C-CR>` | Follow in vertical split |
|
| `<C-CR>` | Follow in vertical split |
|
||||||
| `<C-S-CR>` | Follow in a tab, reusing an existing tab if the file is already open |
|
| `<C-S-CR>` / `<D-CR>` | Follow in a tab, reusing an existing tab if the file is already open (`<D-CR>` is the macOS Cmd alias) |
|
||||||
|
| `<M-CR>` | Add the link target to the buffer list (`:badd`, no jump) |
|
||||||
| `<BS>` | Jump back (`<C-o>`) |
|
| `<BS>` | Jump back (`<C-o>`) |
|
||||||
| `<Tab>` / `<S-Tab>` | Next / previous wikilink on or after the cursor |
|
| `<Tab>` / `<S-Tab>` | Next / previous wikilink on or after the cursor |
|
||||||
| `+` | Wrap word / visual selection as a wikilink (no follow) |
|
| `+` | Wrap word / visual selection as a wikilink (no follow) |
|
||||||
@@ -512,6 +597,7 @@ vimwiki) they fire only after `'timeoutlen'`; type a suffix such as `gln` or
|
|||||||
| Key | Action |
|
| Key | Action |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `<CR>` | Continue the list with the same marker (preserves checkbox), or break out on an empty bullet |
|
| `<CR>` | Continue the list with the same marker (preserves checkbox), or break out on an empty bullet |
|
||||||
|
| `<S-CR>` | Multiline list item continuation (no new marker) |
|
||||||
| `<C-D>` / `<C-T>` | Dedent / indent the current item |
|
| `<C-D>` / `<C-T>` | Dedent / indent the current item |
|
||||||
| `<C-L><C-J>` / `<C-L><C-K>` | Cycle the marker style (`-` → `*` → `#` → `1.` → `1)` → `a)` → `A)` → `i)` → `I)`) |
|
| `<C-L><C-J>` / `<C-L><C-K>` | Cycle the marker style (`-` → `*` → `#` → `1.` → `1)` → `a)` → `A)` → `i)` → `I)`) |
|
||||||
| `<C-L><C-M>` | Toggle / add a checkbox on the current item |
|
| `<C-L><C-M>` | Toggle / add a checkbox on the current item |
|
||||||
@@ -536,14 +622,17 @@ vimwiki) they fire only after `'timeoutlen'`; type a suffix such as `gln` or
|
|||||||
|
|
||||||
**Wiki / diary / export** (normal mode)
|
**Wiki / diary / export** (normal mode)
|
||||||
|
|
||||||
|
The `<Leader>w` prefix below is the default `map_prefix` (`g:nuwiki_map_prefix`); change that option to relocate the whole family.
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `<Leader>ww` | Open the wiki index |
|
| `<Leader>ww` | Open the wiki index |
|
||||||
| `<Leader>wt` | Open the wiki index in a new tab |
|
| `<Leader>wt` | Open the wiki index in a new tab |
|
||||||
| `<Leader>ws` | Pick a wiki |
|
| `<Leader>ws` | Pick a wiki |
|
||||||
| `<Leader>wi` | Open the diary index |
|
| `<Leader>wi` | Open the diary index |
|
||||||
| `<Leader>w<Leader>w` / `<Leader>w<Leader>y` / `<Leader>w<Leader>t` | Diary today / yesterday / tomorrow |
|
| `<Leader>w<Leader>w` / `<Leader>w<Leader>y` | Diary today / yesterday |
|
||||||
| `<Leader>w<Leader>m` | Diary tomorrow (vimwiki-compat alias of `<Leader>w<Leader>t`) |
|
| `<Leader>w<Leader>t` | Diary today, in a new tab |
|
||||||
|
| `<Leader>w<Leader>m` | Diary tomorrow |
|
||||||
| `<Leader>w<Leader>i` | Rebuild the diary index page |
|
| `<Leader>w<Leader>i` | Rebuild the diary index page |
|
||||||
| `<Leader>wn` / `<Leader>wd` / `<Leader>wr` | Goto / delete / rename page |
|
| `<Leader>wn` / `<Leader>wd` / `<Leader>wr` | Goto / delete / rename page |
|
||||||
| `<Leader>wh` / `<Leader>whh` | Export the current page to HTML / open in browser |
|
| `<Leader>wh` / `<Leader>whh` | Export the current page to HTML / open in browser |
|
||||||
@@ -587,14 +676,30 @@ match vimwiki. To migrate:
|
|||||||
|
|
||||||
1. Drop the original `vimwiki` plugin from your config.
|
1. Drop the original `vimwiki` plugin from your config.
|
||||||
2. Install nuwiki.
|
2. Install nuwiki.
|
||||||
3. Point `wiki_root` (or each `wikis[i].root`) at your existing
|
3. Keep your existing config, or point `wiki_root` / `wikis[i].root` at
|
||||||
directory.
|
your directory.
|
||||||
|
|
||||||
|
**Existing `g:vimwiki_list` config works as-is (Vim & Neovim).** When you
|
||||||
|
haven't configured nuwiki natively (`g:nuwiki_wikis` / `setup()` / `g:nuwiki_*`),
|
||||||
|
both clients read your upstream `g:vimwiki_list` and `g:vimwiki_*` globals and
|
||||||
|
translate them: each wiki's `path`/`path_html`/`template_*`/`auto_export`
|
||||||
|
plus globals like `toc_header_level`, `html_header_numbering`,
|
||||||
|
`html_header_numbering_sym`, `links_space_char`, and `list_margin`.
|
||||||
|
Native config always takes precedence. (Settings nuwiki implements
|
||||||
|
differently — see [`known-issues.md`](./known-issues.md) — are ignored.)
|
||||||
|
On Neovim with lazy.nvim, swap the `vimwiki` plugin spec for nuwiki and
|
||||||
|
call `require('nuwiki').setup()` — your `vim.g.vimwiki_list` is picked up
|
||||||
|
unchanged.
|
||||||
|
|
||||||
Existing pages, diary entries, tags, and templates work without
|
Existing pages, diary entries, tags, and templates work without
|
||||||
modification. The first workspace scan after launch may show an empty
|
modification. The first workspace scan after launch may show an empty
|
||||||
`:VimwikiCheckLinks` result until the background index completes;
|
`:VimwikiCheckLinks` result until the background index completes;
|
||||||
subsequent runs are instant.
|
subsequent runs are instant.
|
||||||
|
|
||||||
|
A few upstream options are implemented differently or not at all — by
|
||||||
|
design, since nuwiki is LSP-backed rather than pure VimL. See
|
||||||
|
[`known-issues.md`](./known-issues.md) for the full list of divergences.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
" autoload/nuwiki/colors.vim — conceal colour spans down to their text.
|
||||||
|
"
|
||||||
|
" `:VimwikiColorize NAME` wraps text in `<span style="color:NAME">TEXT</span>`.
|
||||||
|
" This hides the two tags (conceal) and paints TEXT in NAME, so the buffer
|
||||||
|
" shows just the coloured word — matching how the HTML export renders it.
|
||||||
|
"
|
||||||
|
" Pure syntax + `:highlight`, no LSP needed, so it works the same in Vim,
|
||||||
|
" coc.nvim, and Neovim. New colours appear as the user runs :VimwikiColorize,
|
||||||
|
" so the ftplugin re-runs refresh() on text changes; we track which colours
|
||||||
|
" already have a syntax group in b:nuwiki_color_seen to stay idempotent.
|
||||||
|
|
||||||
|
" Scan the buffer for `<span style="color:…">` and define a concealing,
|
||||||
|
" colouring syntax region for each colour not yet seen.
|
||||||
|
function! nuwiki#colors#refresh() abort
|
||||||
|
if !exists('b:nuwiki_color_seen')
|
||||||
|
let b:nuwiki_color_seen = {}
|
||||||
|
endif
|
||||||
|
let l:n = line('$')
|
||||||
|
let l:lnum = 1
|
||||||
|
while l:lnum <= l:n
|
||||||
|
let l:line = getline(l:lnum)
|
||||||
|
if stridx(l:line, '<span style="color:') >= 0
|
||||||
|
let l:pos = 0
|
||||||
|
while 1
|
||||||
|
let l:mp = matchstrpos(l:line, '<span style="color:[^"]\{-}">', l:pos)
|
||||||
|
if l:mp[1] < 0
|
||||||
|
break
|
||||||
|
endif
|
||||||
|
let l:pos = l:mp[2]
|
||||||
|
call s:define(matchstr(l:mp[0], 'color:\zs[^"]\{-}\ze">'))
|
||||||
|
endwhile
|
||||||
|
endif
|
||||||
|
let l:lnum += 1
|
||||||
|
endwhile
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Define (once per colour) a region that conceals `<span …>` / `</span>` and
|
||||||
|
" paints the wrapped text in that colour.
|
||||||
|
function! s:define(color) abort
|
||||||
|
if a:color ==# '' || has_key(b:nuwiki_color_seen, a:color)
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let b:nuwiki_color_seen[a:color] = 1
|
||||||
|
" Only handle colours Vim can actually allocate: a #hex value or an
|
||||||
|
" alphabetic name. Anything else — rgb()/hsl() functions, or a literal
|
||||||
|
" `<span style="color:…">` that turns up in prose/examples — is skipped, so
|
||||||
|
" a stray match can't create a bogus conceal region or abort the refresh.
|
||||||
|
if a:color !~# '^#\x\{3,8}$' && a:color !~# '^[A-Za-z][A-Za-z0-9]*$'
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:grp = 'nuwikiColorSpan_' . substitute(a:color, '[^A-Za-z0-9]', '_', 'g')
|
||||||
|
let l:esc = escape(a:color, '/\.*$^~[]')
|
||||||
|
" Clear first so re-runs / load-order never stack duplicate regions.
|
||||||
|
silent! execute 'syntax clear ' . l:grp
|
||||||
|
" `concealends` hides the start/end matches (the tags); the region body
|
||||||
|
" (the text) keeps the l:grp highlight. `oneline` since spans don't wrap.
|
||||||
|
execute 'syntax region ' . l:grp
|
||||||
|
\ . ' matchgroup=Conceal'
|
||||||
|
\ . ' start=/<span style="color:' . l:esc . '">/'
|
||||||
|
\ . ' end=#</span>#'
|
||||||
|
\ . ' oneline keepend concealends'
|
||||||
|
" guifg works for names and #hex under 'termguicolors'; ctermfg only for
|
||||||
|
" named colours. Both are best-effort — `silent!` swallows E254 for a name
|
||||||
|
" Vim doesn't know (the tags still conceal, the text just isn't recoloured).
|
||||||
|
silent! execute 'highlight default ' . l:grp . ' guifg=' . a:color
|
||||||
|
if a:color !~# '^#'
|
||||||
|
silent! execute 'highlight ' . l:grp . ' ctermfg=' . a:color
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
+387
-52
@@ -168,6 +168,133 @@ endfunction
|
|||||||
" from the server, so the picker works from any buffer before a wiki file is
|
" from the server, so the picker works from any buffer before a wiki file is
|
||||||
" ever opened — opening the chosen index then auto-starts the LSP via the
|
" ever opened — opening the chosen index then auto-starts the LSP via the
|
||||||
" FileType autocmd. Plain Vim has no `vim.ui.select`, so use `inputlist()`.
|
" FileType autocmd. Plain Vim has no `vim.ui.select`, so use `inputlist()`.
|
||||||
|
" Absolute root (with trailing slash) of the wiki that owns `a:file`, or the
|
||||||
|
" first configured wiki as a fallback. Used by auto_chdir + search scoping.
|
||||||
|
" Absolute root (trailing slash) of the wiki that owns `a:file`, or '' when the
|
||||||
|
" file is under no configured wiki (callers no-op / fall back to CWD).
|
||||||
|
function! nuwiki#commands#wiki_root_for(file) abort
|
||||||
|
for l:w in nuwiki#commands#wiki_list()
|
||||||
|
let l:wroot = fnamemodify(expand(l:w.root), ':p')
|
||||||
|
if l:wroot[len(l:wroot) - 1:] !=# '/' | let l:wroot .= '/' | endif
|
||||||
|
if a:file[: len(l:wroot) - 1] ==# l:wroot
|
||||||
|
return l:wroot
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
return ''
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" :NuwikiAutoChdir hook — `:lcd` into the owning wiki's root (vimwiki
|
||||||
|
" `auto_chdir`). Wired from a buffer-local autocmd in the ftplugin.
|
||||||
|
function! nuwiki#commands#auto_chdir() abort
|
||||||
|
let l:root = nuwiki#commands#wiki_root_for(expand('%:p'))
|
||||||
|
if l:root !=# ''
|
||||||
|
execute 'lcd ' . fnameescape(l:root)
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" vimwiki `auto_header`: on a new wiki page, insert a level-1 header derived
|
||||||
|
" from the filename. Skips the wiki index + diary index pages; only acts on an
|
||||||
|
" empty new buffer. Wired from a BufNewFile autocmd.
|
||||||
|
function! nuwiki#commands#auto_header() abort
|
||||||
|
if line('$') > 1 || getline(1) !=# ''
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:file = expand('%:p')
|
||||||
|
let l:space = ' '
|
||||||
|
for l:c in nuwiki#commands#wiki_list()
|
||||||
|
let l:root = fnamemodify(expand(l:c.root), ':p')
|
||||||
|
if l:root[len(l:root) - 1:] !=# '/' | let l:root .= '/' | endif
|
||||||
|
if l:file[: len(l:root) - 1] ==# l:root
|
||||||
|
let l:index_path = l:root . l:c.idx . l:c.ext
|
||||||
|
let l:diary_index_path = l:root . l:c.diary_rel . '/' . l:c.diary_idx . l:c.ext
|
||||||
|
if l:file ==# l:index_path || l:file ==# l:diary_index_path
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:space = get(l:c, 'space', ' ')
|
||||||
|
break
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
" Convert links_space_char back to spaces for the title (vimwiki parity).
|
||||||
|
let l:title = expand('%:t:r')
|
||||||
|
if l:space !=# ' ' && l:space !=# ''
|
||||||
|
let l:title = substitute(l:title, '\V' . escape(l:space, '\'), ' ', 'g')
|
||||||
|
endif
|
||||||
|
call setline(1, '= ' . l:title . ' =')
|
||||||
|
call append(1, '')
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" :VimwikiShowVersion / :NuwikiShowVersion — echo the nuwiki version and the
|
||||||
|
" host editor, mirroring upstream's version banner.
|
||||||
|
function! nuwiki#commands#show_version() abort
|
||||||
|
echo 'nuwiki: ' . get(g:, 'nuwiki_version', '?')
|
||||||
|
echo (has('nvim') ? 'Neovim' : 'Vim') . ': ' . v:version
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" :VimwikiSearch / :VWS {pattern} — search the current wiki's files (upstream's
|
||||||
|
" search scopes to the wiki, not the editor's CWD). Resolves the wiki whose root
|
||||||
|
" contains the current file; falls back to the first configured wiki, then to
|
||||||
|
" the CWD. An empty pattern reuses the last search (`@/`). Populates the
|
||||||
|
" location list.
|
||||||
|
function! nuwiki#commands#search(args) abort
|
||||||
|
let l:pat = a:args ==# '' ? @/ : a:args
|
||||||
|
if l:pat ==# ''
|
||||||
|
echohl WarningMsg | echom 'nuwiki: no search pattern' | echohl None
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
" Resolve { root, ext } for the current buffer's wiki.
|
||||||
|
let l:file = expand('%:p')
|
||||||
|
let l:root = ''
|
||||||
|
let l:ext = get(g:, 'nuwiki_file_extension', '.wiki')
|
||||||
|
for l:w in nuwiki#commands#wiki_list()
|
||||||
|
let l:wroot = fnamemodify(expand(l:w.root), ':p')
|
||||||
|
if l:file[: len(l:wroot) - 1] ==# l:wroot
|
||||||
|
let l:root = l:wroot
|
||||||
|
let l:ext = get(l:w, 'ext', l:ext)
|
||||||
|
break
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
if l:root ==# '' && !empty(nuwiki#commands#wiki_list())
|
||||||
|
let l:first = nuwiki#commands#wiki_list()[0]
|
||||||
|
let l:root = fnamemodify(expand(l:first.root), ':p')
|
||||||
|
let l:ext = get(l:first, 'ext', l:ext)
|
||||||
|
endif
|
||||||
|
let l:ext = l:ext[0] ==# '.' ? l:ext : '.' . l:ext
|
||||||
|
let l:glob = l:root ==# '' ? '**' : fnameescape(l:root) . '**/*' . l:ext
|
||||||
|
try
|
||||||
|
execute 'lvimgrep /' . escape(l:pat, '/') . '/j ' . l:glob
|
||||||
|
catch /E480/
|
||||||
|
echohl WarningMsg | echom 'nuwiki: no match for ' . l:pat | echohl None
|
||||||
|
return
|
||||||
|
endtry
|
||||||
|
lopen
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" :VimwikiVar / :NuwikiVar [key [value]] — get/set the nuwiki client globals
|
||||||
|
" (g:nuwiki_*), mirroring upstream's vimwiki#vars#cmd. No arg prints every
|
||||||
|
" g:nuwiki_* var; one arg prints that var; two+ sets it (as a string — server-
|
||||||
|
" backed settings need a restart to take effect, which we note).
|
||||||
|
function! nuwiki#commands#var(arg) abort
|
||||||
|
let l:parts = split(a:arg, '\s\+')
|
||||||
|
if empty(l:parts)
|
||||||
|
let l:keys = sort(filter(keys(g:), 'v:val =~# "^nuwiki_"'))
|
||||||
|
if empty(l:keys)
|
||||||
|
echo 'nuwiki: no g:nuwiki_* variables set'
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
for l:k in l:keys
|
||||||
|
echo printf('g:%s = %s', l:k, string(g:[l:k]))
|
||||||
|
endfor
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:key = 'nuwiki_' . substitute(l:parts[0], '^nuwiki_', '', '')
|
||||||
|
if len(l:parts) == 1
|
||||||
|
echo printf('g:%s = %s', l:key, exists('g:' . l:key) ? string(g:[l:key]) : '(unset)')
|
||||||
|
else
|
||||||
|
let g:[l:key] = join(l:parts[1:], ' ')
|
||||||
|
echo printf('set g:%s = %s (restart for server-backed settings)', l:key, string(g:[l:key]))
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#wiki_ui_select() abort
|
function! nuwiki#commands#wiki_ui_select() abort
|
||||||
let l:wikis = nuwiki#commands#wiki_list()
|
let l:wikis = nuwiki#commands#wiki_list()
|
||||||
if empty(l:wikis)
|
if empty(l:wikis)
|
||||||
@@ -419,32 +546,37 @@ endfunction
|
|||||||
|
|
||||||
" ===== Diary =====
|
" ===== Diary =====
|
||||||
|
|
||||||
function! s:diary_open(cmd_name, ...) abort
|
" `a:open` is the window command ('edit'/'tabedit'); `a:count` is vimwiki's
|
||||||
let l:open = a:0 >= 1 ? a:1 : 'edit'
|
" diary `-count` — a wiki number (1-indexed at the user level). When > 0 the
|
||||||
call s:exec(a:cmd_name, [{ 'uri': s:buf_uri() }],
|
" server acts on that wiki instead of the buffer's.
|
||||||
\ {n -> s:open_uri_from(n, l:open)})
|
function! s:diary_open(cmd_name, open, count) abort
|
||||||
|
let l:arg = { 'uri': s:buf_uri() }
|
||||||
|
if a:count > 0
|
||||||
|
let l:arg['wiki'] = a:count - 1
|
||||||
|
endif
|
||||||
|
call s:exec(a:cmd_name, [l:arg], {n -> s:open_uri_from(n, a:open)})
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#diary_today() abort
|
function! nuwiki#commands#diary_today(...) abort
|
||||||
call s:diary_open('nuwiki.diary.openToday')
|
call s:diary_open('nuwiki.diary.openToday', 'edit', a:0 >= 1 ? a:1 : 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_today_tab() abort
|
function! nuwiki#commands#diary_today_tab(...) abort
|
||||||
call s:diary_open('nuwiki.diary.openToday', 'tabedit')
|
call s:diary_open('nuwiki.diary.openToday', 'tabedit', a:0 >= 1 ? a:1 : 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_yesterday() abort
|
function! nuwiki#commands#diary_yesterday(...) abort
|
||||||
call s:diary_open('nuwiki.diary.openYesterday')
|
call s:diary_open('nuwiki.diary.openYesterday', 'edit', a:0 >= 1 ? a:1 : 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_tomorrow() abort
|
function! nuwiki#commands#diary_tomorrow(...) abort
|
||||||
call s:diary_open('nuwiki.diary.openTomorrow')
|
call s:diary_open('nuwiki.diary.openTomorrow', 'edit', a:0 >= 1 ? a:1 : 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_index() abort
|
function! nuwiki#commands#diary_index(...) abort
|
||||||
call s:diary_open('nuwiki.diary.openIndex')
|
call s:diary_open('nuwiki.diary.openIndex', 'edit', a:0 >= 1 ? a:1 : 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_next() abort
|
function! nuwiki#commands#diary_next() abort
|
||||||
call s:diary_open('nuwiki.diary.next')
|
call s:diary_open('nuwiki.diary.next', 'edit', 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_prev() abort
|
function! nuwiki#commands#diary_prev() abort
|
||||||
call s:diary_open('nuwiki.diary.prev')
|
call s:diary_open('nuwiki.diary.prev', 'edit', 0)
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#diary_generate_index() abort
|
function! nuwiki#commands#diary_generate_index() abort
|
||||||
call s:exec('nuwiki.diary.generateIndex', [{ 'uri': s:buf_uri() }])
|
call s:exec('nuwiki.diary.generateIndex', [{ 'uri': s:buf_uri() }])
|
||||||
@@ -456,14 +588,13 @@ endfunction
|
|||||||
" open their wiki from any buffer. Files are opened directly from config;
|
" open their wiki from any buffer. Files are opened directly from config;
|
||||||
" the LSP auto-starts via the FileType autocmd once the buffer loads.
|
" the LSP auto-starts via the FileType autocmd once the buffer loads.
|
||||||
|
|
||||||
" Return a dict with {root, ext, idx, diary_rel, diary_idx} for wiki #n
|
" Return a dict with {root, ext, idx, diary_rel, diary_idx, space} for wiki #n
|
||||||
" (0-based). Prefers g:nuwiki_wikis[n]; falls back to scalar g:nuwiki_*
|
" (0-based). Resolves through nuwiki#config#wikis() — so g:nuwiki_wikis AND an
|
||||||
" vars and finally to built-in defaults.
|
" 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
|
function! s:wiki_cfg(n) abort
|
||||||
let l:w = {}
|
let l:wikis = nuwiki#config#wikis()
|
||||||
if exists('g:nuwiki_wikis') && len(g:nuwiki_wikis) > a:n
|
let l:w = (a:n >= 0 && a:n < len(l:wikis)) ? l:wikis[a:n] : {}
|
||||||
let l:w = g:nuwiki_wikis[a:n]
|
|
||||||
endif
|
|
||||||
let l:root = expand(get(l:w, 'root',
|
let l:root = expand(get(l:w, 'root',
|
||||||
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
||||||
let l:ext = get(l:w, 'file_extension',
|
let l:ext = get(l:w, 'file_extension',
|
||||||
@@ -473,19 +604,22 @@ function! s:wiki_cfg(n) abort
|
|||||||
let l:diary_rel = get(l:w, 'diary_rel_path',
|
let l:diary_rel = get(l:w, 'diary_rel_path',
|
||||||
\ get(g:, 'nuwiki_diary_rel_path', 'diary'))
|
\ get(g:, 'nuwiki_diary_rel_path', 'diary'))
|
||||||
let l:diary_idx = get(l:w, 'diary_index', '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,
|
return { 'root': l:root, 'ext': l:ext, 'idx': l:idx,
|
||||||
\ 'diary_rel': l:diary_rel, 'diary_idx': l:diary_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,
|
||||||
" diary_idx} dicts, straight from config — no LSP round-trip. Drives the wiki
|
" diary_idx, space} dicts, straight from config — no LSP round-trip. Drives the
|
||||||
" picker so it works before any wiki buffer (and therefore the server) exists.
|
" wiki picker so it works before any wiki buffer (and therefore the server)
|
||||||
" Falls back to a single entry from the scalar `g:nuwiki_wiki_root` config.
|
" exists. Falls back to a single entry from the scalar `g:nuwiki_wiki_root`.
|
||||||
function! nuwiki#commands#wiki_list() abort
|
function! nuwiki#commands#wiki_list() abort
|
||||||
|
let l:wikis = nuwiki#config#wikis()
|
||||||
let l:list = []
|
let l:list = []
|
||||||
if exists('g:nuwiki_wikis') && !empty(g:nuwiki_wikis)
|
if !empty(l:wikis)
|
||||||
let l:n = 0
|
let l:n = 0
|
||||||
for l:w in g:nuwiki_wikis
|
for l:w in l:wikis
|
||||||
let l:c = s:wiki_cfg(l:n)
|
let l:c = s:wiki_cfg(l:n)
|
||||||
let l:c['name'] = get(l:w, 'name', fnamemodify(l:c.root, ':t'))
|
let l:c['name'] = get(l:w, 'name', fnamemodify(l:c.root, ':t'))
|
||||||
call add(l:list, l:c)
|
call add(l:list, l:c)
|
||||||
@@ -529,6 +663,29 @@ endfunction
|
|||||||
function! nuwiki#commands#toggle_list_item() abort
|
function! nuwiki#commands#toggle_list_item() abort
|
||||||
call s:exec_pos('nuwiki.list.toggleCheckbox')
|
call s:exec_pos('nuwiki.list.toggleCheckbox')
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" Apply a per-line action across [l1, l2] (visual-range commands). Each call
|
||||||
|
" sends its own request for its line; the server's edits are version-less and
|
||||||
|
" single-line, so applying them as they return never conflicts.
|
||||||
|
function! s:over_range(l1, l2, Fn) abort
|
||||||
|
let l:save = getcurpos()
|
||||||
|
for l:ln in range(a:l1, a:l2)
|
||||||
|
call cursor(l:ln, 1)
|
||||||
|
call a:Fn()
|
||||||
|
endfor
|
||||||
|
call setpos('.', l:save)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! nuwiki#commands#toggle_list_item_range(l1, l2) abort
|
||||||
|
call s:over_range(a:l1, a:l2, function('nuwiki#commands#toggle_list_item'))
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! nuwiki#commands#list_cycle_symbol_range(direction, l1, l2) abort
|
||||||
|
call s:over_range(a:l1, a:l2, { -> nuwiki#commands#list_cycle_symbol(a:direction) })
|
||||||
|
endfunction
|
||||||
|
function! nuwiki#commands#reject_list_item_range(l1, l2) abort
|
||||||
|
call s:over_range(a:l1, a:l2, function('nuwiki#commands#reject_list_item'))
|
||||||
|
endfunction
|
||||||
function! nuwiki#commands#cycle_list_item() abort
|
function! nuwiki#commands#cycle_list_item() abort
|
||||||
call s:exec_pos('nuwiki.list.cycleCheckbox')
|
call s:exec_pos('nuwiki.list.cycleCheckbox')
|
||||||
endfunction
|
endfunction
|
||||||
@@ -568,14 +725,31 @@ endfunction
|
|||||||
" ===== Generation + workspace =====
|
" ===== Generation + workspace =====
|
||||||
|
|
||||||
function! nuwiki#commands#toc_generate() abort
|
function! nuwiki#commands#toc_generate() abort
|
||||||
call s:exec('nuwiki.toc.generate', [{'uri': s:buf_uri()}])
|
" Send the cursor line (0-based) so a fresh TOC is inserted there.
|
||||||
|
call s:exec('nuwiki.toc.generate',
|
||||||
|
\ [{'uri': s:buf_uri(), 'line': line('.') - 1}])
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#links_generate() abort
|
" `:VimwikiGenerateLinks [rel_path]` — with no arg, list every page; with a
|
||||||
call s:exec('nuwiki.links.generate', [{'uri': s:buf_uri()}])
|
" rel-path arg, scope the generated links to that subtree (server-side
|
||||||
|
" `generateForPath`). Matches upstream's optional path argument.
|
||||||
|
function! nuwiki#commands#links_generate(...) abort
|
||||||
|
let l:path = a:0 >= 1 ? a:1 : ''
|
||||||
|
" Cursor line (0-based) so a fresh section is inserted there.
|
||||||
|
let l:line = line('.') - 1
|
||||||
|
if l:path !=# ''
|
||||||
|
call s:exec('nuwiki.links.generateForPath',
|
||||||
|
\ [{'uri': s:buf_uri(), 'path': l:path, 'line': l:line}])
|
||||||
|
else
|
||||||
|
call s:exec('nuwiki.links.generate',
|
||||||
|
\ [{'uri': s:buf_uri(), 'line': l:line}])
|
||||||
|
endif
|
||||||
endfunction
|
endfunction
|
||||||
function! nuwiki#commands#check_links() abort
|
function! nuwiki#commands#check_links(...) abort
|
||||||
|
" a:1 = range count; when >0, restrict the report to the current buffer's
|
||||||
|
" [a:2, a:3] lines (`:'<,'>VimwikiCheckLinks`). No range → whole workspace.
|
||||||
|
let l:rng = (a:0 >= 3 && a:1 > 0) ? [a:2, a:3] : []
|
||||||
call s:exec('nuwiki.workspace.checkLinks', [{ 'uri': s:buf_uri() }],
|
call s:exec('nuwiki.workspace.checkLinks', [{ 'uri': s:buf_uri() }],
|
||||||
\ {n -> s:results_to_qf(n, 'broken links')})
|
\ {n -> s:results_to_qf(n, 'broken links', l:rng)})
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#find_orphans() abort
|
function! nuwiki#commands#find_orphans() abort
|
||||||
@@ -583,7 +757,10 @@ function! nuwiki#commands#find_orphans() abort
|
|||||||
\ {n -> s:results_to_qf(n, 'orphans')})
|
\ {n -> s:results_to_qf(n, 'orphans')})
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:results_to_qf(notification, title) abort
|
function! s:results_to_qf(notification, title, ...) abort
|
||||||
|
" Optional a:1 = [line1, line2]: keep only current-buffer rows in that range.
|
||||||
|
let l:rng = a:0 >= 1 ? a:1 : []
|
||||||
|
let l:cur = empty(l:rng) ? '' : resolve(expand('%:p'))
|
||||||
let l:rows = get(get(a:notification, 'response', {}), 'result', [])
|
let l:rows = get(get(a:notification, 'response', {}), 'result', [])
|
||||||
if type(l:rows) != type([]) || empty(l:rows)
|
if type(l:rows) != type([]) || empty(l:rows)
|
||||||
echom 'nuwiki: no ' . a:title
|
echom 'nuwiki: no ' . a:title
|
||||||
@@ -598,9 +775,18 @@ function! s:results_to_qf(notification, title) abort
|
|||||||
let l:lnum = l:row['range']['start']['line'] + 1
|
let l:lnum = l:row['range']['start']['line'] + 1
|
||||||
let l:col = l:row['range']['start']['character'] + 1
|
let l:col = l:row['range']['start']['character'] + 1
|
||||||
endif
|
endif
|
||||||
|
if !empty(l:rng)
|
||||||
|
if resolve(l:path) !=# l:cur || l:lnum < l:rng[0] || l:lnum > l:rng[1]
|
||||||
|
continue
|
||||||
|
endif
|
||||||
|
endif
|
||||||
let l:text = get(l:row, 'message', get(l:row, 'name', ''))
|
let l:text = get(l:row, 'message', get(l:row, 'name', ''))
|
||||||
call add(l:items, { 'filename': l:path, 'lnum': l:lnum, 'col': l:col, 'text': l:text })
|
call add(l:items, { 'filename': l:path, 'lnum': l:lnum, 'col': l:col, 'text': l:text })
|
||||||
endfor
|
endfor
|
||||||
|
if empty(l:items)
|
||||||
|
echom 'nuwiki: no ' . a:title
|
||||||
|
return
|
||||||
|
endif
|
||||||
call setqflist([], ' ', { 'title': 'nuwiki ' . a:title, 'items': l:items })
|
call setqflist([], ' ', { 'title': 'nuwiki ' . a:title, 'items': l:items })
|
||||||
copen
|
copen
|
||||||
endfunction
|
endfunction
|
||||||
@@ -617,15 +803,19 @@ function! nuwiki#commands#tags_search(query) abort
|
|||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#tags_generate_links(tag) abort
|
function! nuwiki#commands#tags_generate_links(tag) abort
|
||||||
let l:args = { 'uri': s:buf_uri() }
|
" Cursor line (0-based) so a fresh section is inserted there.
|
||||||
|
let l:args = { 'uri': s:buf_uri(), 'line': line('.') - 1 }
|
||||||
if a:tag !=# ''
|
if a:tag !=# ''
|
||||||
let l:args['tag'] = a:tag
|
let l:args['tag'] = a:tag
|
||||||
endif
|
endif
|
||||||
call s:exec('nuwiki.tags.generateLinks', [l:args])
|
call s:exec('nuwiki.tags.generateLinks', [l:args])
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#tags_rebuild() abort
|
function! nuwiki#commands#tags_rebuild(...) abort
|
||||||
call s:exec('nuwiki.tags.rebuild', [{ 'uri': s:buf_uri() }])
|
" a:1 (the command's <bang>) → re-index every configured wiki.
|
||||||
|
let l:all = a:0 >= 1 && a:1
|
||||||
|
call s:exec('nuwiki.tags.rebuild',
|
||||||
|
\ [{ 'uri': s:buf_uri(), 'all': l:all ? v:true : v:false }])
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" ===== HTML export =====
|
" ===== HTML export =====
|
||||||
@@ -706,6 +896,26 @@ function! nuwiki#commands#list_remove_done_all() abort
|
|||||||
call s:exec('nuwiki.list.removeDone', l:args)
|
call s:exec('nuwiki.list.removeDone', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" `:'<,'>VimwikiRemoveDone` — remove done items only within the given line
|
||||||
|
" range (1-indexed, inclusive; converted to the server's 0-indexed range).
|
||||||
|
function! nuwiki#commands#list_remove_done_range(l1, l2) abort
|
||||||
|
let l:args = [{ 'uri': s:buf_uri(), 'range': [a:l1 - 1, a:l2 - 1] }]
|
||||||
|
call s:exec('nuwiki.list.removeDone', l:args)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" `:[range]VimwikiRemoveDone[!]` dispatcher. `!` → whole buffer; an explicit
|
||||||
|
" range (`a:range > 0`, e.g. `:'<,'>`) → that line span; otherwise the
|
||||||
|
" current list under the cursor. Mirrors upstream's `-bang -range` form.
|
||||||
|
function! nuwiki#commands#remove_done(bang, range, l1, l2) abort
|
||||||
|
if a:bang
|
||||||
|
call nuwiki#commands#list_remove_done_all()
|
||||||
|
elseif a:range > 0
|
||||||
|
call nuwiki#commands#list_remove_done_range(a:l1, a:l2)
|
||||||
|
else
|
||||||
|
call nuwiki#commands#list_remove_done()
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
" `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
" `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
||||||
function! nuwiki#commands#list_remove_checkbox() abort
|
function! nuwiki#commands#list_remove_checkbox() abort
|
||||||
let l:p = s:cursor_position()
|
let l:p = s:cursor_position()
|
||||||
@@ -751,6 +961,13 @@ function! nuwiki#commands#list_change_symbol(symbol, whole_list) abort
|
|||||||
call s:exec('nuwiki.list.changeSymbol', l:args)
|
call s:exec('nuwiki.list.changeSymbol', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" `:[range]VimwikiChangeSymbolTo {sym}` / `:[range]VimwikiListChangeSymbolI {sym}`
|
||||||
|
" — upstream is `-range -nargs=1`; a range sets the marker on every list item in
|
||||||
|
" the selection (single-item when no range, since line1==line2==cursor line).
|
||||||
|
function! nuwiki#commands#list_change_symbol_range(symbol, l1, l2) abort
|
||||||
|
call s:over_range(a:l1, a:l2, { -> nuwiki#commands#list_change_symbol(a:symbol, 0) })
|
||||||
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#list_change_level(delta, whole_subtree) abort
|
function! nuwiki#commands#list_change_level(delta, whole_subtree) abort
|
||||||
let l:p = s:cursor_position()
|
let l:p = s:cursor_position()
|
||||||
let l:args = [{
|
let l:args = [{
|
||||||
@@ -762,11 +979,15 @@ function! nuwiki#commands#list_change_level(delta, whole_subtree) abort
|
|||||||
call s:exec('nuwiki.list.changeLevel', l:args)
|
call s:exec('nuwiki.list.changeLevel', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" `:VimwikiListChangeLvl decrease|increase 0` compat entry.
|
" `:[range]VimwikiListChangeLvl {increase|decrease} [plus_children]` — upstream
|
||||||
function! nuwiki#commands#list_change_lvl(...) abort
|
" is `-range -nargs=+`: direction is required, the optional second arg cascades
|
||||||
let l:dir = a:0 >= 1 ? a:1 : 'increase'
|
" the level change to each item's subtree, and a range applies it to every list
|
||||||
let l:delta = (l:dir ==# 'increase' || l:dir ==# 'indent') ? 1 : -1
|
" item in the selection.
|
||||||
call nuwiki#commands#list_change_level(l:delta, 0)
|
function! nuwiki#commands#list_change_lvl(line1, line2, direction, ...) abort
|
||||||
|
let l:delta = (a:direction ==# 'increase' || a:direction ==# 'indent') ? 1 : -1
|
||||||
|
let l:children = a:0 >= 1 ? (str2nr(a:1) != 0) : 0
|
||||||
|
call s:over_range(a:line1, a:line2,
|
||||||
|
\ { -> nuwiki#commands#list_change_level(l:delta, l:children) })
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Cycle the list marker through vimwiki's canonical order.
|
" Cycle the list marker through vimwiki's canonical order.
|
||||||
@@ -812,10 +1033,10 @@ endfunction
|
|||||||
" Cluster B — table rewriters.
|
" Cluster B — table rewriters.
|
||||||
|
|
||||||
function! nuwiki#commands#table_insert(...) abort
|
function! nuwiki#commands#table_insert(...) abort
|
||||||
let l:cols = a:0 >= 1 ? str2nr(a:1) : 3
|
let l:cols = a:0 >= 1 ? str2nr(a:1) : 5
|
||||||
let l:rows = a:0 >= 2 ? str2nr(a:2) : 2
|
let l:rows = a:0 >= 2 ? str2nr(a:2) : 2
|
||||||
if l:cols <= 0
|
if l:cols <= 0
|
||||||
let l:cols = 3
|
let l:cols = 5
|
||||||
endif
|
endif
|
||||||
if l:rows <= 0
|
if l:rows <= 0
|
||||||
let l:rows = 2
|
let l:rows = 2
|
||||||
@@ -836,6 +1057,18 @@ function! nuwiki#commands#table_align() abort
|
|||||||
call s:exec('nuwiki.table.align', l:args)
|
call s:exec('nuwiki.table.align', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" Upstream `gqq` (AlignQ) / `gww` (AlignW): on a table row, align the table;
|
||||||
|
" otherwise fall back to the native Vim format command (`normal! gqq` keeps the
|
||||||
|
" cursor free; `gww` keeps the cursor in place). `normal!` bypasses our own
|
||||||
|
" mapping so there's no recursion.
|
||||||
|
function! nuwiki#commands#table_align_or_cmd(cmd) abort
|
||||||
|
if s:is_table_row(getline('.'))
|
||||||
|
call nuwiki#commands#table_align()
|
||||||
|
else
|
||||||
|
execute 'normal! ' . a:cmd
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
function! nuwiki#commands#table_move_left() abort
|
function! nuwiki#commands#table_move_left() abort
|
||||||
let l:p = s:cursor_position()
|
let l:p = s:cursor_position()
|
||||||
let l:args = [{
|
let l:args = [{
|
||||||
@@ -855,18 +1088,53 @@ function! nuwiki#commands#table_move_right() abort
|
|||||||
\ }]
|
\ }]
|
||||||
call s:exec('nuwiki.table.moveColumn', l:args)
|
call s:exec('nuwiki.table.moveColumn', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
" `:VimwikiColorize {color}` — pure-VimL wrap as inline span tag,
|
" `:VimwikiColorize {color}` — wrap text in an inline colour span, matching
|
||||||
" matching vimwiki's `<span style="color:%c">%s</span>` template.
|
" vimwiki's `<span style="color:%c">%s</span>` template. Wraps the visual
|
||||||
|
" selection when invoked with a range (`:'<,'>VimwikiColorize` / the `x`-mode
|
||||||
|
" <Leader>wc mapping), otherwise the word under the cursor.
|
||||||
|
" a:1 = colour (empty → prompt); a:2 = range count (>0 → use the selection).
|
||||||
function! nuwiki#commands#colorize(...) abort
|
function! nuwiki#commands#colorize(...) abort
|
||||||
let l:color = a:0 >= 1 ? a:1 : ''
|
let l:color = a:0 >= 1 ? a:1 : ''
|
||||||
|
let l:visual = a:0 >= 2 && a:2 > 0
|
||||||
if l:color ==# ''
|
if l:color ==# ''
|
||||||
let l:color = input('Colour: ')
|
let l:color = input('Colour: ')
|
||||||
endif
|
endif
|
||||||
if l:color ==# ''
|
if l:color ==# ''
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
let l:open_tag = '<span style="color:' . l:color . '">'
|
" vimwiki `color_tag_template`: split on __CONTENT__ into open/close, with
|
||||||
let l:close_tag = '</span>'
|
" __COLORFG__ replaced by the colour. Configurable via g:nuwiki_color_tag_template.
|
||||||
|
let l:tpl = get(g:, 'nuwiki_color_tag_template',
|
||||||
|
\ '<span style="color:__COLORFG__">__CONTENT__</span>')
|
||||||
|
" Escape `\`, `&`, `~` so a colour containing them isn't treated as a
|
||||||
|
" substitute() replacement metacharacter.
|
||||||
|
let l:tpl = substitute(l:tpl, '__COLORFG__', escape(l:color, '\&~'), 'g')
|
||||||
|
let l:parts = split(l:tpl, '__CONTENT__', 1)
|
||||||
|
let l:open_tag = get(l:parts, 0, '')
|
||||||
|
let l:close_tag = get(l:parts, 1, '')
|
||||||
|
|
||||||
|
if l:visual
|
||||||
|
let l:sp = getpos("'<")
|
||||||
|
let l:ep = getpos("'>")
|
||||||
|
if l:sp[1] > 0 && l:sp[1] == l:ep[1]
|
||||||
|
let l:line = getline(l:sp[1])
|
||||||
|
let l:sc = l:sp[2]
|
||||||
|
let l:ec = l:ep[2]
|
||||||
|
if l:ec > strlen(l:line)
|
||||||
|
let l:ec = strlen(l:line)
|
||||||
|
endif
|
||||||
|
if l:ec >= l:sc
|
||||||
|
let l:new = strpart(l:line, 0, l:sc - 1)
|
||||||
|
\ . l:open_tag . strpart(l:line, l:sc - 1, l:ec - l:sc + 1) . l:close_tag
|
||||||
|
\ . strpart(l:line, l:ec)
|
||||||
|
call setline(l:sp[1], l:new)
|
||||||
|
call nuwiki#colors#refresh()
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
" Multi-line or empty selection: fall through to the cword.
|
||||||
|
endif
|
||||||
|
|
||||||
let l:line = getline('.')
|
let l:line = getline('.')
|
||||||
let l:col = col('.')
|
let l:col = col('.')
|
||||||
" Word boundaries around cursor (1-based).
|
" Word boundaries around cursor (1-based).
|
||||||
@@ -886,6 +1154,7 @@ function! nuwiki#commands#colorize(...) abort
|
|||||||
\ . l:open_tag . l:word . l:close_tag
|
\ . l:open_tag . l:word . l:close_tag
|
||||||
\ . strpart(l:line, l:right - 1)
|
\ . strpart(l:line, l:right - 1)
|
||||||
call setline('.', l:new)
|
call setline('.', l:new)
|
||||||
|
call nuwiki#colors#refresh()
|
||||||
endfunction
|
endfunction
|
||||||
" Cluster C — link helpers.
|
" Cluster C — link helpers.
|
||||||
|
|
||||||
@@ -1070,6 +1339,8 @@ endfunction
|
|||||||
" returned string is fed into Vim's input stream after the function
|
" returned string is fed into Vim's input stream after the function
|
||||||
" finishes, keeping the undo block coherent.
|
" finishes, keeping the undo block coherent.
|
||||||
function! nuwiki#commands#smart_return() abort
|
function! nuwiki#commands#smart_return() abort
|
||||||
|
" Accept the completion (plain <CR>) when the popup menu is open, like upstream.
|
||||||
|
if pumvisible() | return "\<CR>" | endif
|
||||||
let l:line = getline('.')
|
let l:line = getline('.')
|
||||||
" `<expr>` runs under textlock, so the function must be side-effect-
|
" `<expr>` runs under textlock, so the function must be side-effect-
|
||||||
" free on the buffer. Buffer-mutating branches return a `<Cmd>:call
|
" free on the buffer. Buffer-mutating branches return a `<Cmd>:call
|
||||||
@@ -1112,6 +1383,39 @@ function! nuwiki#commands#smart_return() abort
|
|||||||
return "\<CR>"
|
return "\<CR>"
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" Insert-mode `<S-CR>`: continue the current list item on a new line WITHOUT a
|
||||||
|
" new marker, indented to align under the item's text — vimwiki's "multiline
|
||||||
|
" list item" (`VimwikiReturn 2 2`). Off a list item it's a plain `<CR>`.
|
||||||
|
function! nuwiki#commands#smart_shift_return() abort
|
||||||
|
if pumvisible() | return "\<CR>" | endif
|
||||||
|
let l:line = getline('.')
|
||||||
|
let l:m = matchlist(l:line, '^\(\s*\)\([-*#]\)\s\(.*\)$')
|
||||||
|
if empty(l:m)
|
||||||
|
let l:m2 = matchlist(l:line, '^\(\s*\)\(\d\+\)\([.)]\)\s\(.*\)$')
|
||||||
|
if !empty(l:m2)
|
||||||
|
let l:m = [l:line, l:m2[1], l:m2[2] . l:m2[3], l:m2[4]]
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
if empty(l:m)
|
||||||
|
return "\<CR>"
|
||||||
|
endif
|
||||||
|
let l:indent = l:m[1]
|
||||||
|
let l:marker = l:m[2]
|
||||||
|
let l:has_cb = l:m[3] =~# '^\[[ xXoO.\-]\]'
|
||||||
|
let l:pad = len(l:marker) + 1 + (l:has_cb ? 4 : 0)
|
||||||
|
let l:auto = &autoindent || &smartindent || &cindent || !empty(&indentexpr)
|
||||||
|
return "\<CR>" . (l:auto ? '' : l:indent) . repeat(' ', l:pad)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" :VimwikiReturn / :NuwikiReturn — the command form of the smart `<CR>`
|
||||||
|
" continuation (upstream's mapping-driven `VimwikiReturn`). Enters insert at
|
||||||
|
" end of line and triggers the buffer-local `<CR>` mapping (smart_return),
|
||||||
|
" continuing the list item / table row. Args mirror upstream's mode flags and
|
||||||
|
" are accepted but unused (the continuation is inferred from the line).
|
||||||
|
function! nuwiki#commands#return_cmd(...) abort
|
||||||
|
call feedkeys("A\<CR>", 'm')
|
||||||
|
endfunction
|
||||||
|
|
||||||
" Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
" Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||||
" Used as `<expr>` mappings so we can pass `<Tab>` / `<S-Tab>` through
|
" Used as `<expr>` mappings so we can pass `<Tab>` / `<S-Tab>` through
|
||||||
" verbatim when the cursor isn't on a table row.
|
" verbatim when the cursor isn't on a table row.
|
||||||
@@ -1177,15 +1481,46 @@ function! nuwiki#commands#smart_shift_tab() abort
|
|||||||
return "\<Cmd>call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\<CR>"
|
return "\<Cmd>call nuwiki#commands#_table_jump_to_cell(" . line('.') . ", " . l:target_idx . ")\<CR>"
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
" `:VimwikiNormalizeLink [1]` — wrap text as `[[…]]` without following.
|
||||||
" without following. Pure-VimL.
|
" With a:1 (upstream's visual `1` arg, also the `x`-mode mapping) wrap the
|
||||||
function! nuwiki#commands#normalize_link() abort
|
" `'<`/`'>` selection; otherwise the word under the cursor. Pure-VimL.
|
||||||
|
function! nuwiki#commands#normalize_link(...) abort
|
||||||
|
let l:visual = a:0 >= 1 && a:1
|
||||||
|
if l:visual
|
||||||
|
call s:wrap_visual_as_wikilink()
|
||||||
|
return
|
||||||
|
endif
|
||||||
if s:cursor_inside_wikilink()
|
if s:cursor_inside_wikilink()
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
call s:wrap_cword_as_wikilink()
|
call s:wrap_cword_as_wikilink()
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" Wrap the single-line `'<`/`'>` visual selection as `[[selection]]`.
|
||||||
|
function! s:wrap_visual_as_wikilink() abort
|
||||||
|
let l:sp = getpos("'<")
|
||||||
|
let l:ep = getpos("'>")
|
||||||
|
if l:sp[1] <= 0 || l:sp[1] != l:ep[1]
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:line = getline(l:sp[1])
|
||||||
|
let l:sc = l:sp[2]
|
||||||
|
let l:ec = l:ep[2]
|
||||||
|
if l:ec > strlen(l:line)
|
||||||
|
let l:ec = strlen(l:line)
|
||||||
|
endif
|
||||||
|
if l:ec < l:sc
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:sel = strpart(l:line, l:sc - 1, l:ec - l:sc + 1)
|
||||||
|
" Don't double-wrap if the selection is already a bracketed link.
|
||||||
|
if l:sel =~# '^\[\[.*\]\]$'
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:new = strpart(l:line, 0, l:sc - 1) . '[[' . l:sel . ']]' . strpart(l:line, l:ec)
|
||||||
|
call setline(l:sp[1], l:new)
|
||||||
|
endfunction
|
||||||
|
|
||||||
function! s:echo_url_from(response, ...) abort
|
function! s:echo_url_from(response, ...) abort
|
||||||
let l:result = get(get(a:response, 'response', {}), 'result', '')
|
let l:result = get(get(a:response, 'response', {}), 'result', '')
|
||||||
if type(l:result) == type('') && l:result !=# ''
|
if type(l:result) == type('') && l:result !=# ''
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
" autoload/nuwiki/complete.vim — command-line completion for the
|
||||||
|
" :Vimwiki*/:Nuwiki* commands (mirrors upstream vimwiki's -complete= specs).
|
||||||
|
"
|
||||||
|
" Pure client-side and synchronous: candidates come from config and the
|
||||||
|
" filesystem, never the LSP, so a `-complete=customlist,{fn}` attribute can
|
||||||
|
" call these directly under vim-lsp, coc, and Neovim alike.
|
||||||
|
"
|
||||||
|
" NOTE on files/RenameFile: upstream's :VimwikiRenameFile takes a new-name arg
|
||||||
|
" with file completion because upstream performs the rename itself. nuwiki
|
||||||
|
" delegates renaming to the LSP rename request (`:LspRename` / coc `rename`),
|
||||||
|
" which drives its own interactive prompt — there is no filename argument to
|
||||||
|
" complete, so no file completer is provided (documented divergence).
|
||||||
|
|
||||||
|
" Page-name completion (VimwikiGoto). Globs every configured wiki root for its
|
||||||
|
" `*<ext>` files and returns their root-relative names with the extension
|
||||||
|
" stripped, filtered by {arglead}. Mirrors upstream complete_links_raw.
|
||||||
|
function! nuwiki#complete#pages(arglead, cmdline, cursorpos) abort
|
||||||
|
let l:names = {}
|
||||||
|
for l:w in nuwiki#commands#wiki_list()
|
||||||
|
let l:root = substitute(get(l:w, 'root', ''), '/\+$', '', '')
|
||||||
|
if l:root ==# '' | continue | endif
|
||||||
|
let l:ext = get(l:w, 'ext', '.wiki')
|
||||||
|
for l:f in glob(l:root . '/**/*' . l:ext, 0, 1)
|
||||||
|
let l:rel = l:f[len(l:root) + 1 :]
|
||||||
|
if l:rel ==# '' | continue | endif
|
||||||
|
let l:rel = l:rel[: -len(l:ext) - 1]
|
||||||
|
let l:names[l:rel] = 1
|
||||||
|
endfor
|
||||||
|
endfor
|
||||||
|
return filter(sort(keys(l:names)), 'stridx(v:val, a:arglead) == 0')
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Colour completion (VimwikiColorize) — keys from any configured `color_dic`
|
||||||
|
" (per-wiki `g:nuwiki_wikis[].color_dic` or the scalar `g:nuwiki_color_dic`)
|
||||||
|
" plus colours already used in `color:NAME` spans in the current buffer.
|
||||||
|
function! nuwiki#complete#colors(arglead, cmdline, cursorpos) abort
|
||||||
|
let l:set = {}
|
||||||
|
for l:w in get(g:, 'nuwiki_wikis', [])
|
||||||
|
for l:k in keys(get(l:w, 'color_dic', {}))
|
||||||
|
let l:set[l:k] = 1
|
||||||
|
endfor
|
||||||
|
endfor
|
||||||
|
for l:k in keys(get(g:, 'nuwiki_color_dic', {}))
|
||||||
|
let l:set[l:k] = 1
|
||||||
|
endfor
|
||||||
|
for l:line in getline(1, '$')
|
||||||
|
let l:start = 0
|
||||||
|
while 1
|
||||||
|
let l:m = matchstrpos(l:line, 'color:\s*\zs[^"'';)]\+', l:start)
|
||||||
|
if l:m[1] < 0 | break | endif
|
||||||
|
let l:set[trim(l:m[0])] = 1
|
||||||
|
let l:start = l:m[2]
|
||||||
|
endwhile
|
||||||
|
endfor
|
||||||
|
return filter(sort(keys(l:set)), 'stridx(v:val, a:arglead) == 0')
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Tag completion (VimwikiSearchTags / VimwikiGenerateTagLinks). The tag index
|
||||||
|
" lives in the LSP server, which a synchronous completer can't query, so we
|
||||||
|
" harvest `:tag:` tokens by scanning the configured wikis' files directly —
|
||||||
|
" the same source the server indexes. Runs only on <Tab> at the `:` line.
|
||||||
|
function! nuwiki#complete#tags(arglead, cmdline, cursorpos) abort
|
||||||
|
let l:set = {}
|
||||||
|
for l:w in nuwiki#commands#wiki_list()
|
||||||
|
let l:root = substitute(get(l:w, 'root', ''), '/\+$', '', '')
|
||||||
|
if l:root ==# '' | continue | endif
|
||||||
|
let l:ext = get(l:w, 'ext', '.wiki')
|
||||||
|
for l:f in glob(l:root . '/**/*' . l:ext, 0, 1)
|
||||||
|
for l:line in readfile(l:f)
|
||||||
|
" Tags come in colon-delimited runs like `:foo:bar:` (no spaces).
|
||||||
|
" Match a whole run, then split out each tag (adjacent tags share a
|
||||||
|
" colon, so a per-tag scan would miss every other one).
|
||||||
|
let l:start = 0
|
||||||
|
while 1
|
||||||
|
let l:m = matchstrpos(l:line, ':\%([^:[:space:]]\+:\)\+', l:start)
|
||||||
|
if l:m[1] < 0 | break | endif
|
||||||
|
for l:t in split(l:m[0], ':')
|
||||||
|
if l:t !=# '' | let l:set[l:t] = 1 | endif
|
||||||
|
endfor
|
||||||
|
let l:start = l:m[2]
|
||||||
|
endwhile
|
||||||
|
endfor
|
||||||
|
endfor
|
||||||
|
endfor
|
||||||
|
return filter(sort(keys(l:set)), 'stridx(v:val, a:arglead) == 0')
|
||||||
|
endfunction
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
" autoload/nuwiki/config.vim — single source of truth for the configured wiki
|
||||||
|
" list, shared by the LSP payload (lsp.vim) and the buffer-side commands
|
||||||
|
" (commands.vim, diary.vim, complete.vim).
|
||||||
|
"
|
||||||
|
" Resolves wikis from g:nuwiki_wikis (native) or, when that's unset, an upstream
|
||||||
|
" g:vimwiki_list (drop-in compatibility), translating upstream key names and
|
||||||
|
" folding the global scalar defaults (g:vimwiki_* / g:nuwiki_*) into each entry.
|
||||||
|
" Everything downstream — `<Leader>ww`, link resolution, the server payload —
|
||||||
|
" then sees the same wikis.
|
||||||
|
|
||||||
|
" Per-wiki dict keys: upstream `g:vimwiki_list[i]` key -> nuwiki/server key.
|
||||||
|
let s:vimwiki_wiki_map = {
|
||||||
|
\ 'path': 'root',
|
||||||
|
\ 'path_html': 'html_path',
|
||||||
|
\ 'template_path': 'template_path',
|
||||||
|
\ 'template_default': 'template_default',
|
||||||
|
\ 'template_ext': 'template_ext',
|
||||||
|
\ 'css_name': 'css_name',
|
||||||
|
\ 'auto_export': 'auto_export',
|
||||||
|
\ 'auto_toc': 'auto_toc',
|
||||||
|
\ 'syntax': 'syntax',
|
||||||
|
\ 'ext': 'file_extension',
|
||||||
|
\ 'index': 'index',
|
||||||
|
\ 'diary_rel_path': 'diary_rel_path',
|
||||||
|
\ 'diary_index': 'diary_index',
|
||||||
|
\ 'name': 'name',
|
||||||
|
\ }
|
||||||
|
|
||||||
|
" Global scalar settings vimwiki applies to every wiki (via wikilocal
|
||||||
|
" defaults): upstream suffix (`g:vimwiki_<suffix>`) -> nuwiki/server key.
|
||||||
|
let s:scalar_global_map = {
|
||||||
|
\ 'toc_header': 'toc_header',
|
||||||
|
\ 'toc_header_level': 'toc_header_level',
|
||||||
|
\ 'toc_link_format': 'toc_link_format',
|
||||||
|
\ 'links_header': 'links_header',
|
||||||
|
\ 'links_header_level': 'links_header_level',
|
||||||
|
\ 'tags_header': 'tags_header',
|
||||||
|
\ 'tags_header_level': 'tags_header_level',
|
||||||
|
\ 'html_header_numbering': 'html_header_numbering',
|
||||||
|
\ 'html_header_numbering_sym': 'html_header_numbering_sym',
|
||||||
|
\ 'links_space_char': 'links_space_char',
|
||||||
|
\ 'list_margin': 'list_margin',
|
||||||
|
\ 'listsyms': 'listsyms',
|
||||||
|
\ 'listsym_rejected': 'listsym_rejected',
|
||||||
|
\ 'auto_export': 'auto_export',
|
||||||
|
\ 'auto_toc': 'auto_toc',
|
||||||
|
\ 'auto_generate_links': 'auto_generate_links',
|
||||||
|
\ 'auto_generate_tags': 'auto_generate_tags',
|
||||||
|
\ 'auto_diary_index': 'auto_diary_index',
|
||||||
|
\ }
|
||||||
|
|
||||||
|
" Resolve the per-wiki scalar globals: vimwiki values first (compat), then
|
||||||
|
" nuwiki-native `g:nuwiki_<key>` overrides on top.
|
||||||
|
function! nuwiki#config#scalar_globals() abort
|
||||||
|
let l:g = {}
|
||||||
|
for [l:src, l:dst] in items(s:scalar_global_map)
|
||||||
|
if exists('g:vimwiki_' . l:src)
|
||||||
|
let l:g[l:dst] = get(g:, 'vimwiki_' . l:src)
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
for l:dst in values(s:scalar_global_map)
|
||||||
|
if exists('g:nuwiki_' . l:dst)
|
||||||
|
let l:g[l:dst] = get(g:, 'nuwiki_' . l:dst)
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
return l:g
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Translate an upstream g:vimwiki_list into nuwiki's wiki list, folding the
|
||||||
|
" given global scalars into each entry. Returns [] when unset/malformed.
|
||||||
|
function! s:wikis_from_vimwiki(globals) abort
|
||||||
|
if !exists('g:vimwiki_list') || type(g:vimwiki_list) != type([]) || empty(g:vimwiki_list)
|
||||||
|
return []
|
||||||
|
endif
|
||||||
|
let l:wikis = []
|
||||||
|
for l:vw in g:vimwiki_list
|
||||||
|
if type(l:vw) != type({}) | continue | endif
|
||||||
|
let l:w = copy(a:globals)
|
||||||
|
for [l:src, l:dst] in items(s:vimwiki_wiki_map)
|
||||||
|
if has_key(l:vw, l:src)
|
||||||
|
let l:w[l:dst] = l:vw[l:src]
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
if has_key(l:w, 'root')
|
||||||
|
call add(l:wikis, l:w)
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
return l:wikis
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" The configured wikis as normalized dicts (nuwiki/server key names), with the
|
||||||
|
" global scalar defaults folded in (per-wiki value wins). Priority:
|
||||||
|
" 1. g:nuwiki_wikis (native)
|
||||||
|
" 2. g:vimwiki_list (upstream drop-in)
|
||||||
|
" Returns [] when neither is set — callers fall back to the single-wiki
|
||||||
|
" shorthand (g:nuwiki_wiki_root).
|
||||||
|
function! nuwiki#config#wikis() abort
|
||||||
|
let l:globals = nuwiki#config#scalar_globals()
|
||||||
|
if exists('g:nuwiki_wikis') && !empty(g:nuwiki_wikis)
|
||||||
|
return map(copy(g:nuwiki_wikis), {_, w -> extend(copy(l:globals), w, 'force')})
|
||||||
|
endif
|
||||||
|
return s:wikis_from_vimwiki(l:globals)
|
||||||
|
endfunction
|
||||||
+36
-10
@@ -3,6 +3,33 @@
|
|||||||
|
|
||||||
" ===== Calendar-Vim Integration =====
|
" ===== Calendar-Vim Integration =====
|
||||||
|
|
||||||
|
" The wiki index (0-based) of the most recently entered wiki buffer. The
|
||||||
|
" calendar shows / creates diary entries for this wiki, so opening the calendar
|
||||||
|
" while editing `ifood_wiki` diaries into ifood_wiki — not always the first
|
||||||
|
" wiki. Updated by the BufEnter autocmd in plugin/nuwiki.vim; defaults to 0.
|
||||||
|
" (The LSP-driven diary commands already follow the current buffer via its URI;
|
||||||
|
" this mirrors that for the VimL calendar-vim callbacks.)
|
||||||
|
let s:current_wiki_idx = 0
|
||||||
|
|
||||||
|
" Record the wiki that owns `file` as the current one. Called on entering a
|
||||||
|
" wiki buffer. No-op when the file is under no configured wiki.
|
||||||
|
function! nuwiki#diary#track_wiki(file) abort
|
||||||
|
let l:file = fnamemodify(a:file, ':p')
|
||||||
|
if empty(l:file)
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:n = 0
|
||||||
|
for l:w in nuwiki#commands#wiki_list()
|
||||||
|
let l:root = fnamemodify(expand(l:w.root), ':p')
|
||||||
|
if l:root[len(l:root) - 1:] !=# '/' | let l:root .= '/' | endif
|
||||||
|
if l:file[: len(l:root) - 1] ==# l:root
|
||||||
|
let s:current_wiki_idx = l:n
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let l:n += 1
|
||||||
|
endfor
|
||||||
|
endfunction
|
||||||
|
|
||||||
" Function: nuwiki#diary#calendar_action(day, month, year, week, dir)
|
" Function: nuwiki#diary#calendar_action(day, month, year, week, dir)
|
||||||
" Called by calendar-vim when user presses Enter on a date.
|
" Called by calendar-vim when user presses Enter on a date.
|
||||||
" day/month/year are numeric integers; dir is 'V' for a vertical split (any
|
" day/month/year are numeric integers; dir is 'V' for a vertical split (any
|
||||||
@@ -12,8 +39,8 @@ function! nuwiki#diary#calendar_action(day, month, year, week, dir) abort
|
|||||||
" Build YYYY-MM-DD from the parts passed by calendar-vim
|
" Build YYYY-MM-DD from the parts passed by calendar-vim
|
||||||
let l:date_str = printf('%04d-%02d-%02d', a:year, a:month, a:day)
|
let l:date_str = printf('%04d-%02d-%02d', a:year, a:month, a:day)
|
||||||
|
|
||||||
" Get wiki configuration for the first wiki (index 0)
|
" Wiki of the buffer the calendar was opened from (falls back to the first).
|
||||||
let l:c = s:wiki_cfg(0)
|
let l:c = s:wiki_cfg(s:current_wiki_idx)
|
||||||
|
|
||||||
" Construct file path: {root}/{diary_rel}/{YYYY-MM-DD}.{ext}
|
" Construct file path: {root}/{diary_rel}/{YYYY-MM-DD}.{ext}
|
||||||
let l:file_path = l:c.root . '/' . l:c.diary_rel . '/' . l:date_str . l:c.ext
|
let l:file_path = l:c.root . '/' . l:c.diary_rel . '/' . l:date_str . l:c.ext
|
||||||
@@ -62,8 +89,8 @@ function! nuwiki#diary#calendar_sign(day, month, year) abort
|
|||||||
" Build YYYY-MM-DD from the parts passed by calendar-vim
|
" Build YYYY-MM-DD from the parts passed by calendar-vim
|
||||||
let l:date_str = printf('%04d-%02d-%02d', a:year, a:month, a:day)
|
let l:date_str = printf('%04d-%02d-%02d', a:year, a:month, a:day)
|
||||||
|
|
||||||
" Get wiki configuration for the first wiki (index 0)
|
" Wiki of the buffer the calendar was opened from (falls back to the first).
|
||||||
let l:c = s:wiki_cfg(0)
|
let l:c = s:wiki_cfg(s:current_wiki_idx)
|
||||||
|
|
||||||
" Construct expected file path
|
" Construct expected file path
|
||||||
let l:file_path = l:c.root . '/' . l:c.diary_rel . '/' . l:date_str . l:c.ext
|
let l:file_path = l:c.root . '/' . l:c.diary_rel . '/' . l:date_str . l:c.ext
|
||||||
@@ -75,13 +102,12 @@ function! nuwiki#diary#calendar_sign(day, month, year) abort
|
|||||||
return ''
|
return ''
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Helper: get wiki configuration for wiki n (0-based).
|
" Helper: get wiki configuration for wiki n (0-based). Resolves through
|
||||||
" Mirrors the same logic used by nuwiki#commands#open_diary_path etc.
|
" 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
|
function! s:wiki_cfg(n) abort
|
||||||
let l:w = {}
|
let l:wikis = nuwiki#config#wikis()
|
||||||
if exists('g:nuwiki_wikis') && len(g:nuwiki_wikis) > a:n
|
let l:w = (a:n >= 0 && a:n < len(l:wikis)) ? l:wikis[a:n] : {}
|
||||||
let l:w = g:nuwiki_wikis[a:n]
|
|
||||||
endif
|
|
||||||
let l:root = expand(get(l:w, 'root',
|
let l:root = expand(get(l:w, 'root',
|
||||||
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
|
||||||
let l:ext = get(l:w, 'file_extension',
|
let l:ext = get(l:w, 'file_extension',
|
||||||
|
|||||||
+71
-22
@@ -45,22 +45,7 @@ function! nuwiki#lsp#start() abort
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
if exists(':CocConfig') == 2 || exists('*coc#config')
|
if exists(':CocConfig') == 2 || exists('*coc#config')
|
||||||
" coc.nvim — coc reads its server list from coc-settings.json.
|
call s:register_with_coc(l:bin)
|
||||||
" We can't inject it dynamically without owning the file, so just point
|
|
||||||
" the user at the snippet. `initializationOptions` carries wiki_root etc.
|
|
||||||
" to the server exactly like the vim-lsp registration above — without it
|
|
||||||
" the server resolves links against its default root, so existing pages
|
|
||||||
" look broken and follow-to-create lands in the wrong directory.
|
|
||||||
echohl WarningMsg
|
|
||||||
echom 'nuwiki: coc.nvim detected. Add this to your coc-settings.json:'
|
|
||||||
echom ' "languageserver": {'
|
|
||||||
echom ' "nuwiki": {'
|
|
||||||
echom ' "command": "' . l:bin . '",'
|
|
||||||
echom ' "filetypes": ["vimwiki"],'
|
|
||||||
echom ' "initializationOptions": ' . json_encode(s:settings())
|
|
||||||
echom ' }'
|
|
||||||
echom ' }'
|
|
||||||
echohl None
|
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -71,6 +56,66 @@ function! nuwiki#lsp#start() abort
|
|||||||
echohl None
|
echohl None
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" ===== coc.nvim integration =====
|
||||||
|
"
|
||||||
|
" Register the nuwiki language server with coc *programmatically* via
|
||||||
|
" coc#config, so users don't have to hand-maintain a coc-settings.json entry
|
||||||
|
" per wiki. The config (wiki roots, per-wiki keys, the vimwiki/nuwiki global
|
||||||
|
" shorthand) comes from s:settings() — the same payload the vim-lsp path sends.
|
||||||
|
" coc reacts to the `languageserver` config change, registers the server, and
|
||||||
|
" starts it for the open vimwiki buffer.
|
||||||
|
"
|
||||||
|
" Opt out with `let g:nuwiki_no_coc_register = 1` to manage the entry in
|
||||||
|
" coc-settings.json yourself (we then just print the snippet).
|
||||||
|
function! s:register_with_coc(bin) abort
|
||||||
|
if get(g:, 'nuwiki_no_coc_register', 0)
|
||||||
|
call s:print_coc_snippet(a:bin)
|
||||||
|
return
|
||||||
|
endif
|
||||||
|
let s:coc_bin = a:bin
|
||||||
|
if get(g:, 'coc_service_initialized', 0)
|
||||||
|
call s:coc_register()
|
||||||
|
else
|
||||||
|
" coc hasn't finished starting its node service yet — defer until it has,
|
||||||
|
" otherwise the updateConfig notification is sent to a service that isn't
|
||||||
|
" listening.
|
||||||
|
augroup nuwiki_coc_register
|
||||||
|
autocmd!
|
||||||
|
autocmd User CocNvimInit ++once call s:coc_register()
|
||||||
|
augroup END
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! s:coc_register() abort
|
||||||
|
let l:cfg = s:settings()
|
||||||
|
" Call coc#config directly — `exists('*coc#config')` can't be trusted because
|
||||||
|
" it doesn't trigger autoloading, so we let the call autoload coc.vim and
|
||||||
|
" fall back to the manual snippet only if coc genuinely isn't there.
|
||||||
|
try
|
||||||
|
call coc#config('languageserver.nuwiki', {
|
||||||
|
\ 'command': s:coc_bin,
|
||||||
|
\ 'filetypes': ['vimwiki'],
|
||||||
|
\ 'initializationOptions': l:cfg,
|
||||||
|
\ 'settings': { 'nuwiki': l:cfg },
|
||||||
|
\ })
|
||||||
|
catch /^Vim\%((\a\+)\)\=:E11[78]/
|
||||||
|
call s:print_coc_snippet(s:coc_bin)
|
||||||
|
endtry
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! s:print_coc_snippet(bin) abort
|
||||||
|
echohl WarningMsg
|
||||||
|
echom 'nuwiki: coc.nvim detected. Add this to your coc-settings.json:'
|
||||||
|
echom ' "languageserver": {'
|
||||||
|
echom ' "nuwiki": {'
|
||||||
|
echom ' "command": "' . a:bin . '",'
|
||||||
|
echom ' "filetypes": ["vimwiki"],'
|
||||||
|
echom ' "initializationOptions": ' . json_encode(s:settings())
|
||||||
|
echom ' }'
|
||||||
|
echom ' }'
|
||||||
|
echohl None
|
||||||
|
endfunction
|
||||||
|
|
||||||
function! s:bin_path() abort
|
function! s:bin_path() abort
|
||||||
if exists('g:nuwiki_binary_path') && filereadable(g:nuwiki_binary_path)
|
if exists('g:nuwiki_binary_path') && filereadable(g:nuwiki_binary_path)
|
||||||
return g:nuwiki_binary_path
|
return g:nuwiki_binary_path
|
||||||
@@ -89,12 +134,16 @@ function! s:settings() abort
|
|||||||
\ 'link_severity': get(g:, 'nuwiki_link_severity', 'warn'),
|
\ 'link_severity': get(g:, 'nuwiki_link_severity', 'warn'),
|
||||||
\ },
|
\ },
|
||||||
\ }
|
\ }
|
||||||
" Include the per-wiki list when configured via g:nuwiki_wikis so the
|
" Wiki list comes from the shared resolver (nuwiki#config#wikis), so the
|
||||||
" server knows each wiki's root, diary path, etc. Without this it only
|
" server payload and the buffer-side commands agree on which wikis exist —
|
||||||
" sees wiki_root and resolves links relative to that directory instead
|
" native g:nuwiki_wikis or an upstream g:vimwiki_list, globals already folded.
|
||||||
" of the individual wiki's root.
|
let l:wikis = nuwiki#config#wikis()
|
||||||
if exists('g:nuwiki_wikis') && !empty(g:nuwiki_wikis)
|
if !empty(l:wikis)
|
||||||
let l:cfg['wikis'] = g:nuwiki_wikis
|
let l:cfg['wikis'] = l:wikis
|
||||||
|
else
|
||||||
|
" Single-wiki shorthand: fold the global scalars into the top-level payload
|
||||||
|
" so the server's single-wiki desugaring honours them.
|
||||||
|
call extend(l:cfg, nuwiki#config#scalar_globals(), 'force')
|
||||||
endif
|
endif
|
||||||
return l:cfg
|
return l:cfg
|
||||||
endfunction
|
endfunction
|
||||||
|
|||||||
@@ -6,6 +6,30 @@
|
|||||||
" Values are read from nuwiki's own config variables so users don't have
|
" Values are read from nuwiki's own config variables so users don't have
|
||||||
" to duplicate settings.
|
" to duplicate settings.
|
||||||
|
|
||||||
|
" Resolve which wiki a `get_wikilocal` call refers to, mirroring upstream:
|
||||||
|
" - an explicit numeric second argument selects that wiki (0-based);
|
||||||
|
" - otherwise the wiki owning the current buffer is used
|
||||||
|
" (upstream's `g:vimwiki_current_idx`);
|
||||||
|
" - failing that, the first/shorthand wiki (index 0).
|
||||||
|
function! s:resolve_index(...) abort
|
||||||
|
if a:0 >= 1 && type(a:1) == type(0)
|
||||||
|
return a:1
|
||||||
|
endif
|
||||||
|
let l:file = expand('%:p')
|
||||||
|
if !empty(l:file)
|
||||||
|
let l:n = 0
|
||||||
|
for l:w in nuwiki#commands#wiki_list()
|
||||||
|
let l:wroot = fnamemodify(expand(l:w.root), ':p')
|
||||||
|
if l:wroot[len(l:wroot) - 1:] !=# '/' | let l:wroot .= '/' | endif
|
||||||
|
if l:file[: len(l:wroot) - 1] ==# l:wroot
|
||||||
|
return l:n
|
||||||
|
endif
|
||||||
|
let l:n += 1
|
||||||
|
endfor
|
||||||
|
endif
|
||||||
|
return 0
|
||||||
|
endfunction
|
||||||
|
|
||||||
" Per-wiki local config values.
|
" Per-wiki local config values.
|
||||||
"
|
"
|
||||||
" Supported keys:
|
" Supported keys:
|
||||||
@@ -13,21 +37,38 @@
|
|||||||
" is_temporary_wiki — always 0 for a configured wiki
|
" is_temporary_wiki — always 0 for a configured wiki
|
||||||
" ext / extension — file extension (default '.wiki')
|
" ext / extension — file extension (default '.wiki')
|
||||||
" syntax — syntax name (default 'vimwiki')
|
" syntax — syntax name (default 'vimwiki')
|
||||||
|
"
|
||||||
|
" The optional second argument is the wiki index (upstream's signature);
|
||||||
|
" when omitted, the wiki owning the current buffer is used. Values are
|
||||||
|
" pulled from the matching `g:nuwiki_wikis` entry, falling back to the
|
||||||
|
" scalar `g:nuwiki_*` globals for keys that entry doesn't set.
|
||||||
function! vimwiki#vars#get_wikilocal(key, ...) abort
|
function! vimwiki#vars#get_wikilocal(key, ...) abort
|
||||||
|
if a:key ==# 'is_temporary_wiki'
|
||||||
|
return 0
|
||||||
|
endif
|
||||||
|
|
||||||
|
let l:idx = call('s:resolve_index', a:000)
|
||||||
|
let l:wikis = nuwiki#commands#wiki_list()
|
||||||
|
if l:idx < 0 || l:idx >= len(l:wikis)
|
||||||
|
let l:idx = 0
|
||||||
|
endif
|
||||||
|
let l:w = l:wikis[l:idx]
|
||||||
|
|
||||||
if a:key ==# 'path'
|
if a:key ==# 'path'
|
||||||
let l:root = expand(get(g:, 'nuwiki_wiki_root', '~/vimwiki'))
|
let l:root = expand(l:w.root)
|
||||||
" Ensure trailing slash to match what the original vimwiki returns.
|
" Ensure trailing slash to match what the original vimwiki returns.
|
||||||
return l:root =~# '/$' ? l:root : l:root . '/'
|
return l:root =~# '/$' ? l:root : l:root . '/'
|
||||||
|
|
||||||
elseif a:key ==# 'is_temporary_wiki'
|
|
||||||
return 0
|
|
||||||
|
|
||||||
elseif a:key ==# 'ext' || a:key ==# 'extension'
|
elseif a:key ==# 'ext' || a:key ==# 'extension'
|
||||||
let l:ext = get(g:, 'nuwiki_file_extension', '.wiki')
|
" wiki_list() already normalises the leading dot.
|
||||||
return l:ext[0] ==# '.' ? l:ext : '.' . l:ext
|
return l:w.ext
|
||||||
|
|
||||||
elseif a:key ==# 'syntax'
|
elseif a:key ==# 'syntax'
|
||||||
return get(g:, 'nuwiki_syntax', 'vimwiki')
|
" wiki_list() doesn't carry `syntax`; read the per-wiki entry, then the
|
||||||
|
" global default.
|
||||||
|
let l:cfg = (exists('g:nuwiki_wikis') && len(g:nuwiki_wikis) > l:idx)
|
||||||
|
\ ? g:nuwiki_wikis[l:idx] : {}
|
||||||
|
return get(l:cfg, 'syntax', get(g:, 'nuwiki_syntax', 'vimwiki'))
|
||||||
|
|
||||||
endif
|
endif
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ pub struct PreformattedNode {
|
|||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
|
/// Verbatim text after the opening `{{{`, trailing whitespace trimmed
|
||||||
|
/// (e.g. `python` or `class="brush: python"`). vimwiki inserts this as
|
||||||
|
/// raw attributes on the `<pre>` tag, so we preserve it for byte-parity.
|
||||||
|
pub attrs: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ pub enum Keyword {
|
|||||||
Stopped,
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Keyword {
|
||||||
|
/// The literal source text of the keyword (e.g. `"TODO"`). Used both for
|
||||||
|
/// the rendered span and for anchor/id derivation via [`inline_text`].
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Keyword::Todo => "TODO",
|
||||||
|
Keyword::Done => "DONE",
|
||||||
|
Keyword::Started => "STARTED",
|
||||||
|
Keyword::Fixme => "FIXME",
|
||||||
|
Keyword::Fixed => "FIXED",
|
||||||
|
Keyword::Xxx => "XXX",
|
||||||
|
Keyword::Stopped => "STOPPED",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum InlineNode {
|
pub enum InlineNode {
|
||||||
Text(TextNode),
|
Text(TextNode),
|
||||||
@@ -34,6 +50,77 @@ pub enum InlineNode {
|
|||||||
ExternalLink(ExternalLinkNode),
|
ExternalLink(ExternalLinkNode),
|
||||||
Transclusion(TransclusionNode),
|
Transclusion(TransclusionNode),
|
||||||
RawUrl(RawUrlNode),
|
RawUrl(RawUrlNode),
|
||||||
|
/// A soft line break joining two content lines within a paragraph or
|
||||||
|
/// list item. Consumers treat it as whitespace; the HTML renderer emits
|
||||||
|
/// a space (vimwiki `*_ignore_newline = 1`, the default) or `<br />`
|
||||||
|
/// (`= 0`).
|
||||||
|
SoftBreak(SoftBreakNode),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InlineNode {
|
||||||
|
/// The source [`Span`] this node covers. Every variant wraps a node with
|
||||||
|
/// its own `span` field; this is the one place that maps variant → span.
|
||||||
|
pub fn span(&self) -> Span {
|
||||||
|
match self {
|
||||||
|
InlineNode::Text(n) => n.span,
|
||||||
|
InlineNode::Bold(n) => n.span,
|
||||||
|
InlineNode::Italic(n) => n.span,
|
||||||
|
InlineNode::BoldItalic(n) => n.span,
|
||||||
|
InlineNode::Strikethrough(n) => n.span,
|
||||||
|
InlineNode::Code(n) => n.span,
|
||||||
|
InlineNode::Superscript(n) => n.span,
|
||||||
|
InlineNode::Subscript(n) => n.span,
|
||||||
|
InlineNode::MathInline(n) => n.span,
|
||||||
|
InlineNode::Keyword(n) => n.span,
|
||||||
|
InlineNode::Color(n) => n.span,
|
||||||
|
InlineNode::WikiLink(n) => n.span,
|
||||||
|
InlineNode::ExternalLink(n) => n.span,
|
||||||
|
InlineNode::Transclusion(n) => n.span,
|
||||||
|
InlineNode::RawUrl(n) => n.span,
|
||||||
|
InlineNode::SoftBreak(n) => n.span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Concatenate the plain-text content of a run of inlines, descending into
|
||||||
|
/// formatting containers. Links/external links use their description, falling
|
||||||
|
/// back to the target path / URL. This is the canonical heading/anchor text —
|
||||||
|
/// the HTML renderer uses it for heading `id`s and the LSP for anchor matching,
|
||||||
|
/// so the two never drift.
|
||||||
|
pub fn inline_text(inlines: &[InlineNode]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
push_inline_text(inlines, &mut out);
|
||||||
|
out.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_inline_text(inlines: &[InlineNode], out: &mut String) {
|
||||||
|
for n in inlines {
|
||||||
|
match n {
|
||||||
|
InlineNode::Text(t) => out.push_str(&t.content),
|
||||||
|
InlineNode::Bold(b) => push_inline_text(&b.children, out),
|
||||||
|
InlineNode::Italic(i) => push_inline_text(&i.children, out),
|
||||||
|
InlineNode::BoldItalic(bi) => push_inline_text(&bi.children, out),
|
||||||
|
InlineNode::Strikethrough(s) => push_inline_text(&s.children, out),
|
||||||
|
InlineNode::Code(c) => out.push_str(&c.content),
|
||||||
|
InlineNode::Superscript(s) => push_inline_text(&s.children, out),
|
||||||
|
InlineNode::Subscript(s) => push_inline_text(&s.children, out),
|
||||||
|
InlineNode::Color(c) => push_inline_text(&c.children, out),
|
||||||
|
InlineNode::Keyword(k) => out.push_str(k.keyword.label()),
|
||||||
|
InlineNode::WikiLink(w) => match &w.description {
|
||||||
|
Some(d) => push_inline_text(d, out),
|
||||||
|
None => {
|
||||||
|
if let Some(p) = &w.target.path {
|
||||||
|
out.push_str(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
InlineNode::ExternalLink(e) => match &e.description {
|
||||||
|
Some(d) => push_inline_text(d, out),
|
||||||
|
None => out.push_str(&e.url),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -42,6 +129,11 @@ pub struct TextNode {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SoftBreakNode {
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct BoldNode {
|
pub struct BoldNode {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
@@ -96,6 +188,15 @@ pub struct KeywordNode {
|
|||||||
pub keyword: Keyword,
|
pub keyword: Keyword,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A named colour span (`color` is a `color_dic` key, `children` the wrapped
|
||||||
|
/// inline content).
|
||||||
|
///
|
||||||
|
/// This is an **extension point**, not a node the vimwiki parser emits: in
|
||||||
|
/// the editor, colour comes from the `:Colorize` family writing literal
|
||||||
|
/// `<span style="color:…">` markup, which round-trips through export via
|
||||||
|
/// `valid_html_tags`. `ColorNode` exists so an alternate syntax (or a future
|
||||||
|
/// `[color]` markup) can feed the renderer's `color_dic` / `color_tag_template`
|
||||||
|
/// path directly; see `HtmlRenderer::render_color`.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ColorNode {
|
pub struct ColorNode {
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ pub use block::{
|
|||||||
TagScope,
|
TagScope,
|
||||||
};
|
};
|
||||||
pub use inline::{
|
pub use inline::{
|
||||||
BoldItalicNode, BoldNode, CodeNode, ColorNode, InlineNode, ItalicNode, Keyword, KeywordNode,
|
inline_text, BoldItalicNode, BoldNode, CodeNode, ColorNode, InlineNode, ItalicNode, Keyword,
|
||||||
MathInlineNode, StrikethroughNode, SubscriptNode, SuperscriptNode, TextNode,
|
KeywordNode, MathInlineNode, SoftBreakNode, StrikethroughNode, SubscriptNode, SuperscriptNode,
|
||||||
|
TextNode,
|
||||||
};
|
};
|
||||||
pub use link::{
|
pub use link::{
|
||||||
ExternalLinkNode, LinkKind, LinkTarget, RawUrlNode, TransclusionNode, WikiLinkNode,
|
ExternalLinkNode, LinkKind, LinkTarget, RawUrlNode, TransclusionNode, WikiLinkNode,
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ pub fn walk_inline<V: Visitor + ?Sized>(v: &mut V, node: &InlineNode) {
|
|||||||
InlineNode::ExternalLink(n) => v.visit_external_link(n),
|
InlineNode::ExternalLink(n) => v.visit_external_link(n),
|
||||||
InlineNode::Transclusion(n) => v.visit_transclusion(n),
|
InlineNode::Transclusion(n) => v.visit_transclusion(n),
|
||||||
InlineNode::RawUrl(n) => v.visit_raw_url(n),
|
InlineNode::RawUrl(n) => v.visit_raw_url(n),
|
||||||
|
// A soft break carries no content/children — treated as whitespace.
|
||||||
|
InlineNode::SoftBreak(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,132 @@ impl fmt::Display for DiaryPeriod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How a *weekly* diary entry is named.
|
||||||
|
///
|
||||||
|
/// `Iso` — ISO-week label `YYYY-Www` (nuwiki's original scheme; the week
|
||||||
|
/// always begins on Monday, `week_start` is ignored).
|
||||||
|
/// `Date` — the week-start day's calendar date `YYYY-MM-DD`, matching
|
||||||
|
/// upstream vimwiki; honours `week_start`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WeeklyStyle {
|
||||||
|
Iso,
|
||||||
|
Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeeklyStyle {
|
||||||
|
/// Parse the config string. `date`/`vimwiki` → `Date`; anything else
|
||||||
|
/// (including `iso`/`week`/empty) → `Iso`.
|
||||||
|
pub fn parse(s: &str) -> Self {
|
||||||
|
match s.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"date" | "vimwiki" | "day" => Self::Date,
|
||||||
|
_ => Self::Iso,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Day a week begins on, in ISO numbering (Monday = 1 … Sunday = 7).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct WeekStart(u8);
|
||||||
|
|
||||||
|
impl WeekStart {
|
||||||
|
/// Parse a weekday name (`monday`..`sunday`); unknown → Monday.
|
||||||
|
pub fn parse(s: &str) -> Self {
|
||||||
|
let n = match s.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"tuesday" => 2,
|
||||||
|
"wednesday" => 3,
|
||||||
|
"thursday" => 4,
|
||||||
|
"friday" => 5,
|
||||||
|
"saturday" => 6,
|
||||||
|
"sunday" => 7,
|
||||||
|
_ => 1, // monday (default)
|
||||||
|
};
|
||||||
|
Self(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn monday() -> Self {
|
||||||
|
Self(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iso(self) -> i64 {
|
||||||
|
self.0 as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diary navigation policy: frequency plus, for weekly diaries, the naming
|
||||||
|
/// scheme and week-start day. Built from a wiki's `diary_*` config so the
|
||||||
|
/// LSP commands can compute today / next / prev without re-deriving the
|
||||||
|
/// rules. `Daily`/`Monthly`/`Yearly` ignore the weekly fields.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct DiaryCalendar {
|
||||||
|
pub freq: DiaryFrequency,
|
||||||
|
pub weekly_style: WeeklyStyle,
|
||||||
|
pub week_start: WeekStart,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiaryCalendar {
|
||||||
|
pub fn new(freq: DiaryFrequency, weekly_style: WeeklyStyle, week_start: WeekStart) -> Self {
|
||||||
|
Self {
|
||||||
|
freq,
|
||||||
|
weekly_style,
|
||||||
|
week_start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_weekly_date(&self) -> bool {
|
||||||
|
self.freq == DiaryFrequency::Weekly && self.weekly_style == WeeklyStyle::Date
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Now" as a diary period for this calendar. Date-mode weekly snaps
|
||||||
|
/// today back to the most recent week-start day (a plain `Day`, so the
|
||||||
|
/// file is `YYYY-MM-DD` like upstream); everything else defers to
|
||||||
|
/// [`DiaryPeriod::today_utc`].
|
||||||
|
pub fn today(&self) -> DiaryPeriod {
|
||||||
|
if self.is_weekly_date() {
|
||||||
|
DiaryPeriod::Day(self.week_start_of(DiaryDate::today_utc()))
|
||||||
|
} else {
|
||||||
|
DiaryPeriod::today_utc(self.freq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The period one cadence-step after `p`.
|
||||||
|
pub fn next(&self, p: DiaryPeriod) -> DiaryPeriod {
|
||||||
|
self.step(p, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The period one cadence-step before `p`.
|
||||||
|
pub fn prev(&self, p: DiaryPeriod) -> DiaryPeriod {
|
||||||
|
self.step(p, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&self, p: DiaryPeriod, dir: i64) -> DiaryPeriod {
|
||||||
|
// Date-mode weekly: the period is a `Day` on a week-start; a step is
|
||||||
|
// ±7 days (re-snapped so an off-week-start pivot still lands cleanly).
|
||||||
|
if self.is_weekly_date() {
|
||||||
|
if let DiaryPeriod::Day(d) = p {
|
||||||
|
let start = self.week_start_of(d);
|
||||||
|
return DiaryPeriod::Day(add_days(start, 7 * dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dir >= 0 {
|
||||||
|
p.next()
|
||||||
|
} else {
|
||||||
|
p.prev()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The most recent `week_start` weekday on or before `d`.
|
||||||
|
fn week_start_of(&self, d: DiaryDate) -> DiaryDate {
|
||||||
|
let wd = iso_weekday(&d) as i64; // 1..=7
|
||||||
|
let back = (wd - self.week_start.iso()).rem_euclid(7);
|
||||||
|
add_days(d, -back)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shift a calendar date by `delta` days (may be negative).
|
||||||
|
fn add_days(d: DiaryDate, delta: i64) -> DiaryDate {
|
||||||
|
from_days(to_days(d.year, d.month, d.day) + delta)
|
||||||
|
}
|
||||||
|
|
||||||
/// Day-of-week with Monday=1 … Sunday=7 (ISO 8601).
|
/// Day-of-week with Monday=1 … Sunday=7 (ISO 8601).
|
||||||
fn iso_weekday(d: &DiaryDate) -> u8 {
|
fn iso_weekday(d: &DiaryDate) -> u8 {
|
||||||
// Zeller-style via days-from-epoch: 1970-01-01 was a Thursday.
|
// Zeller-style via days-from-epoch: 1970-01-01 was a Thursday.
|
||||||
|
|||||||
@@ -10,32 +10,47 @@
|
|||||||
|
|
||||||
use crate::ast::CheckboxState;
|
use crate::ast::CheckboxState;
|
||||||
|
|
||||||
|
/// Default rejected/cancelled marker (vimwiki's `listsym_rejected`).
|
||||||
const REJECTED: char = '-';
|
const REJECTED: char = '-';
|
||||||
|
|
||||||
/// An ordered checkbox progress palette. Always holds at least two glyphs.
|
/// An ordered checkbox progress palette. Always holds at least two glyphs.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ListSyms {
|
pub struct ListSyms {
|
||||||
progression: Vec<char>,
|
progression: Vec<char>,
|
||||||
|
rejected: char,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ListSyms {
|
impl Default for ListSyms {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
progression: " .oOX".chars().collect(),
|
progression: " .oOX".chars().collect(),
|
||||||
|
rejected: REJECTED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListSyms {
|
impl ListSyms {
|
||||||
/// Build a palette from a glyph string. Falls back to the default when
|
/// Build a palette from a glyph string, with the default rejected marker.
|
||||||
/// fewer than two glyphs are supplied (a one-symbol palette can't express
|
/// Falls back to the default when fewer than two glyphs are supplied (a
|
||||||
/// both "empty" and "done").
|
/// one-symbol palette can't express both "empty" and "done").
|
||||||
pub fn new(s: &str) -> Self {
|
pub fn new(s: &str) -> Self {
|
||||||
|
Self::new_with_rejected(s, REJECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`new`](Self::new) but with a custom rejected/cancelled glyph
|
||||||
|
/// (vimwiki's `listsym_rejected`).
|
||||||
|
pub fn new_with_rejected(s: &str, rejected: char) -> Self {
|
||||||
let progression: Vec<char> = s.chars().collect();
|
let progression: Vec<char> = s.chars().collect();
|
||||||
if progression.len() < 2 {
|
if progression.len() < 2 {
|
||||||
Self::default()
|
Self {
|
||||||
|
rejected,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Self { progression }
|
Self {
|
||||||
|
progression,
|
||||||
|
rejected,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +71,7 @@ impl ListSyms {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn rejected_glyph(&self) -> char {
|
pub fn rejected_glyph(&self) -> char {
|
||||||
REJECTED
|
self.rejected
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyph_at(&self, index: usize) -> char {
|
pub fn glyph_at(&self, index: usize) -> char {
|
||||||
@@ -76,7 +91,7 @@ impl ListSyms {
|
|||||||
/// Normalised five-bucket [`CheckboxState`] for a glyph, or `None` when the
|
/// Normalised five-bucket [`CheckboxState`] for a glyph, or `None` when the
|
||||||
/// glyph is not part of this palette (and is not the rejected marker).
|
/// glyph is not part of this palette (and is not the rejected marker).
|
||||||
pub fn state_of(&self, c: char) -> Option<CheckboxState> {
|
pub fn state_of(&self, c: char) -> Option<CheckboxState> {
|
||||||
if c == REJECTED {
|
if c == self.rejected {
|
||||||
return Some(CheckboxState::Rejected);
|
return Some(CheckboxState::Rejected);
|
||||||
}
|
}
|
||||||
let index = self.index_of(c)?;
|
let index = self.index_of(c)?;
|
||||||
@@ -96,7 +111,7 @@ impl ListSyms {
|
|||||||
/// `ceil` rule, matching the stock five-symbol behaviour.
|
/// `ceil` rule, matching the stock five-symbol behaviour.
|
||||||
pub fn glyph_for_rate(&self, rate: i32) -> char {
|
pub fn glyph_for_rate(&self, rate: i32) -> char {
|
||||||
if rate < 0 {
|
if rate < 0 {
|
||||||
return REJECTED;
|
return self.rejected;
|
||||||
}
|
}
|
||||||
if rate <= 0 {
|
if rate <= 0 {
|
||||||
return self.empty_glyph();
|
return self.empty_glyph();
|
||||||
@@ -157,6 +172,19 @@ mod tests {
|
|||||||
assert_eq!(ListSyms::new("x"), ListSyms::default());
|
assert_eq!(ListSyms::new("x"), ListSyms::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_rejected_glyph_is_honoured() {
|
||||||
|
// `listsym_rejected = '✗'`: that glyph lexes/snaps as Rejected and the
|
||||||
|
// default `-` no longer does.
|
||||||
|
let s = ListSyms::new_with_rejected(" .oOX", '✗');
|
||||||
|
assert_eq!(s.rejected_glyph(), '✗');
|
||||||
|
assert_eq!(s.state_of('✗'), Some(CheckboxState::Rejected));
|
||||||
|
assert_eq!(s.glyph_for_rate(-1), '✗');
|
||||||
|
assert_eq!(s.state_of('-'), None); // no longer the rejected marker
|
||||||
|
// A short palette still keeps the custom rejected glyph.
|
||||||
|
assert_eq!(ListSyms::new_with_rejected("x", '✗').rejected_glyph(), '✗');
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_palette_buckets_to_nearest_state() {
|
fn custom_palette_buckets_to_nearest_state() {
|
||||||
// 3 glyphs → rates 0 / 50 / 100.
|
// 3 glyphs → rates 0 / 50 / 100.
|
||||||
|
|||||||
@@ -46,10 +46,37 @@ pub struct HtmlRenderer {
|
|||||||
/// names fall through to the default `class="color-<name>"`
|
/// names fall through to the default `class="color-<name>"`
|
||||||
/// rendering. Matches vimwiki's `color_dic`.
|
/// rendering. Matches vimwiki's `color_dic`.
|
||||||
colors: HashMap<String, String>,
|
colors: HashMap<String, String>,
|
||||||
/// vimwiki `list_margin`: left margin (in `em`) applied to the
|
/// vimwiki `color_tag_template`: the HTML emitted for a colour span whose
|
||||||
/// outermost list. `-1` (the default) emits no inline style and
|
/// name resolves in `colors`. `__STYLE__` expands to the inline style
|
||||||
/// defers to the stylesheet; `>= 0` forces `margin-left:<n>em`.
|
/// (`color:<css>`) and `__CONTENT__` to the rendered inner HTML. The
|
||||||
list_margin: i32,
|
/// default matches upstream's `<span style="…">…</span>`.
|
||||||
|
color_template: String,
|
||||||
|
/// vimwiki `html_header_numbering`: the heading level at which automatic
|
||||||
|
/// section numbering begins (`0` = off, the default). When `>= 1`, every
|
||||||
|
/// heading at that level or deeper is prefixed with a dotted section
|
||||||
|
/// number (`1`, `1.1`, `1.2`, …); shallower headings stay unnumbered.
|
||||||
|
header_numbering: u8,
|
||||||
|
/// vimwiki `html_header_numbering_sym`: a symbol appended after the
|
||||||
|
/// section number (e.g. `.` → `1.2. Heading`). Empty by default.
|
||||||
|
header_numbering_sym: String,
|
||||||
|
/// vimwiki `toc_header`: the heading text that identifies the TOC
|
||||||
|
/// section. A heading matching this value (case-insensitive) is wrapped
|
||||||
|
/// in `<div class="toc">…</div>` on export, matching upstream so vimwiki's
|
||||||
|
/// stylesheet applies.
|
||||||
|
toc_header: String,
|
||||||
|
/// vimwiki `valid_html_tags`: inline HTML tag names passed through to
|
||||||
|
/// export verbatim (instead of escaping `<`/`>`). Empty = escape all.
|
||||||
|
/// `span` is always allowed so colour spans render.
|
||||||
|
valid_html_tags: Vec<String>,
|
||||||
|
/// vimwiki `emoji_enable` (default `false` in the renderer; set from
|
||||||
|
/// config): substitute `:alias:` shortcodes with their emoji glyph.
|
||||||
|
emoji_enable: bool,
|
||||||
|
/// vimwiki `text_ignore_newline` (default `true`): a soft break in a
|
||||||
|
/// paragraph renders as a space; `false` → `<br />`.
|
||||||
|
text_ignore_newline: bool,
|
||||||
|
/// vimwiki `list_ignore_newline` (default `true`): a soft break in a list
|
||||||
|
/// item renders as a space; `false` → `<br />`.
|
||||||
|
list_ignore_newline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HtmlRenderer {
|
impl Default for HtmlRenderer {
|
||||||
@@ -65,10 +92,43 @@ impl HtmlRenderer {
|
|||||||
template: None,
|
template: None,
|
||||||
vars: HashMap::new(),
|
vars: HashMap::new(),
|
||||||
colors: HashMap::new(),
|
colors: HashMap::new(),
|
||||||
list_margin: -1,
|
color_template: "<span style=\"__STYLE__\">__CONTENT__</span>".to_string(),
|
||||||
|
header_numbering: 0,
|
||||||
|
header_numbering_sym: String::new(),
|
||||||
|
toc_header: String::new(),
|
||||||
|
valid_html_tags: Vec::new(),
|
||||||
|
emoji_enable: false,
|
||||||
|
text_ignore_newline: true,
|
||||||
|
list_ignore_newline: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set vimwiki `text_ignore_newline` / `list_ignore_newline`. When `false`,
|
||||||
|
/// a soft line break in a paragraph / list item renders as `<br />`
|
||||||
|
/// instead of a space.
|
||||||
|
pub fn with_newline_handling(mut self, text_ignore: bool, list_ignore: bool) -> Self {
|
||||||
|
self.text_ignore_newline = text_ignore;
|
||||||
|
self.list_ignore_newline = list_ignore;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the inline HTML tags allowed through export unescaped (vimwiki
|
||||||
|
/// `valid_html_tags`). `span` is always added so colour spans render.
|
||||||
|
pub fn with_valid_html_tags(mut self, mut tags: Vec<String>) -> Self {
|
||||||
|
if !tags.iter().any(|t| t.eq_ignore_ascii_case("span")) {
|
||||||
|
tags.push("span".to_string());
|
||||||
|
}
|
||||||
|
self.valid_html_tags = tags;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable `:alias:` → emoji substitution during export (vimwiki
|
||||||
|
/// `emoji_enable`).
|
||||||
|
pub fn with_emoji(mut self, enable: bool) -> Self {
|
||||||
|
self.emoji_enable = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Override the link resolver. The callback is invoked for every
|
/// Override the link resolver. The callback is invoked for every
|
||||||
/// wikilink and is expected to return a URL string.
|
/// wikilink and is expected to return a URL string.
|
||||||
pub fn with_link_resolver<F>(mut self, f: F) -> Self
|
pub fn with_link_resolver<F>(mut self, f: F) -> Self
|
||||||
@@ -100,13 +160,6 @@ impl HtmlRenderer {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the vimwiki `list_margin`. `-1` defers to the stylesheet;
|
|
||||||
/// `>= 0` forces a `margin-left:<n>em` on the outermost list.
|
|
||||||
pub fn with_list_margin(mut self, margin: i32) -> Self {
|
|
||||||
self.list_margin = margin;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supply a `color_dic`: vimwiki colour-tag names mapped to their
|
/// Supply a `color_dic`: vimwiki colour-tag names mapped to their
|
||||||
/// CSS values. A name listed here is rendered as `style="color:V"`;
|
/// CSS values. A name listed here is rendered as `style="color:V"`;
|
||||||
/// missing names fall through to `class="color-<name>"`.
|
/// missing names fall through to `class="color-<name>"`.
|
||||||
@@ -114,6 +167,80 @@ impl HtmlRenderer {
|
|||||||
self.colors = colors;
|
self.colors = colors;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override the colour-span template (vimwiki `color_tag_template`).
|
||||||
|
/// `__STYLE__` is replaced with the resolved `color:<css>` style and
|
||||||
|
/// `__CONTENT__` with the rendered inner HTML. An empty string keeps the
|
||||||
|
/// built-in default.
|
||||||
|
pub fn with_color_template(mut self, template: impl Into<String>) -> Self {
|
||||||
|
let template = template.into();
|
||||||
|
if !template.is_empty() {
|
||||||
|
self.color_template = template;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable vimwiki-style HTML section numbering. `level` is the heading
|
||||||
|
/// level at which numbering starts (`0` disables it); `sym` is appended
|
||||||
|
/// after the number. Mirrors `html_header_numbering` /
|
||||||
|
/// `html_header_numbering_sym`.
|
||||||
|
pub fn with_header_numbering(mut self, level: u8, sym: impl Into<String>) -> Self {
|
||||||
|
self.header_numbering = level;
|
||||||
|
self.header_numbering_sym = sym.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the vimwiki TOC heading text. When a heading matches this value
|
||||||
|
/// (case-insensitive) it gets `class="toc"` in HTML export.
|
||||||
|
pub fn with_toc_header(mut self, header: impl Into<String>) -> Self {
|
||||||
|
self.toc_header = header.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Running state for vimwiki-style HTML section numbering
|
||||||
|
/// (`html_header_numbering`). Headings are visited in document order; each
|
||||||
|
/// at or below the start level bumps its depth's counter, resets deeper
|
||||||
|
/// counters, and yields a dotted prefix (`1`, `1.1`, `2`, …).
|
||||||
|
struct HeadingNumberer {
|
||||||
|
/// Heading level numbering starts at (`0` disables it).
|
||||||
|
start: u8,
|
||||||
|
/// Symbol appended after the dotted number.
|
||||||
|
sym: String,
|
||||||
|
/// Per-depth counters, indexed by `level - start` (levels clamp to 1–6,
|
||||||
|
/// so the depth is always `0..=5`).
|
||||||
|
counters: [u32; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeadingNumberer {
|
||||||
|
fn new(start: u8, sym: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
start,
|
||||||
|
sym: sym.to_string(),
|
||||||
|
counters: [0; 6],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance to a heading at `level` (1–6) and return its numeric prefix,
|
||||||
|
/// including the trailing symbol and a separating space (e.g. `"1.2. "`).
|
||||||
|
/// Returns an empty string when numbering is off or the heading is
|
||||||
|
/// shallower than the start level.
|
||||||
|
fn prefix(&mut self, level: u8) -> String {
|
||||||
|
if self.start == 0 || level < self.start {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let depth = (level - self.start) as usize;
|
||||||
|
self.counters[depth] += 1;
|
||||||
|
for c in self.counters.iter_mut().skip(depth + 1) {
|
||||||
|
*c = 0;
|
||||||
|
}
|
||||||
|
let num = self.counters[..=depth]
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(".");
|
||||||
|
format!("{num}{} ", self.sym)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer for HtmlRenderer {
|
impl Renderer for HtmlRenderer {
|
||||||
@@ -146,15 +273,41 @@ impl Renderer for HtmlRenderer {
|
|||||||
|
|
||||||
impl HtmlRenderer {
|
impl HtmlRenderer {
|
||||||
fn render_body(&self, doc: &DocumentNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_body(&self, doc: &DocumentNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
|
// Section numbering runs over top-level headings in document order,
|
||||||
|
// mirroring vimwiki's line-by-line scan. Nested headings (in lists,
|
||||||
|
// quotes) go through `render_block` and are left unnumbered, matching
|
||||||
|
// upstream which only numbers document-level headers.
|
||||||
|
let mut numberer = HeadingNumberer::new(self.header_numbering, &self.header_numbering_sym);
|
||||||
|
// Stack of (level, anchor) for the headings enclosing the current one,
|
||||||
|
// so each heading can carry vimwiki's hierarchical id (the parent
|
||||||
|
// anchors joined by `-`, e.g. `Title-Section`).
|
||||||
|
let mut ancestors: Vec<(u8, String)> = Vec::new();
|
||||||
for block in &doc.children {
|
for block in &doc.children {
|
||||||
self.render_block(block, w)?;
|
if let BlockNode::Heading(n) = block {
|
||||||
|
let level = n.level.clamp(1, 6);
|
||||||
|
let number = numberer.prefix(level);
|
||||||
|
let anchor = crate::ast::inline_text(&n.children);
|
||||||
|
while ancestors.last().is_some_and(|(l, _)| *l >= level) {
|
||||||
|
ancestors.pop();
|
||||||
|
}
|
||||||
|
let mut hier = String::new();
|
||||||
|
for (_, a) in &ancestors {
|
||||||
|
hier.push_str(a);
|
||||||
|
hier.push('-');
|
||||||
|
}
|
||||||
|
hier.push_str(&anchor);
|
||||||
|
ancestors.push((level, anchor));
|
||||||
|
self.render_heading(n, &number, &hier, w)?;
|
||||||
|
} else {
|
||||||
|
self.render_block(block, w)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_block(&self, block: &BlockNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_block(&self, block: &BlockNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
match block {
|
match block {
|
||||||
BlockNode::Heading(n) => self.render_heading(n, w),
|
BlockNode::Heading(n) => self.render_heading(n, "", "", w),
|
||||||
BlockNode::Paragraph(n) => self.render_paragraph(n, w),
|
BlockNode::Paragraph(n) => self.render_paragraph(n, w),
|
||||||
BlockNode::HorizontalRule(n) => self.render_hr(n, w),
|
BlockNode::HorizontalRule(n) => self.render_hr(n, w),
|
||||||
BlockNode::Blockquote(n) => self.render_blockquote(n, w),
|
BlockNode::Blockquote(n) => self.render_blockquote(n, w),
|
||||||
@@ -170,13 +323,14 @@ impl HtmlRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_tag(&self, n: &TagNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_tag(&self, n: &TagNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
// `id="tag-…"` lets `[[Page#tag-name]]` jump to the rendered tag.
|
// `id="…"` (bare tag name, matching vimwiki) lets `[[Page#name]]` jump
|
||||||
|
// to the rendered tag.
|
||||||
w.write_all(b"<div class=\"tags\">")?;
|
w.write_all(b"<div class=\"tags\">")?;
|
||||||
for (i, name) in n.tags.iter().enumerate() {
|
for (i, name) in n.tags.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
w.write_all(b" ")?;
|
w.write_all(b" ")?;
|
||||||
}
|
}
|
||||||
w.write_all(b"<span class=\"tag\" id=\"tag-")?;
|
w.write_all(b"<span class=\"tag\" id=\"")?;
|
||||||
write_escaped(name, w)?;
|
write_escaped(name, w)?;
|
||||||
w.write_all(b"\">")?;
|
w.write_all(b"\">")?;
|
||||||
write_escaped(name, w)?;
|
write_escaped(name, w)?;
|
||||||
@@ -185,21 +339,83 @@ impl HtmlRenderer {
|
|||||||
w.write_all(b"</div>\n")
|
w.write_all(b"</div>\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_heading(&self, n: &HeadingNode, w: &mut dyn Write) -> io::Result<()> {
|
/// Render a heading. `number` is the optional section-number prefix
|
||||||
|
/// (already including its trailing symbol and space, e.g. `"1.2. "`);
|
||||||
|
/// pass `""` for no numbering. `hier_id` is the hierarchical anchor id
|
||||||
|
/// (parent anchors joined by `-`); pass `""` to default it to the flat id.
|
||||||
|
fn render_heading(
|
||||||
|
&self,
|
||||||
|
n: &HeadingNode,
|
||||||
|
number: &str,
|
||||||
|
hier_id: &str,
|
||||||
|
w: &mut dyn Write,
|
||||||
|
) -> io::Result<()> {
|
||||||
let level = n.level.clamp(1, 6);
|
let level = n.level.clamp(1, 6);
|
||||||
let class = if n.centered {
|
let anchor = crate::ast::inline_text(&n.children);
|
||||||
" class=\"centered\""
|
// Inside a heading, render TODO/DONE/… as plain text rather than a
|
||||||
} else {
|
// keyword badge — a section title that *is* a keyword (`== TODO ==`)
|
||||||
""
|
// should look like a header, not an inline tag. (The keyword text is
|
||||||
};
|
// still part of `anchor` above, so the id stays correct.)
|
||||||
write!(w, "<h{level}{class}>")?;
|
let body = flatten_keywords(&n.children);
|
||||||
self.render_inlines(&n.children, w)?;
|
|
||||||
writeln!(w, "</h{level}>")
|
// The TOC section heading (vimwiki `toc_header`) is wrapped in a
|
||||||
|
// `<div class="toc">`, matching upstream so vimwiki's `.toc { … }`
|
||||||
|
// stylesheet rules apply. Kept in its simple form.
|
||||||
|
let is_toc = !self.toc_header.is_empty() && anchor.eq_ignore_ascii_case(&self.toc_header);
|
||||||
|
if is_toc {
|
||||||
|
w.write_all(b"<div class=\"toc\">")?;
|
||||||
|
write!(w, "<h{level} id=\"")?;
|
||||||
|
write_escaped(&anchor, w)?;
|
||||||
|
write!(w, "\">")?;
|
||||||
|
if !number.is_empty() {
|
||||||
|
write_escaped(number, w)?;
|
||||||
|
}
|
||||||
|
self.render_inlines(&body, w)?;
|
||||||
|
write!(w, "</h{level}></div>")?;
|
||||||
|
return writeln!(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centered headings keep their `class="centered"` form.
|
||||||
|
if n.centered {
|
||||||
|
write!(w, "<h{level} class=\"centered\" id=\"")?;
|
||||||
|
write_escaped(&anchor, w)?;
|
||||||
|
write!(w, "\">")?;
|
||||||
|
if !number.is_empty() {
|
||||||
|
write_escaped(number, w)?;
|
||||||
|
}
|
||||||
|
self.render_inlines(&body, w)?;
|
||||||
|
write!(w, "</h{level}>")?;
|
||||||
|
return writeln!(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular heading: vimwiki's structure — a wrapping `<div>` carrying the
|
||||||
|
// hierarchical id, `class="header"` on the heading, and an in-heading
|
||||||
|
// self-anchor linking to that id. The heading keeps the flat id so
|
||||||
|
// existing intra-page TOC / `#anchor` links still resolve.
|
||||||
|
let hier = if hier_id.is_empty() { &anchor } else { hier_id };
|
||||||
|
w.write_all(b"<div id=\"")?;
|
||||||
|
write_escaped(hier, w)?;
|
||||||
|
write!(w, "\"><h{level} id=\"")?;
|
||||||
|
write_escaped(&anchor, w)?;
|
||||||
|
w.write_all(b"\" class=\"header\"><a href=\"#")?;
|
||||||
|
write_escaped(hier, w)?;
|
||||||
|
write!(w, "\">")?;
|
||||||
|
if !number.is_empty() {
|
||||||
|
write_escaped(number, w)?;
|
||||||
|
}
|
||||||
|
self.render_inlines(&body, w)?;
|
||||||
|
write!(w, "</a></h{level}></div>")?;
|
||||||
|
writeln!(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_paragraph(&self, n: &ParagraphNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_paragraph(&self, n: &ParagraphNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
w.write_all(b"<p>")?;
|
w.write_all(b"<p>")?;
|
||||||
self.render_inlines(&n.children, w)?;
|
let sb = if self.text_ignore_newline {
|
||||||
|
" "
|
||||||
|
} else {
|
||||||
|
"<br />"
|
||||||
|
};
|
||||||
|
self.render_inlines_break(&n.children, sb, w)?;
|
||||||
w.write_all(b"</p>\n")
|
w.write_all(b"</p>\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,16 +432,16 @@ impl HtmlRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_preformatted(&self, n: &PreformattedNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_preformatted(&self, n: &PreformattedNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
match &n.language {
|
// vimwiki drops the verbatim fence text into the `<pre>` tag as raw
|
||||||
Some(lang) => {
|
// attributes (`{{{python` → `<pre python>`), with no inner `<code>`.
|
||||||
w.write_all(b"<pre><code class=\"language-")?;
|
// We mirror that so highlighters wired for vimwiki output still apply.
|
||||||
write_escaped(lang, w)?;
|
if n.attrs.is_empty() {
|
||||||
w.write_all(b"\">")?;
|
w.write_all(b"<pre>")?;
|
||||||
}
|
} else {
|
||||||
None => w.write_all(b"<pre><code>")?,
|
write!(w, "<pre {}>", n.attrs)?;
|
||||||
}
|
}
|
||||||
write_escaped(&n.content, w)?;
|
write_escaped(&n.content, w)?;
|
||||||
w.write_all(b"</code></pre>\n")
|
w.write_all(b"</pre>\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_math_block(&self, n: &MathBlockNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_math_block(&self, n: &MathBlockNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
@@ -248,16 +464,8 @@ impl HtmlRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_list(&self, n: &ListNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_list(&self, n: &ListNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
self.render_list_nested(n, w, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_nested(&self, n: &ListNode, w: &mut dyn Write, top: bool) -> io::Result<()> {
|
|
||||||
let tag = if n.ordered { "ol" } else { "ul" };
|
let tag = if n.ordered { "ol" } else { "ul" };
|
||||||
if top && self.list_margin >= 0 {
|
writeln!(w, "<{tag}>")?;
|
||||||
writeln!(w, "<{tag} style=\"margin-left:{}em\">", self.list_margin)?;
|
|
||||||
} else {
|
|
||||||
writeln!(w, "<{tag}>")?;
|
|
||||||
}
|
|
||||||
for item in &n.items {
|
for item in &n.items {
|
||||||
self.render_list_item(item, w)?;
|
self.render_list_item(item, w)?;
|
||||||
}
|
}
|
||||||
@@ -284,10 +492,15 @@ impl HtmlRenderer {
|
|||||||
}
|
}
|
||||||
None => w.write_all(b"<li>")?,
|
None => w.write_all(b"<li>")?,
|
||||||
}
|
}
|
||||||
self.render_inlines(&n.children, w)?;
|
let sb = if self.list_ignore_newline {
|
||||||
|
" "
|
||||||
|
} else {
|
||||||
|
"<br />"
|
||||||
|
};
|
||||||
|
self.render_inlines_break(&n.children, sb, w)?;
|
||||||
if let Some(sublist) = &n.sublist {
|
if let Some(sublist) = &n.sublist {
|
||||||
w.write_all(b"\n")?;
|
w.write_all(b"\n")?;
|
||||||
self.render_list_nested(sublist, w, false)?;
|
self.render_list(sublist, w)?;
|
||||||
}
|
}
|
||||||
w.write_all(b"</li>\n")
|
w.write_all(b"</li>\n")
|
||||||
}
|
}
|
||||||
@@ -439,6 +652,24 @@ impl HtmlRenderer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`render_inlines`] but renders top-level soft breaks as `soft_break`
|
||||||
|
/// (a space or `<br />`). Used by paragraphs / list items, which are the
|
||||||
|
/// only blocks that contain `SoftBreak` nodes.
|
||||||
|
fn render_inlines_break(
|
||||||
|
&self,
|
||||||
|
nodes: &[InlineNode],
|
||||||
|
soft_break: &str,
|
||||||
|
w: &mut dyn Write,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
for n in nodes {
|
||||||
|
match n {
|
||||||
|
InlineNode::SoftBreak(_) => w.write_all(soft_break.as_bytes())?,
|
||||||
|
_ => self.render_inline(n, w)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn render_inline(&self, n: &InlineNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_inline(&self, n: &InlineNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
match n {
|
match n {
|
||||||
InlineNode::Text(t) => self.render_text(t, w),
|
InlineNode::Text(t) => self.render_text(t, w),
|
||||||
@@ -456,11 +687,23 @@ impl HtmlRenderer {
|
|||||||
InlineNode::ExternalLink(t) => self.render_external_link(t, w),
|
InlineNode::ExternalLink(t) => self.render_external_link(t, w),
|
||||||
InlineNode::Transclusion(t) => self.render_transclusion(t, w),
|
InlineNode::Transclusion(t) => self.render_transclusion(t, w),
|
||||||
InlineNode::RawUrl(t) => self.render_raw_url(t, w),
|
InlineNode::RawUrl(t) => self.render_raw_url(t, w),
|
||||||
|
// Default rendering (e.g. inside a heading): a space. Paragraphs
|
||||||
|
// and list items use render_inlines_break for `<br />` support.
|
||||||
|
InlineNode::SoftBreak(_) => w.write_all(b" "),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_text(&self, n: &TextNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_text(&self, n: &TextNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
write_escaped(&n.content, w)
|
let content = if self.emoji_enable {
|
||||||
|
substitute_emoji(&n.content)
|
||||||
|
} else {
|
||||||
|
std::borrow::Cow::Borrowed(n.content.as_str())
|
||||||
|
};
|
||||||
|
if self.valid_html_tags.is_empty() {
|
||||||
|
write_escaped(&content, w)
|
||||||
|
} else {
|
||||||
|
write_escaped_allowing(&content, &self.valid_html_tags, w)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_bold(&self, n: &BoldNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_bold(&self, n: &BoldNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
@@ -515,30 +758,45 @@ impl HtmlRenderer {
|
|||||||
// One class per keyword so stylesheets can colour them independently
|
// One class per keyword so stylesheets can colour them independently
|
||||||
// (e.g. red TODO/FIXME/XXX vs green DONE/FIXED/STARTED). TODO keeps the
|
// (e.g. red TODO/FIXME/XXX vs green DONE/FIXED/STARTED). TODO keeps the
|
||||||
// vimwiki `todo` class so stock vimwiki CSS still styles it.
|
// vimwiki `todo` class so stock vimwiki CSS still styles it.
|
||||||
let (label, class) = match n.keyword {
|
let class = match n.keyword {
|
||||||
Keyword::Todo => ("TODO", "todo"),
|
Keyword::Todo => "todo",
|
||||||
Keyword::Done => ("DONE", "done"),
|
Keyword::Done => "done",
|
||||||
Keyword::Started => ("STARTED", "started"),
|
Keyword::Started => "started",
|
||||||
Keyword::Fixme => ("FIXME", "fixme"),
|
Keyword::Fixme => "fixme",
|
||||||
Keyword::Fixed => ("FIXED", "fixed"),
|
Keyword::Fixed => "fixed",
|
||||||
Keyword::Xxx => ("XXX", "xxx"),
|
Keyword::Xxx => "xxx",
|
||||||
Keyword::Stopped => ("STOPPED", "stopped"),
|
Keyword::Stopped => "stopped",
|
||||||
};
|
};
|
||||||
write!(w, "<span class=\"{class}\">{label}</span>")
|
write!(w, "<span class=\"{class}\">{}</span>", n.keyword.label())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_color(&self, n: &ColorNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_color(&self, n: &ColorNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
|
// Render the inner content up front so it can be slotted into the
|
||||||
|
// colour template.
|
||||||
|
let mut content = Vec::new();
|
||||||
|
self.render_inlines(&n.children, &mut content)?;
|
||||||
|
let content = String::from_utf8_lossy(&content);
|
||||||
|
|
||||||
if let Some(css) = self.colors.get(&n.color) {
|
if let Some(css) = self.colors.get(&n.color) {
|
||||||
w.write_all(b"<span style=\"color:")?;
|
// Known colour name: expand `color_tag_template` with the resolved
|
||||||
write_escaped(css, w)?;
|
// inline style and rendered content.
|
||||||
w.write_all(b"\">")?;
|
let mut style = b"color:".to_vec();
|
||||||
|
write_escaped(css, &mut style)?;
|
||||||
|
let style = String::from_utf8_lossy(&style);
|
||||||
|
let html = self
|
||||||
|
.color_template
|
||||||
|
.replace("__STYLE__", &style)
|
||||||
|
.replace("__CONTENT__", &content);
|
||||||
|
w.write_all(html.as_bytes())
|
||||||
} else {
|
} else {
|
||||||
|
// Unknown name: fall back to a class hook the stylesheet can target
|
||||||
|
// (the template only models the inline-style case).
|
||||||
w.write_all(b"<span class=\"color-")?;
|
w.write_all(b"<span class=\"color-")?;
|
||||||
write_escaped(&n.color, w)?;
|
write_escaped(&n.color, w)?;
|
||||||
w.write_all(b"\">")?;
|
w.write_all(b"\">")?;
|
||||||
|
w.write_all(content.as_bytes())?;
|
||||||
|
w.write_all(b"</span>")
|
||||||
}
|
}
|
||||||
self.render_inlines(&n.children, w)?;
|
|
||||||
w.write_all(b"</span>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_wikilink(&self, n: &WikiLinkNode, w: &mut dyn Write) -> io::Result<()> {
|
fn render_wikilink(&self, n: &WikiLinkNode, w: &mut dyn Write) -> io::Result<()> {
|
||||||
@@ -686,6 +944,51 @@ fn table_spans(table: &TableNode) -> Vec<Vec<CellLayout>> {
|
|||||||
/// wiki pages become `path.html`; interwiki links land in sibling
|
/// wiki pages become `path.html`; interwiki links land in sibling
|
||||||
/// directories; diary entries land under `diary/`; file/local schemes
|
/// directories; diary entries land under `diary/`; file/local schemes
|
||||||
/// use their literal path; anchor-only links collapse to a fragment.
|
/// use their literal path; anchor-only links collapse to a fragment.
|
||||||
|
/// Return a copy of `nodes` with every `Keyword` replaced by its literal text.
|
||||||
|
/// Used for heading content so a keyword in a title renders as plain text (with
|
||||||
|
/// the heading's own styling) rather than as an inline keyword badge.
|
||||||
|
fn flatten_keywords(nodes: &[InlineNode]) -> Vec<InlineNode> {
|
||||||
|
nodes
|
||||||
|
.iter()
|
||||||
|
.map(|n| match n {
|
||||||
|
InlineNode::Keyword(k) => InlineNode::Text(TextNode {
|
||||||
|
span: k.span,
|
||||||
|
content: k.keyword.label().to_string(),
|
||||||
|
}),
|
||||||
|
InlineNode::Bold(b) => InlineNode::Bold(BoldNode {
|
||||||
|
span: b.span,
|
||||||
|
children: flatten_keywords(&b.children),
|
||||||
|
}),
|
||||||
|
InlineNode::Italic(i) => InlineNode::Italic(ItalicNode {
|
||||||
|
span: i.span,
|
||||||
|
children: flatten_keywords(&i.children),
|
||||||
|
}),
|
||||||
|
InlineNode::BoldItalic(b) => InlineNode::BoldItalic(BoldItalicNode {
|
||||||
|
span: b.span,
|
||||||
|
children: flatten_keywords(&b.children),
|
||||||
|
}),
|
||||||
|
InlineNode::Strikethrough(s) => InlineNode::Strikethrough(StrikethroughNode {
|
||||||
|
span: s.span,
|
||||||
|
children: flatten_keywords(&s.children),
|
||||||
|
}),
|
||||||
|
InlineNode::Superscript(s) => InlineNode::Superscript(SuperscriptNode {
|
||||||
|
span: s.span,
|
||||||
|
children: flatten_keywords(&s.children),
|
||||||
|
}),
|
||||||
|
InlineNode::Subscript(s) => InlineNode::Subscript(SubscriptNode {
|
||||||
|
span: s.span,
|
||||||
|
children: flatten_keywords(&s.children),
|
||||||
|
}),
|
||||||
|
InlineNode::Color(c) => InlineNode::Color(ColorNode {
|
||||||
|
span: c.span,
|
||||||
|
color: c.color.clone(),
|
||||||
|
children: flatten_keywords(&c.children),
|
||||||
|
}),
|
||||||
|
other => other.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_link_resolver(target: &LinkTarget) -> String {
|
pub fn default_link_resolver(target: &LinkTarget) -> String {
|
||||||
if matches!(target.kind, LinkKind::AnchorOnly) {
|
if matches!(target.kind, LinkKind::AnchorOnly) {
|
||||||
return match &target.anchor {
|
return match &target.anchor {
|
||||||
@@ -750,6 +1053,145 @@ pub fn default_link_resolver(target: &LinkTarget) -> String {
|
|||||||
|
|
||||||
// ===== HTML escaping =====
|
// ===== HTML escaping =====
|
||||||
|
|
||||||
|
/// Escape `s` for HTML, but pass through inline tags whose name is in
|
||||||
|
/// `allowed` (vimwiki `valid_html_tags`) verbatim — mirroring upstream's
|
||||||
|
/// render-time `s:safe_html_line` regex rather than parsing raw HTML.
|
||||||
|
fn write_escaped_allowing(s: &str, allowed: &[String], w: &mut dyn Write) -> io::Result<()> {
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut last = 0;
|
||||||
|
while i < bytes.len() {
|
||||||
|
if bytes[i] == b'<' {
|
||||||
|
if let Some(end) = match_allowed_tag(bytes, i, allowed) {
|
||||||
|
write_escaped(&s[last..i], w)?;
|
||||||
|
w.write_all(&bytes[i..end])?;
|
||||||
|
i = end;
|
||||||
|
last = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
write_escaped(&s[last..], w)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `bytes[lt]` (`<`) begins a `<tag…>` / `</tag>` whose name is in
|
||||||
|
/// `allowed`, return the index just past the closing `>`. Otherwise `None`.
|
||||||
|
fn match_allowed_tag(bytes: &[u8], lt: usize, allowed: &[String]) -> Option<usize> {
|
||||||
|
let mut p = lt + 1;
|
||||||
|
if bytes.get(p) == Some(&b'/') {
|
||||||
|
p += 1;
|
||||||
|
}
|
||||||
|
let name_begin = p;
|
||||||
|
while p < bytes.len() && (bytes[p] as char).is_ascii_alphabetic() {
|
||||||
|
p += 1;
|
||||||
|
}
|
||||||
|
if p == name_begin {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = std::str::from_utf8(&bytes[name_begin..p]).ok()?;
|
||||||
|
if !allowed.iter().any(|t| t.eq_ignore_ascii_case(name)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Scan to the closing `>` (bail on a nested `<` — not a well-formed tag).
|
||||||
|
while p < bytes.len() && bytes[p] != b'>' {
|
||||||
|
if bytes[p] == b'<' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
p += 1;
|
||||||
|
}
|
||||||
|
if p >= bytes.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(p + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace `:alias:` emoji shortcodes with their glyph (vimwiki
|
||||||
|
/// `emoji_enable`). Only a curated common subset is supported; unknown
|
||||||
|
/// shortcodes are left untouched.
|
||||||
|
fn substitute_emoji(s: &str) -> std::borrow::Cow<'_, str> {
|
||||||
|
if !s.contains(':') {
|
||||||
|
return std::borrow::Cow::Borrowed(s);
|
||||||
|
}
|
||||||
|
let mut out = String::with_capacity(s.len());
|
||||||
|
let mut rest = s;
|
||||||
|
let mut changed = false;
|
||||||
|
while let Some(open) = rest.find(':') {
|
||||||
|
out.push_str(&rest[..open]);
|
||||||
|
let after = &rest[open + 1..];
|
||||||
|
if let Some(close) = after.find(':') {
|
||||||
|
let alias = &after[..close];
|
||||||
|
if !alias.is_empty()
|
||||||
|
&& alias
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-')
|
||||||
|
{
|
||||||
|
if let Some(glyph) = emoji_for(alias) {
|
||||||
|
out.push_str(glyph);
|
||||||
|
rest = &after[close + 1..];
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not a shortcode — keep the `:` and continue past it.
|
||||||
|
out.push(':');
|
||||||
|
rest = after;
|
||||||
|
}
|
||||||
|
out.push_str(rest);
|
||||||
|
if changed {
|
||||||
|
std::borrow::Cow::Owned(out)
|
||||||
|
} else {
|
||||||
|
std::borrow::Cow::Borrowed(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A curated subset of GitHub-style emoji shortcodes.
|
||||||
|
fn emoji_for(alias: &str) -> Option<&'static str> {
|
||||||
|
Some(match alias {
|
||||||
|
"smile" => "😄",
|
||||||
|
"smiley" => "😃",
|
||||||
|
"grin" => "😁",
|
||||||
|
"laughing" | "satisfied" => "😆",
|
||||||
|
"wink" => "😉",
|
||||||
|
"blush" => "😊",
|
||||||
|
"heart" => "❤️",
|
||||||
|
"broken_heart" => "💔",
|
||||||
|
"thumbsup" | "+1" => "👍",
|
||||||
|
"thumbsdown" | "-1" => "👎",
|
||||||
|
"ok_hand" => "👌",
|
||||||
|
"wave" => "👋",
|
||||||
|
"clap" => "👏",
|
||||||
|
"fire" => "🔥",
|
||||||
|
"star" => "⭐",
|
||||||
|
"sparkles" => "✨",
|
||||||
|
"zap" => "⚡",
|
||||||
|
"boom" => "💥",
|
||||||
|
"tada" => "🎉",
|
||||||
|
"rocket" => "🚀",
|
||||||
|
"bulb" => "💡",
|
||||||
|
"bug" => "🐛",
|
||||||
|
"warning" => "⚠️",
|
||||||
|
"white_check_mark" | "check" => "✅",
|
||||||
|
"x" => "❌",
|
||||||
|
"question" => "❓",
|
||||||
|
"exclamation" => "❗",
|
||||||
|
"eyes" => "👀",
|
||||||
|
"100" => "💯",
|
||||||
|
"pray" => "🙏",
|
||||||
|
"muscle" => "💪",
|
||||||
|
"coffee" => "☕",
|
||||||
|
"book" => "📖",
|
||||||
|
"memo" | "pencil" => "📝",
|
||||||
|
"lock" => "🔒",
|
||||||
|
"key" => "🔑",
|
||||||
|
"bell" => "🔔",
|
||||||
|
"hourglass" => "⌛",
|
||||||
|
"calendar" => "📅",
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn write_escaped(s: &str, w: &mut dyn Write) -> io::Result<()> {
|
fn write_escaped(s: &str, w: &mut dyn Write) -> io::Result<()> {
|
||||||
let bytes = s.as_bytes();
|
let bytes = s.as_bytes();
|
||||||
let mut last = 0;
|
let mut last = 0;
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ impl SyntaxRegistry {
|
|||||||
self.plugins.push(Arc::new(plugin));
|
self.plugins.push(Arc::new(plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a pre-arc'd plugin. Useful when the same plugin instance
|
|
||||||
/// also needs to be held elsewhere.
|
|
||||||
pub fn register_arc(&mut self, plugin: Arc<dyn SyntaxPlugin>) {
|
|
||||||
self.plugins.push(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Look up a plugin by its `id`.
|
/// Look up a plugin by its `id`.
|
||||||
pub fn get(&self, id: &str) -> Option<&dyn SyntaxPlugin> {
|
pub fn get(&self, id: &str) -> Option<&dyn SyntaxPlugin> {
|
||||||
self.plugins
|
self.plugins
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ pub enum VimwikiTokenKind {
|
|||||||
PreformattedOpen {
|
PreformattedOpen {
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
attrs: HashMap<String, String>,
|
attrs: HashMap<String, String>,
|
||||||
|
/// Verbatim text after `{{{` (trailing whitespace trimmed), preserved
|
||||||
|
/// so the HTML renderer can reproduce vimwiki's raw `<pre …>` attrs.
|
||||||
|
raw: String,
|
||||||
},
|
},
|
||||||
PreformattedClose,
|
PreformattedClose,
|
||||||
PreformattedLine(String),
|
PreformattedLine(String),
|
||||||
@@ -421,7 +424,14 @@ impl<'src> LexState<'src> {
|
|||||||
// pairs separated by spaces. Keep parsing forgiving.
|
// pairs separated by spaces. Keep parsing forgiving.
|
||||||
let after = trimmed[3..].trim();
|
let after = trimmed[3..].trim();
|
||||||
let (language, attrs) = parse_fence_attrs(after);
|
let (language, attrs) = parse_fence_attrs(after);
|
||||||
let kind = VimwikiTokenKind::PreformattedOpen { language, attrs };
|
// vimwiki keeps everything after `{{{` (minus trailing whitespace) and
|
||||||
|
// drops it verbatim into the `<pre>` tag, so capture it unparsed.
|
||||||
|
let raw = trimmed[3..].trim_end().to_string();
|
||||||
|
let kind = VimwikiTokenKind::PreformattedOpen {
|
||||||
|
language,
|
||||||
|
attrs,
|
||||||
|
raw,
|
||||||
|
};
|
||||||
let span = Span::new(self.pos_in_line(indent), self.line_end_pos(line));
|
let span = Span::new(self.pos_in_line(indent), self.line_end_pos(line));
|
||||||
self.push(kind, span);
|
self.push(kind, span);
|
||||||
self.mode = BlockMode::Preformatted;
|
self.mode = BlockMode::Preformatted;
|
||||||
@@ -516,10 +526,9 @@ impl<'src> LexState<'src> {
|
|||||||
if level == 0 {
|
if level == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Must be followed by at least one space.
|
// vimwiki accepts both spaced (`== H ==`) and spaceless (`==H==`)
|
||||||
if leading_ws + level >= bytes.len() || bytes[leading_ws + level] != b' ' {
|
// headings, so we don't require a space after the opening `=`s. The
|
||||||
return false;
|
// title is still trimmed of one optional space per side below.
|
||||||
}
|
|
||||||
|
|
||||||
// Trailing `=`s of the same level.
|
// Trailing `=`s of the same level.
|
||||||
let trimmed_end = line.trim_end_matches([' ', '\t']);
|
let trimmed_end = line.trim_end_matches([' ', '\t']);
|
||||||
@@ -865,8 +874,10 @@ impl<'src> LexState<'src> {
|
|||||||
flush(self, &mut buf, &mut buf_start, abs_col);
|
flush(self, &mut buf, &mut buf_start, abs_col);
|
||||||
self.emit(VimwikiTokenKind::WikiLinkOpen, abs_col, abs_col + 2);
|
self.emit(VimwikiTokenKind::WikiLinkOpen, abs_col, abs_col + 2);
|
||||||
i += 2;
|
i += 2;
|
||||||
// Lex until `]]` (absolute index into `slice`).
|
// Lex until the closing `]]`. A single `]` inside the body —
|
||||||
let close_abs_in_slice = after_open.find("]]").map(|c| i + c);
|
// e.g. a `[TICKET-1]` in the description — is literal text, so
|
||||||
|
// we balance inner `[ ]` rather than stopping at the first `]]`.
|
||||||
|
let close_abs_in_slice = find_wikilink_close(after_open).map(|c| i + c);
|
||||||
let inner_end = close_abs_in_slice.unwrap_or(slice.len());
|
let inner_end = close_abs_in_slice.unwrap_or(slice.len());
|
||||||
self.lex_link_body(slice, i, line_col_start, inner_end, true);
|
self.lex_link_body(slice, i, line_col_start, inner_end, true);
|
||||||
i = inner_end;
|
i = inner_end;
|
||||||
@@ -1052,6 +1063,27 @@ impl<'src> LexState<'src> {
|
|||||||
/// Parse the bit after `{{{` on a fence line. Format is forgiving:
|
/// Parse the bit after `{{{` on a fence line. Format is forgiving:
|
||||||
/// `lang` (single word) followed by optional `key=value` pairs separated
|
/// `lang` (single word) followed by optional `key=value` pairs separated
|
||||||
/// by spaces.
|
/// by spaces.
|
||||||
|
/// Byte offset of the closing `]]` for a wikilink body that begins at the start
|
||||||
|
/// of `s` (just after the opening `[[`). Inner `[ ]` pairs are balanced so a
|
||||||
|
/// bracketed description like `[[page|Task [B-1]]]` keeps the `[B-1]` and closes
|
||||||
|
/// at the final `]]`. A stray single `]` (no matching `[`) is treated as literal
|
||||||
|
/// body text. Returns `None` if no closing `]]` is found.
|
||||||
|
fn find_wikilink_close(s: &str) -> Option<usize> {
|
||||||
|
let b = s.as_bytes();
|
||||||
|
let mut depth: u32 = 0;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < b.len() {
|
||||||
|
match b[i] {
|
||||||
|
b'[' => depth += 1,
|
||||||
|
b']' if depth > 0 => depth -= 1, // closes an inner '['
|
||||||
|
b']' if i + 1 < b.len() && b[i + 1] == b']' => return Some(i),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_fence_attrs(s: &str) -> (Option<String>, HashMap<String, String>) {
|
fn parse_fence_attrs(s: &str) -> (Option<String>, HashMap<String, String>) {
|
||||||
let mut attrs = HashMap::new();
|
let mut attrs = HashMap::new();
|
||||||
let mut language: Option<String> = None;
|
let mut language: Option<String> = None;
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ use crate::ast::{
|
|||||||
BlockNode, BlockquoteNode, BoldNode, CodeNode, CommentNode, DefinitionItemNode,
|
BlockNode, BlockquoteNode, BoldNode, CodeNode, CommentNode, DefinitionItemNode,
|
||||||
DefinitionListNode, DocumentNode, ErrorNode, ExternalLinkNode, HeadingNode, HorizontalRuleNode,
|
DefinitionListNode, DocumentNode, ErrorNode, ExternalLinkNode, HeadingNode, HorizontalRuleNode,
|
||||||
InlineNode, ItalicNode, KeywordNode, LinkKind, LinkTarget, ListItemNode, ListNode, ListSymbol,
|
InlineNode, ItalicNode, KeywordNode, LinkKind, LinkTarget, ListItemNode, ListNode, ListSymbol,
|
||||||
MathBlockNode, MathInlineNode, PageMetadata, ParagraphNode, PreformattedNode, RawUrlNode, Span,
|
MathBlockNode, MathInlineNode, PageMetadata, ParagraphNode, PreformattedNode, RawUrlNode,
|
||||||
StrikethroughNode, SubscriptNode, SuperscriptNode, TableCellNode, TableNode, TableRowNode,
|
SoftBreakNode, Span, StrikethroughNode, SubscriptNode, SuperscriptNode, TableCellNode,
|
||||||
TagNode, TagScope, TextNode, TransclusionNode, WikiLinkNode,
|
TableNode, TableRowNode, TagNode, TagScope, TextNode, TransclusionNode, WikiLinkNode,
|
||||||
};
|
};
|
||||||
use crate::syntax::{Parser, TokenStream};
|
use crate::syntax::{Parser, TokenStream};
|
||||||
|
|
||||||
@@ -374,8 +374,8 @@ impl<'a> ParseState<'a> {
|
|||||||
let open = self.advance().unwrap();
|
let open = self.advance().unwrap();
|
||||||
let span_start = open.span.start;
|
let span_start = open.span.start;
|
||||||
let mut span_end = open.span.end;
|
let mut span_end = open.span.end;
|
||||||
let language = match &open.kind {
|
let (language, attrs) = match &open.kind {
|
||||||
K::PreformattedOpen { language, .. } => language.clone(),
|
K::PreformattedOpen { language, raw, .. } => (language.clone(), raw.clone()),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
@@ -390,6 +390,7 @@ impl<'a> ParseState<'a> {
|
|||||||
span: Span::new(span_start, span_end),
|
span: Span::new(span_start, span_end),
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
|
attrs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
K::PreformattedLine(s) => {
|
K::PreformattedLine(s) => {
|
||||||
@@ -413,6 +414,7 @@ impl<'a> ParseState<'a> {
|
|||||||
span: Span::new(span_start, span_end),
|
span: Span::new(span_start, span_end),
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
|
attrs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,14 +598,11 @@ impl<'a> ParseState<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Insert a synthetic " " between the previous content and
|
// Insert a soft break between the previous content and the
|
||||||
// the continuation so the rendered text flows naturally.
|
// continuation so the rendered text flows naturally (and honours
|
||||||
|
// `*_ignore_newline`).
|
||||||
if !children.is_empty() {
|
if !children.is_empty() {
|
||||||
let span = first.span;
|
children.push(InlineNode::SoftBreak(SoftBreakNode { span: first.span }));
|
||||||
children.push(InlineNode::Text(TextNode {
|
|
||||||
span,
|
|
||||||
content: " ".into(),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
let cont = parse_inline_seq(&buf);
|
let cont = parse_inline_seq(&buf);
|
||||||
children.extend(cont);
|
children.extend(cont);
|
||||||
@@ -1008,11 +1007,10 @@ fn parse_inline_seq(toks: &[VimwikiToken]) -> Vec<InlineNode> {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
K::Newline => {
|
K::Newline => {
|
||||||
// Soft break inside a multi-line block: render as space.
|
// Soft break inside a multi-line block. A dedicated node lets
|
||||||
out.push(InlineNode::Text(TextNode {
|
// the renderer honour `*_ignore_newline` (space vs `<br />`);
|
||||||
span: token.span,
|
// other consumers treat it as whitespace.
|
||||||
content: " ".into(),
|
out.push(InlineNode::SoftBreak(SoftBreakNode { span: token.span }));
|
||||||
}));
|
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
K::BoldDelim => {
|
K::BoldDelim => {
|
||||||
@@ -1430,29 +1428,9 @@ fn strip_directory(s: &str) -> (String, bool) {
|
|||||||
// ===== Span helpers =====
|
// ===== Span helpers =====
|
||||||
|
|
||||||
fn span_start_of_inline(node: &InlineNode) -> Option<crate::ast::Position> {
|
fn span_start_of_inline(node: &InlineNode) -> Option<crate::ast::Position> {
|
||||||
Some(span_of_inline(node).start)
|
Some(node.span().start)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn span_end_of_inline(node: &InlineNode) -> Option<crate::ast::Position> {
|
fn span_end_of_inline(node: &InlineNode) -> Option<crate::ast::Position> {
|
||||||
Some(span_of_inline(node).end)
|
Some(node.span().end)
|
||||||
}
|
|
||||||
|
|
||||||
fn span_of_inline(node: &InlineNode) -> Span {
|
|
||||||
match node {
|
|
||||||
InlineNode::Text(n) => n.span,
|
|
||||||
InlineNode::Bold(n) => n.span,
|
|
||||||
InlineNode::Italic(n) => n.span,
|
|
||||||
InlineNode::BoldItalic(n) => n.span,
|
|
||||||
InlineNode::Strikethrough(n) => n.span,
|
|
||||||
InlineNode::Code(n) => n.span,
|
|
||||||
InlineNode::Superscript(n) => n.span,
|
|
||||||
InlineNode::Subscript(n) => n.span,
|
|
||||||
InlineNode::MathInline(n) => n.span,
|
|
||||||
InlineNode::Keyword(n) => n.span,
|
|
||||||
InlineNode::Color(n) => n.span,
|
|
||||||
InlineNode::WikiLink(n) => n.span,
|
|
||||||
InlineNode::ExternalLink(n) => n.span,
|
|
||||||
InlineNode::Transclusion(n) => n.span,
|
|
||||||
InlineNode::RawUrl(n) => n.span,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
//! Cluster 4 — diary frequency / period primitives.
|
//! Cluster 4 — diary frequency / period primitives.
|
||||||
//! Tests the date math for ISO weeks, monthly, yearly periods.
|
//! Tests the date math for ISO weeks, monthly, yearly periods.
|
||||||
|
|
||||||
use nuwiki_core::date::{DiaryDate, DiaryFrequency, DiaryPeriod};
|
use nuwiki_core::date::{
|
||||||
|
DiaryCalendar, DiaryDate, DiaryFrequency, DiaryPeriod, WeekStart, WeeklyStyle,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn frequency_parses_canonical_strings() {
|
fn frequency_parses_canonical_strings() {
|
||||||
@@ -182,3 +184,95 @@ fn day_next_and_prev_still_work() {
|
|||||||
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 11).unwrap())
|
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 11).unwrap())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== DiaryCalendar — weekly naming styles + week-start =====
|
||||||
|
// 2026-05-25 is a Monday (so 2026-05-27 is a Wednesday, 2026-05-24 a Sunday).
|
||||||
|
|
||||||
|
fn day(y: i32, m: u8, d: u8) -> DiaryPeriod {
|
||||||
|
DiaryPeriod::Day(DiaryDate::from_ymd(y, m, d).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn weekly_style_and_week_start_parse() {
|
||||||
|
assert_eq!(WeeklyStyle::parse("date"), WeeklyStyle::Date);
|
||||||
|
assert_eq!(WeeklyStyle::parse("vimwiki"), WeeklyStyle::Date);
|
||||||
|
assert_eq!(WeeklyStyle::parse("iso"), WeeklyStyle::Iso);
|
||||||
|
assert_eq!(WeeklyStyle::parse(""), WeeklyStyle::Iso); // default
|
||||||
|
assert_eq!(WeeklyStyle::parse("WEEK"), WeeklyStyle::Iso);
|
||||||
|
// WeekStart::parse is exercised via the calendar tests below; unknown
|
||||||
|
// names fall back to Monday.
|
||||||
|
assert_eq!(WeekStart::parse("sunday"), WeekStart::parse("SUNDAY"));
|
||||||
|
assert_eq!(WeekStart::parse("nonsense"), WeekStart::monday());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn date_mode_weekly_today_is_a_day() {
|
||||||
|
let cal = DiaryCalendar::new(
|
||||||
|
DiaryFrequency::Weekly,
|
||||||
|
WeeklyStyle::Date,
|
||||||
|
WeekStart::monday(),
|
||||||
|
);
|
||||||
|
assert!(matches!(cal.today(), DiaryPeriod::Day(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iso_mode_weekly_today_is_a_week() {
|
||||||
|
let cal = DiaryCalendar::new(
|
||||||
|
DiaryFrequency::Weekly,
|
||||||
|
WeeklyStyle::Iso,
|
||||||
|
WeekStart::monday(),
|
||||||
|
);
|
||||||
|
assert!(matches!(cal.today(), DiaryPeriod::Week { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn date_mode_weekly_steps_seven_days_from_the_week_start_monday() {
|
||||||
|
let cal = DiaryCalendar::new(
|
||||||
|
DiaryFrequency::Weekly,
|
||||||
|
WeeklyStyle::Date,
|
||||||
|
WeekStart::monday(),
|
||||||
|
);
|
||||||
|
// From a Wednesday: snap back to Mon 05-25, then ±7.
|
||||||
|
assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 6, 1));
|
||||||
|
assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 18));
|
||||||
|
// From the Monday itself: no snap, just ±7.
|
||||||
|
assert_eq!(cal.next(day(2026, 5, 25)), day(2026, 6, 1));
|
||||||
|
assert_eq!(cal.prev(day(2026, 5, 25)), day(2026, 5, 18));
|
||||||
|
// Stem is the plain date, like upstream.
|
||||||
|
assert_eq!(cal.next(day(2026, 5, 27)).format(), "2026-06-01");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn date_mode_weekly_honours_a_sunday_week_start() {
|
||||||
|
let cal = DiaryCalendar::new(
|
||||||
|
DiaryFrequency::Weekly,
|
||||||
|
WeeklyStyle::Date,
|
||||||
|
WeekStart::parse("sunday"),
|
||||||
|
);
|
||||||
|
// Wed 05-27 snaps back to Sun 05-24, +7 = Sun 05-31.
|
||||||
|
assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 5, 31));
|
||||||
|
assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 17));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iso_mode_weekly_delegates_to_iso_week_stepping() {
|
||||||
|
let cal = DiaryCalendar::new(
|
||||||
|
DiaryFrequency::Weekly,
|
||||||
|
WeeklyStyle::Iso,
|
||||||
|
WeekStart::monday(),
|
||||||
|
);
|
||||||
|
let wk = DiaryPeriod::week_containing(DiaryDate::from_ymd(2026, 5, 27).unwrap());
|
||||||
|
assert_eq!(cal.next(wk), wk.next());
|
||||||
|
assert_eq!(cal.prev(wk), wk.prev());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daily_calendar_steps_one_day_regardless_of_weekly_fields() {
|
||||||
|
let cal = DiaryCalendar::new(
|
||||||
|
DiaryFrequency::Daily,
|
||||||
|
WeeklyStyle::Date,
|
||||||
|
WeekStart::parse("sunday"),
|
||||||
|
);
|
||||||
|
assert_eq!(cal.next(day(2026, 5, 27)), day(2026, 5, 28));
|
||||||
|
assert_eq!(cal.prev(day(2026, 5, 27)), day(2026, 5, 26));
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,14 +63,76 @@ fn heading_levels() {
|
|||||||
for level in 1..=6 {
|
for level in 1..=6 {
|
||||||
let bars = "=".repeat(level);
|
let bars = "=".repeat(level);
|
||||||
let out = render(&format!("{bars} Title {bars}\n"));
|
let out = render(&format!("{bars} Title {bars}\n"));
|
||||||
assert!(out.starts_with(&format!("<h{level}>Title</h{level}>")));
|
// vimwiki structure: <div id="hier"><h{l} id="flat" class="header">
|
||||||
|
// <a href="#hier">…</a></h{l}></div>. A lone heading has hier == flat.
|
||||||
|
assert!(out.starts_with(&format!(
|
||||||
|
"<div id=\"Title\"><h{level} id=\"Title\" class=\"header\"><a href=\"#Title\">Title</a></h{level}></div>"
|
||||||
|
)), "got: {out}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn centered_heading_gets_centered_class() {
|
fn centered_heading_gets_centered_class() {
|
||||||
let out = render(" = T =\n");
|
let out = render(" = T =\n");
|
||||||
assert!(out.contains("<h1 class=\"centered\">T</h1>"));
|
assert!(out.contains("<h1 class=\"centered\" id=\"T\">T</h1>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaceless_heading_renders_as_heading() {
|
||||||
|
// `==Heading==` (no surrounding spaces) is a heading, like vimwiki — not a
|
||||||
|
// literal `<p>==Heading==</p>`.
|
||||||
|
let out = render("==Plain==\n");
|
||||||
|
assert!(
|
||||||
|
out.contains(
|
||||||
|
"<div id=\"Plain\"><h2 id=\"Plain\" class=\"header\">\
|
||||||
|
<a href=\"#Plain\">Plain</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
// The spaceless + keyword combo from the report: a header, not a badge.
|
||||||
|
let out2 = render("==TODO==\n");
|
||||||
|
assert!(
|
||||||
|
out2.contains("<h2 id=\"TODO\" class=\"header\">"),
|
||||||
|
"got: {out2}"
|
||||||
|
);
|
||||||
|
assert!(!out2.contains("<p>"), "not a paragraph: {out2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heading_keyword_renders_as_plain_text_not_badge() {
|
||||||
|
// `== TODO ==` is a section title, not an inline keyword: it must render as
|
||||||
|
// a normal header (with a correct, non-empty id), not a `<span class="todo">`
|
||||||
|
// badge. Regression for the exported header losing its header styling + id.
|
||||||
|
let out = render("== TODO ==\n");
|
||||||
|
assert!(
|
||||||
|
out.contains(
|
||||||
|
"<div id=\"TODO\"><h2 id=\"TODO\" class=\"header\">\
|
||||||
|
<a href=\"#TODO\">TODO</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
assert!(!out.contains("class=\"todo\""), "no keyword badge: {out}");
|
||||||
|
|
||||||
|
// A keyword mid-title keeps its text in the id (no dropped word / double
|
||||||
|
// space) and still renders plainly.
|
||||||
|
let out2 = render("= My TODO list =\n");
|
||||||
|
assert!(out2.contains("id=\"My TODO list\""), "got: {out2}");
|
||||||
|
assert!(!out2.contains("class=\"todo\""), "got: {out2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wikilink_description_keeps_inner_brackets() {
|
||||||
|
// A `]` inside a wikilink description (e.g. a `[TICKET-1]`) must not close
|
||||||
|
// the link early. Regression for descriptions with brackets being mangled.
|
||||||
|
let out = render("[[page|Task [B-1]]]\n");
|
||||||
|
assert!(
|
||||||
|
out.contains("<a href=\"page.html\">Task [B-1]</a>"),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
// Two bracketed links on one line both resolve.
|
||||||
|
let out2 = render("[[a|X [1]]] and [[b|Y [2]]]\n");
|
||||||
|
assert!(out2.contains("<a href=\"a.html\">X [1]</a>"), "got: {out2}");
|
||||||
|
assert!(out2.contains("<a href=\"b.html\">Y [2]</a>"), "got: {out2}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -94,11 +156,26 @@ fn blockquote_lines_become_paragraphs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preformatted_block_emits_pre_code_with_language_class() {
|
fn preformatted_block_emits_pre_with_raw_fence_attrs() {
|
||||||
|
// vimwiki drops the verbatim fence text into the <pre> tag (`{{{rust` →
|
||||||
|
// `<pre rust>`), with no inner <code> wrapper.
|
||||||
let out = render("{{{rust\nfn main() {}\n}}}\n");
|
let out = render("{{{rust\nfn main() {}\n}}}\n");
|
||||||
assert!(out.contains("<pre><code class=\"language-rust\">"));
|
assert!(out.contains("<pre rust>"), "got: {out}");
|
||||||
assert!(out.contains("fn main() {}"));
|
assert!(out.contains("fn main() {}"));
|
||||||
assert!(out.contains("</code></pre>"));
|
assert!(out.contains("</pre>"));
|
||||||
|
assert!(!out.contains("<code"), "no inner code element: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preformatted_block_without_attrs_is_bare_pre() {
|
||||||
|
let out = render("{{{\nplain\n}}}\n");
|
||||||
|
assert!(out.contains("<pre>plain"), "got: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preformatted_block_keeps_full_attr_string() {
|
||||||
|
let out = render("{{{class=\"brush: python\"\nx\n}}}\n");
|
||||||
|
assert!(out.contains("<pre class=\"brush: python\">"), "got: {out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -237,6 +314,58 @@ fn anchor_only_link() {
|
|||||||
assert!(out.contains("<a href=\"#Section\">"));
|
assert!(out.contains("<a href=\"#Section\">"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heading_id_matches_anchor_link_href() {
|
||||||
|
// The heading carries an `id` equal to its plain text, so a `#anchor`
|
||||||
|
// link (TOC or cross-reference) actually lands on it. Regression guard
|
||||||
|
// for "exported HTML anchor links don't work".
|
||||||
|
let out = render("= My Section =\n\n[[#My Section]]\n");
|
||||||
|
assert!(
|
||||||
|
out.contains(
|
||||||
|
"<div id=\"My Section\"><h1 id=\"My Section\" class=\"header\">\
|
||||||
|
<a href=\"#My Section\">My Section</a></h1></div>"
|
||||||
|
),
|
||||||
|
"heading id: {out}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.contains("<a href=\"#My Section\">"),
|
||||||
|
"anchor href: {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toc_header_heading_wrapped_in_div_toc() {
|
||||||
|
// The TOC section heading gets a `<div class="toc">` wrapper (vimwiki
|
||||||
|
// scheme) so `.toc` stylesheet rules apply. The class is on the div, not
|
||||||
|
// the heading.
|
||||||
|
let doc = VimwikiSyntax::new().parse("== Contents ==\n");
|
||||||
|
let out = HtmlRenderer::new()
|
||||||
|
.with_toc_header("Contents")
|
||||||
|
.render_to_string(&doc)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
out.contains("<div class=\"toc\"><h2 id=\"Contents\">Contents</h2></div>"),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_toc_heading_not_wrapped() {
|
||||||
|
let doc = VimwikiSyntax::new().parse("== Intro ==\n");
|
||||||
|
let out = HtmlRenderer::new()
|
||||||
|
.with_toc_header("Contents")
|
||||||
|
.render_to_string(&doc)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
out.contains(
|
||||||
|
"<div id=\"Intro\"><h2 id=\"Intro\" class=\"header\">\
|
||||||
|
<a href=\"#Intro\">Intro</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
assert!(!out.contains("class=\"toc\""), "got: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interwiki_numbered_link() {
|
fn interwiki_numbered_link() {
|
||||||
let out = render("[[wiki1:Page]]\n");
|
let out = render("[[wiki1:Page]]\n");
|
||||||
@@ -299,7 +428,7 @@ fn template_substitutes_content_and_title() {
|
|||||||
);
|
);
|
||||||
let out = renderer.render_to_string(&doc).unwrap();
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
assert!(out.contains("<title>My Page</title>"));
|
assert!(out.contains("<title>My Page</title>"));
|
||||||
assert!(out.contains("<body><h1>Hello</h1>"));
|
assert!(out.contains("<body><div id=\"Hello\"><h1 id=\"Hello\" class=\"header\"><a href=\"#Hello\">Hello</a></h1></div>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -308,7 +437,7 @@ fn template_with_missing_title_substitutes_empty_string() {
|
|||||||
let renderer = HtmlRenderer::new().with_template("<title>{{title}}</title>{{content}}");
|
let renderer = HtmlRenderer::new().with_template("<title>{{title}}</title>{{content}}");
|
||||||
let out = renderer.render_to_string(&doc).unwrap();
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
assert!(out.contains("<title></title>"));
|
assert!(out.contains("<title></title>"));
|
||||||
assert!(out.contains("<h1>Heading</h1>"));
|
assert!(out.contains("<div id=\"Heading\"><h1 id=\"Heading\" class=\"header\"><a href=\"#Heading\">Heading</a></h1></div>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -323,11 +452,72 @@ fn template_substitutes_vimwiki_percent_placeholders() {
|
|||||||
.with_var("root_path", "../");
|
.with_var("root_path", "../");
|
||||||
let out = renderer.render_to_string(&doc).unwrap();
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
assert!(out.contains("<title>My Page</title>"), "title: {out}");
|
assert!(out.contains("<title>My Page</title>"), "title: {out}");
|
||||||
assert!(out.contains("<body><h1>Hello</h1>"), "content: {out}");
|
assert!(
|
||||||
|
out.contains("<body><div id=\"Hello\"><h1 id=\"Hello\" class=\"header\"><a href=\"#Hello\">Hello</a></h1></div>"),
|
||||||
|
"content: {out}"
|
||||||
|
);
|
||||||
assert!(out.contains("href=\"../style.css\""), "root_path: {out}");
|
assert!(out.contains("href=\"../style.css\""), "root_path: {out}");
|
||||||
assert!(!out.contains('%'), "no placeholder left: {out}");
|
assert!(!out.contains('%'), "no placeholder left: {out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Colour spans =====
|
||||||
|
|
||||||
|
// `ColorNode` is an extension point the vimwiki parser never emits, so build
|
||||||
|
// a document around one by hand and render it directly.
|
||||||
|
fn doc_with_color(color: &str) -> DocumentNode {
|
||||||
|
use nuwiki_core::ast::{ColorNode, ParagraphNode};
|
||||||
|
let color_node = InlineNode::Color(ColorNode {
|
||||||
|
span: Span::default(),
|
||||||
|
color: color.to_string(),
|
||||||
|
children: vec![InlineNode::Text(TextNode {
|
||||||
|
span: Span::default(),
|
||||||
|
content: "hi".to_string(),
|
||||||
|
})],
|
||||||
|
});
|
||||||
|
DocumentNode {
|
||||||
|
children: vec![BlockNode::Paragraph(ParagraphNode {
|
||||||
|
span: Span::default(),
|
||||||
|
children: vec![color_node],
|
||||||
|
})],
|
||||||
|
..DocumentNode::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_dic_name_uses_inline_style_via_default_template() {
|
||||||
|
let doc = doc_with_color("red");
|
||||||
|
let renderer =
|
||||||
|
HtmlRenderer::new().with_colors([("red".to_string(), "crimson".to_string())].into());
|
||||||
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
|
assert!(
|
||||||
|
out.contains("<span style=\"color:crimson\">hi</span>"),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_tag_template_override_is_honored() {
|
||||||
|
let doc = doc_with_color("red");
|
||||||
|
let renderer = HtmlRenderer::new()
|
||||||
|
.with_colors([("red".to_string(), "crimson".to_string())].into())
|
||||||
|
.with_color_template("<em data-style=\"__STYLE__\">__CONTENT__</em>");
|
||||||
|
let out = renderer.render_to_string(&doc).unwrap();
|
||||||
|
assert!(
|
||||||
|
out.contains("<em data-style=\"color:crimson\">hi</em>"),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_color_name_falls_back_to_class() {
|
||||||
|
let doc = doc_with_color("plaid");
|
||||||
|
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
||||||
|
assert!(
|
||||||
|
out.contains("<span class=\"color-plaid\">hi</span>"),
|
||||||
|
"got: {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== End-to-end smoke =====
|
// ===== End-to-end smoke =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -357,7 +547,7 @@ fn x() {}
|
|||||||
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
let out = HtmlRenderer::new().render_to_string(&doc).unwrap();
|
||||||
// A few sanity checks; the full output is exercised piece-by-piece above.
|
// A few sanity checks; the full output is exercised piece-by-piece above.
|
||||||
for needle in [
|
for needle in [
|
||||||
"<h1>Heading</h1>",
|
"<div id=\"Heading\"><h1 id=\"Heading\" class=\"header\"><a href=\"#Heading\">Heading</a></h1></div>",
|
||||||
"<strong>bold</strong>",
|
"<strong>bold</strong>",
|
||||||
"<em>italic</em>",
|
"<em>italic</em>",
|
||||||
"<code>code</code>",
|
"<code>code</code>",
|
||||||
@@ -365,7 +555,7 @@ fn x() {}
|
|||||||
"<blockquote>",
|
"<blockquote>",
|
||||||
"<hr>",
|
"<hr>",
|
||||||
"<table>",
|
"<table>",
|
||||||
"<pre><code class=\"language-rust\">",
|
"<pre rust>",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
out.contains(needle),
|
out.contains(needle),
|
||||||
|
|||||||
@@ -85,6 +85,32 @@ fn centered_heading_is_marked_centered() {
|
|||||||
assert!(matches!(lexed[2], HeadingClose));
|
assert!(matches!(lexed[2], HeadingClose));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaceless_heading_is_a_heading() {
|
||||||
|
// vimwiki accepts `==Heading==` with no spaces around the text.
|
||||||
|
let lexed = lex("==Plain==\n");
|
||||||
|
assert_eq!(
|
||||||
|
lexed,
|
||||||
|
vec![
|
||||||
|
HeadingOpen {
|
||||||
|
level: 2,
|
||||||
|
centered: false
|
||||||
|
},
|
||||||
|
text("Plain"),
|
||||||
|
HeadingClose,
|
||||||
|
Newline,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn marker_only_or_unbalanced_lines_are_not_headings() {
|
||||||
|
// No title between markers, and trailing `=` not at line end → plain text.
|
||||||
|
assert!(!matches!(lex("======\n")[0], HeadingOpen { .. }));
|
||||||
|
assert!(!matches!(lex("==> arrow\n")[0], HeadingOpen { .. }));
|
||||||
|
assert!(!matches!(lex("==a==b\n")[0], HeadingOpen { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn heading_with_inline_bold() {
|
fn heading_with_inline_bold() {
|
||||||
let lexed = lex("== Hello *world* ==\n");
|
let lexed = lex("== Hello *world* ==\n");
|
||||||
@@ -179,7 +205,13 @@ fn preformatted_block_parses_language() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn preformatted_block_parses_attrs() {
|
fn preformatted_block_parses_attrs() {
|
||||||
let lexed = lex("{{{rust class=\"hl\" key=val\nx\n}}}\n");
|
let lexed = lex("{{{rust class=\"hl\" key=val\nx\n}}}\n");
|
||||||
let PreformattedOpen { language, attrs } = &lexed[0] else {
|
let PreformattedOpen {
|
||||||
|
language,
|
||||||
|
attrs,
|
||||||
|
raw,
|
||||||
|
..
|
||||||
|
} = &lexed[0]
|
||||||
|
else {
|
||||||
panic!("expected PreformattedOpen");
|
panic!("expected PreformattedOpen");
|
||||||
};
|
};
|
||||||
assert_eq!(language.as_deref(), Some("rust"));
|
assert_eq!(language.as_deref(), Some("rust"));
|
||||||
@@ -187,6 +219,8 @@ fn preformatted_block_parses_attrs() {
|
|||||||
expected.insert("class".into(), "hl".into());
|
expected.insert("class".into(), "hl".into());
|
||||||
expected.insert("key".into(), "val".into());
|
expected.insert("key".into(), "val".into());
|
||||||
assert_eq!(attrs, &expected);
|
assert_eq!(attrs, &expected);
|
||||||
|
// The verbatim fence text is preserved for the HTML renderer.
|
||||||
|
assert_eq!(raw, "rust class=\"hl\" key=val");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Math block =====
|
// ===== Math block =====
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ fn text_of(inlines: &[InlineNode]) -> String {
|
|||||||
InlineNode::Text(t) => out.push_str(&t.content),
|
InlineNode::Text(t) => out.push_str(&t.content),
|
||||||
InlineNode::Bold(b) => out.push_str(&text_of(&b.children)),
|
InlineNode::Bold(b) => out.push_str(&text_of(&b.children)),
|
||||||
InlineNode::Italic(i) => out.push_str(&text_of(&i.children)),
|
InlineNode::Italic(i) => out.push_str(&text_of(&i.children)),
|
||||||
|
// Soft breaks join continuation lines (formerly a Text(" ")).
|
||||||
|
InlineNode::SoftBreak(_) => out.push(' '),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,12 +91,13 @@ fn paragraph_spans_multiple_lines() {
|
|||||||
let BlockNode::Paragraph(p) = &doc.children[0] else {
|
let BlockNode::Paragraph(p) = &doc.children[0] else {
|
||||||
panic!("expected paragraph");
|
panic!("expected paragraph");
|
||||||
};
|
};
|
||||||
// Contents: Text("Hello"), Text(" ") (from soft newline), Text("world")
|
// Contents: Text("Hello"), SoftBreak (from soft newline), Text("world").
|
||||||
let text_concat: String = p
|
let text_concat: String = p
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|n| match n {
|
.filter_map(|n| match n {
|
||||||
InlineNode::Text(t) => Some(t.content.as_str()),
|
InlineNode::Text(t) => Some(t.content.clone()),
|
||||||
|
InlineNode::SoftBreak(_) => Some(" ".to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -190,8 +190,9 @@ fn file_tags_are_deduped_in_metadata() {
|
|||||||
fn renderer_emits_div_with_tag_spans() {
|
fn renderer_emits_div_with_tag_spans() {
|
||||||
let html = render(":todo:done:\n");
|
let html = render(":todo:done:\n");
|
||||||
assert!(html.contains("<div class=\"tags\">"));
|
assert!(html.contains("<div class=\"tags\">"));
|
||||||
assert!(html.contains("<span class=\"tag\" id=\"tag-todo\">todo</span>"));
|
// Bare tag name as the id (vimwiki parity), so `[[Page#todo]]` resolves.
|
||||||
assert!(html.contains("<span class=\"tag\" id=\"tag-done\">done</span>"));
|
assert!(html.contains("<span class=\"tag\" id=\"todo\">todo</span>"));
|
||||||
|
assert!(html.contains("<span class=\"tag\" id=\"done\">done</span>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ name = "nuwiki-ls"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nuwiki-lsp = { path = "../nuwiki-lsp", version = "0.1.0" }
|
nuwiki-lsp = { path = "../nuwiki-lsp", version = "0.4.2" }
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std"] }
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ homepage.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nuwiki-core = { path = "../nuwiki-core", version = "0.1.0" }
|
nuwiki-core = { path = "../nuwiki-core", version = "0.4.2" }
|
||||||
tower-lsp = "0.20"
|
tower-lsp = "0.20"
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "sync", "fs"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "sync", "fs"] }
|
||||||
dashmap = "6"
|
dashmap = "6"
|
||||||
|
|||||||
+839
-177
File diff suppressed because it is too large
Load Diff
+490
-30
@@ -12,7 +12,7 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tower_lsp::lsp_types::{InitializeParams, Url};
|
use tower_lsp::lsp_types::InitializeParams;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -41,22 +41,45 @@ pub struct WikiConfig {
|
|||||||
/// diary commands use. `daily` is supported; the others fall through
|
/// diary commands use. `daily` is supported; the others fall through
|
||||||
/// to vimwiki-style names but the commands are no-ops for them.
|
/// to vimwiki-style names but the commands are no-ops for them.
|
||||||
pub diary_frequency: String,
|
pub diary_frequency: String,
|
||||||
/// Caption level for the diary index page headings (1 = h1).
|
/// Weekly-diary naming: `iso` (nuwiki default — `YYYY-Www` labels,
|
||||||
pub diary_caption_level: u8,
|
/// always Monday-based) or `date`/`vimwiki` (the week-start day's date
|
||||||
|
/// `YYYY-MM-DD`, honouring `diary_start_week_day`). Only affects
|
||||||
|
/// `diary_frequency = weekly`.
|
||||||
|
pub diary_weekly_style: String,
|
||||||
|
/// Weekday the diary week begins on (`monday`..`sunday`, vimwiki's
|
||||||
|
/// `diary_start_week_day`). Used only when `diary_weekly_style = date`.
|
||||||
|
pub diary_start_week_day: String,
|
||||||
|
/// Caption level for the diary index page headings (1 = h1). vimwiki's
|
||||||
|
/// `diary_caption_level` (`min: -1`). nuwiki uses it as the base level of
|
||||||
|
/// the year/month index tree; values `< 0` clamp to `0` (upstream's `-1`
|
||||||
|
/// "read no per-page captions" mode isn't modelled — nuwiki builds the
|
||||||
|
/// tree from dates, not page headers).
|
||||||
|
pub diary_caption_level: i8,
|
||||||
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
||||||
/// index.
|
/// index.
|
||||||
pub diary_sort: String,
|
pub diary_sort: String,
|
||||||
/// H1 text for the diary index page.
|
/// H1 text for the diary index page.
|
||||||
pub diary_header: String,
|
pub diary_header: String,
|
||||||
|
/// vimwiki `diary_months`: month-number → display name for the diary
|
||||||
|
/// index tree. Twelve entries (January..December) by default; a custom
|
||||||
|
/// list falls back to the English name for any missing/out-of-range slot.
|
||||||
|
pub diary_months: Vec<String>,
|
||||||
/// Vimwiki's `g:vimwiki_listsyms`. A progression of checkbox glyphs
|
/// Vimwiki's `g:vimwiki_listsyms`. A progression of checkbox glyphs
|
||||||
/// from "empty" (first) to "done" (last); the lexer recognises these
|
/// from "empty" (first) to "done" (last); the lexer recognises these
|
||||||
/// glyphs and the toggle/cycle commands walk them. Any length ≥ 2 is
|
/// glyphs and the toggle/cycle commands walk them. Any length ≥ 2 is
|
||||||
/// honoured. Default: `" .oOX"`.
|
/// honoured. Default: `" .oOX"`.
|
||||||
pub listsyms: String,
|
pub listsyms: String,
|
||||||
|
/// Vimwiki's `g:vimwiki_listsym_rejected` — the glyph for a cancelled
|
||||||
|
/// checkbox (`[-]` by default). First character is used.
|
||||||
|
pub listsym_rejected: String,
|
||||||
/// When toggling a parent list item, also cascade to descendants.
|
/// When toggling a parent list item, also cascade to descendants.
|
||||||
pub listsyms_propagate: bool,
|
pub listsyms_propagate: bool,
|
||||||
/// `-1` keeps tight lists; positive integers indent list items by
|
/// vimwiki `list_margin`: number of leading spaces before bullets in
|
||||||
/// that many extra columns. Layout hint for the renderer.
|
/// **buffer-side generated lists** — `:VimwikiGenerateLinks`, the TOC, the
|
||||||
|
/// tags index, and the diary index. `>= 0` indents by that many spaces;
|
||||||
|
/// `< 0` means "no margin" (default `-1`; `0` for markdown wikis).
|
||||||
|
/// Divergence: upstream resolves `< 0` to the buffer's `'shiftwidth'`,
|
||||||
|
/// which the server can't observe, so negatives collapse to zero indent.
|
||||||
pub list_margin: i32,
|
pub list_margin: i32,
|
||||||
/// Character used in place of literal spaces inside wikilink
|
/// Character used in place of literal spaces inside wikilink
|
||||||
/// targets when writing to disk (a single space `" "` by default,
|
/// targets when writing to disk (a single space `" "` by default,
|
||||||
@@ -64,6 +87,48 @@ pub struct WikiConfig {
|
|||||||
pub links_space_char: String,
|
pub links_space_char: String,
|
||||||
/// Rebuild the page's TOC on save when set.
|
/// Rebuild the page's TOC on save when set.
|
||||||
pub auto_toc: bool,
|
pub auto_toc: bool,
|
||||||
|
/// On save, regenerate an existing `Generated Links` section
|
||||||
|
/// (vimwiki's `auto_generate_links`).
|
||||||
|
pub auto_generate_links: bool,
|
||||||
|
/// On save, regenerate an existing `Generated Tags` index section
|
||||||
|
/// (vimwiki's `auto_generate_tags`).
|
||||||
|
pub auto_generate_tags: bool,
|
||||||
|
/// On save of a diary entry, regenerate the diary index page
|
||||||
|
/// (vimwiki's `auto_diary_index`).
|
||||||
|
pub auto_diary_index: bool,
|
||||||
|
/// Heading text + level for the generated `:VimwikiTOC` section
|
||||||
|
/// (vimwiki's `toc_header` / `toc_header_level`).
|
||||||
|
pub toc_header: String,
|
||||||
|
pub toc_header_level: u8,
|
||||||
|
/// Heading text + level for `:VimwikiGenerateLinks` (`links_header` /
|
||||||
|
/// `links_header_level`).
|
||||||
|
pub links_header: String,
|
||||||
|
pub links_header_level: u8,
|
||||||
|
/// Heading text + level for the no-arg `:VimwikiGenerateTagLinks` index
|
||||||
|
/// (`tags_header` / `tags_header_level`).
|
||||||
|
pub tags_header: String,
|
||||||
|
pub tags_header_level: u8,
|
||||||
|
/// vimwiki `create_link` (default `true`): when following a link to a
|
||||||
|
/// missing page, create it. When `false`, missing-link follow is a no-op.
|
||||||
|
pub create_link: bool,
|
||||||
|
/// vimwiki `dir_link`: index file opened when following a link to a
|
||||||
|
/// directory (e.g. `index` → `dir/index.wiki`). Empty = open the directory.
|
||||||
|
pub dir_link: String,
|
||||||
|
/// vimwiki `bullet_types`: ordered list of unordered-bullet glyphs for
|
||||||
|
/// this wiki's syntax (default `-`, `*`, `#`); drives `cycle_bullets`.
|
||||||
|
pub bullet_types: Vec<String>,
|
||||||
|
/// vimwiki `cycle_bullets` (default `false`): rotate an unordered item's
|
||||||
|
/// glyph through `bullet_types` as it is indented/dedented.
|
||||||
|
pub cycle_bullets: bool,
|
||||||
|
/// vimwiki `generated_links_caption` (default `false`): emit
|
||||||
|
/// `[[page|Heading]]` (first heading as caption) from `:VimwikiGenerateLinks`.
|
||||||
|
pub generated_links_caption: bool,
|
||||||
|
/// vimwiki `toc_link_format` (default `0`): `0` = `[[#anchor|title]]`
|
||||||
|
/// (with description); `1` = `[[#anchor]]` (anchor only, no description).
|
||||||
|
pub toc_link_format: u8,
|
||||||
|
/// vimwiki `table_reduce_last_col` (default `false`): don't pad the last
|
||||||
|
/// table column to fill — keep it at its minimum width.
|
||||||
|
pub table_reduce_last_col: bool,
|
||||||
/// HTML export options.
|
/// HTML export options.
|
||||||
pub html: HtmlConfig,
|
pub html: HtmlConfig,
|
||||||
}
|
}
|
||||||
@@ -98,6 +163,51 @@ pub struct HtmlConfig {
|
|||||||
/// renderer falls back to `class="color-<name>"` when a name
|
/// renderer falls back to `class="color-<name>"` when a name
|
||||||
/// isn't in the dict.
|
/// isn't in the dict.
|
||||||
pub color_dic: std::collections::HashMap<String, String>,
|
pub color_dic: std::collections::HashMap<String, String>,
|
||||||
|
/// External wiki→HTML converter (vimwiki's `custom_wiki2html`). When
|
||||||
|
/// non-empty, export shells out to it instead of the built-in renderer.
|
||||||
|
pub custom_wiki2html: String,
|
||||||
|
/// Extra args appended to the `custom_wiki2html` invocation.
|
||||||
|
pub custom_wiki2html_args: String,
|
||||||
|
/// URL prefix prepended to RSS feed / diary item links (`base_url`).
|
||||||
|
pub base_url: String,
|
||||||
|
/// vimwiki `html_header_numbering`: heading level at which automatic
|
||||||
|
/// section numbering starts in HTML export (`0` = off, the default).
|
||||||
|
pub html_header_numbering: u8,
|
||||||
|
/// vimwiki `html_header_numbering_sym`: symbol appended after the
|
||||||
|
/// section number (e.g. `.` → `1.2. Heading`). Empty by default.
|
||||||
|
pub html_header_numbering_sym: String,
|
||||||
|
/// vimwiki `toc_header`: the heading text that identifies the TOC
|
||||||
|
/// section. When a heading matches this value (case-insensitive), it
|
||||||
|
/// gets `class="toc"` in HTML export.
|
||||||
|
pub toc_header: String,
|
||||||
|
/// vimwiki `rss_name`: filename of the generated RSS feed (default
|
||||||
|
/// `rss.xml`), relative to `html_path`.
|
||||||
|
pub rss_name: String,
|
||||||
|
/// vimwiki `rss_max_items`: cap on the number of diary items in the RSS
|
||||||
|
/// feed (default `10`; `0` = unlimited-but-empty per upstream's min).
|
||||||
|
pub rss_max_items: usize,
|
||||||
|
/// vimwiki `valid_html_tags`: inline HTML tag names that pass through to
|
||||||
|
/// HTML export unescaped (everything else has `<`/`>` escaped). Default
|
||||||
|
/// `b,i,s,u,sub,sup,kbd,br,hr,div,center,strong,em`.
|
||||||
|
pub valid_html_tags: Vec<String>,
|
||||||
|
/// vimwiki `list_ignore_newline` (default `true`): a single newline inside
|
||||||
|
/// a list item is a space in HTML export; when `false` it becomes `<br>`.
|
||||||
|
pub list_ignore_newline: bool,
|
||||||
|
/// vimwiki `text_ignore_newline` (default `true`): a single newline inside
|
||||||
|
/// a paragraph is a space in HTML export; when `false` it becomes `<br>`.
|
||||||
|
pub text_ignore_newline: bool,
|
||||||
|
/// vimwiki `emoji_enable` (default `true`): substitute `:alias:` emoji
|
||||||
|
/// shortcodes with their glyph during HTML export.
|
||||||
|
pub emoji_enable: bool,
|
||||||
|
/// vimwiki `user_htmls`: basenames of HTML files with no wiki source that
|
||||||
|
/// `allToHtml` must not prune. Empty by default.
|
||||||
|
pub user_htmls: Vec<String>,
|
||||||
|
/// vimwiki `color_tag_template`: HTML template for a colour span whose name
|
||||||
|
/// resolves in `color_dic`. `__STYLE__` expands to the inline style
|
||||||
|
/// (`color:<css>`) and `__CONTENT__` to the rendered inner HTML. Default
|
||||||
|
/// matches upstream's `<span style="…">…</span>` template. Consumed by
|
||||||
|
/// `HtmlRenderer::render_color`.
|
||||||
|
pub color_tag_template: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HtmlConfig {
|
impl Default for HtmlConfig {
|
||||||
@@ -112,11 +222,57 @@ impl Default for HtmlConfig {
|
|||||||
auto_export: false,
|
auto_export: false,
|
||||||
html_filename_parameterization: false,
|
html_filename_parameterization: false,
|
||||||
exclude_files: Vec::new(),
|
exclude_files: Vec::new(),
|
||||||
color_dic: std::collections::HashMap::new(),
|
color_dic: default_color_dic(),
|
||||||
|
custom_wiki2html: String::new(),
|
||||||
|
custom_wiki2html_args: String::new(),
|
||||||
|
base_url: String::new(),
|
||||||
|
html_header_numbering: 0,
|
||||||
|
html_header_numbering_sym: String::new(),
|
||||||
|
toc_header: default_toc_header(),
|
||||||
|
rss_name: "rss.xml".into(),
|
||||||
|
rss_max_items: 10,
|
||||||
|
valid_html_tags: default_valid_html_tags(),
|
||||||
|
list_ignore_newline: true,
|
||||||
|
text_ignore_newline: true,
|
||||||
|
emoji_enable: true,
|
||||||
|
user_htmls: Vec::new(),
|
||||||
|
color_tag_template: default_color_tag_template(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// vimwiki's default `valid_html_tags` — inline tags allowed through export.
|
||||||
|
fn default_valid_html_tags() -> Vec<String> {
|
||||||
|
"b,i,s,u,sub,sup,kbd,br,hr,div,center,strong,em"
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// vimwiki's default `color_tag_template`.
|
||||||
|
fn default_color_tag_template() -> String {
|
||||||
|
"<span style=\"__STYLE__\">__CONTENT__</span>".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A populated default `color_dic` (vimwiki ships a palette rather than the
|
||||||
|
/// empty map nuwiki previously defaulted to). Names → CSS colour value.
|
||||||
|
fn default_color_dic() -> std::collections::HashMap<String, String> {
|
||||||
|
[
|
||||||
|
("default", "inherit"),
|
||||||
|
("red", "red"),
|
||||||
|
("green", "green"),
|
||||||
|
("blue", "blue"),
|
||||||
|
("yellow", "#b8860b"),
|
||||||
|
("magenta", "magenta"),
|
||||||
|
("cyan", "darkcyan"),
|
||||||
|
("gray", "gray"),
|
||||||
|
("grey", "gray"),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl HtmlConfig {
|
impl HtmlConfig {
|
||||||
/// Synthesise default paths under `root` when the user hasn't
|
/// Synthesise default paths under `root` when the user hasn't
|
||||||
/// specified explicit ones. `html_path` defaults to `<root>/_html`
|
/// specified explicit ones. `html_path` defaults to `<root>/_html`
|
||||||
@@ -174,14 +330,34 @@ impl WikiConfig {
|
|||||||
diary_rel_path: d.diary_rel_path,
|
diary_rel_path: d.diary_rel_path,
|
||||||
diary_index: d.diary_index,
|
diary_index: d.diary_index,
|
||||||
diary_frequency: d.diary_frequency,
|
diary_frequency: d.diary_frequency,
|
||||||
|
diary_weekly_style: d.diary_weekly_style,
|
||||||
|
diary_start_week_day: d.diary_start_week_day,
|
||||||
diary_caption_level: d.diary_caption_level,
|
diary_caption_level: d.diary_caption_level,
|
||||||
diary_sort: d.diary_sort,
|
diary_sort: d.diary_sort,
|
||||||
diary_header: d.diary_header,
|
diary_header: d.diary_header,
|
||||||
|
diary_months: d.diary_months,
|
||||||
listsyms: d.listsyms,
|
listsyms: d.listsyms,
|
||||||
|
listsym_rejected: d.listsym_rejected,
|
||||||
listsyms_propagate: d.listsyms_propagate,
|
listsyms_propagate: d.listsyms_propagate,
|
||||||
list_margin: d.list_margin,
|
list_margin: d.list_margin,
|
||||||
links_space_char: d.links_space_char,
|
links_space_char: d.links_space_char,
|
||||||
auto_toc: d.auto_toc,
|
auto_toc: d.auto_toc,
|
||||||
|
auto_generate_links: d.auto_generate_links,
|
||||||
|
auto_generate_tags: d.auto_generate_tags,
|
||||||
|
auto_diary_index: d.auto_diary_index,
|
||||||
|
toc_header: d.toc_header,
|
||||||
|
toc_header_level: d.toc_header_level,
|
||||||
|
links_header: d.links_header,
|
||||||
|
links_header_level: d.links_header_level,
|
||||||
|
tags_header: d.tags_header,
|
||||||
|
tags_header_level: d.tags_header_level,
|
||||||
|
create_link: d.create_link,
|
||||||
|
dir_link: d.dir_link,
|
||||||
|
bullet_types: d.bullet_types,
|
||||||
|
cycle_bullets: d.cycle_bullets,
|
||||||
|
generated_links_caption: d.generated_links_caption,
|
||||||
|
toc_link_format: d.toc_link_format,
|
||||||
|
table_reduce_last_col: d.table_reduce_last_col,
|
||||||
html: HtmlConfig::default(),
|
html: HtmlConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,14 +377,34 @@ impl WikiConfig {
|
|||||||
diary_rel_path: d.diary_rel_path,
|
diary_rel_path: d.diary_rel_path,
|
||||||
diary_index: d.diary_index,
|
diary_index: d.diary_index,
|
||||||
diary_frequency: d.diary_frequency,
|
diary_frequency: d.diary_frequency,
|
||||||
|
diary_weekly_style: d.diary_weekly_style,
|
||||||
|
diary_start_week_day: d.diary_start_week_day,
|
||||||
diary_caption_level: d.diary_caption_level,
|
diary_caption_level: d.diary_caption_level,
|
||||||
diary_sort: d.diary_sort,
|
diary_sort: d.diary_sort,
|
||||||
diary_header: d.diary_header,
|
diary_header: d.diary_header,
|
||||||
|
diary_months: d.diary_months,
|
||||||
listsyms: d.listsyms,
|
listsyms: d.listsyms,
|
||||||
|
listsym_rejected: d.listsym_rejected,
|
||||||
listsyms_propagate: d.listsyms_propagate,
|
listsyms_propagate: d.listsyms_propagate,
|
||||||
list_margin: d.list_margin,
|
list_margin: d.list_margin,
|
||||||
links_space_char: d.links_space_char,
|
links_space_char: d.links_space_char,
|
||||||
auto_toc: d.auto_toc,
|
auto_toc: d.auto_toc,
|
||||||
|
auto_generate_links: d.auto_generate_links,
|
||||||
|
auto_generate_tags: d.auto_generate_tags,
|
||||||
|
auto_diary_index: d.auto_diary_index,
|
||||||
|
toc_header: d.toc_header,
|
||||||
|
toc_header_level: d.toc_header_level,
|
||||||
|
links_header: d.links_header,
|
||||||
|
links_header_level: d.links_header_level,
|
||||||
|
tags_header: d.tags_header,
|
||||||
|
tags_header_level: d.tags_header_level,
|
||||||
|
create_link: d.create_link,
|
||||||
|
dir_link: d.dir_link,
|
||||||
|
bullet_types: d.bullet_types,
|
||||||
|
cycle_bullets: d.cycle_bullets,
|
||||||
|
generated_links_caption: d.generated_links_caption,
|
||||||
|
toc_link_format: d.toc_link_format,
|
||||||
|
table_reduce_last_col: d.table_reduce_last_col,
|
||||||
html,
|
html,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +441,23 @@ impl WikiConfig {
|
|||||||
pub fn frequency(&self) -> nuwiki_core::date::DiaryFrequency {
|
pub fn frequency(&self) -> nuwiki_core::date::DiaryFrequency {
|
||||||
nuwiki_core::date::DiaryFrequency::parse(&self.diary_frequency)
|
nuwiki_core::date::DiaryFrequency::parse(&self.diary_frequency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The checkbox palette for this wiki — the `listsyms` progression plus
|
||||||
|
/// the configured `listsym_rejected` glyph (default `-`).
|
||||||
|
pub fn list_syms(&self) -> nuwiki_core::listsyms::ListSyms {
|
||||||
|
let rejected = self.listsym_rejected.chars().next().unwrap_or('-');
|
||||||
|
nuwiki_core::listsyms::ListSyms::new_with_rejected(&self.listsyms, rejected)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The diary navigation policy (frequency + weekly naming/week-start),
|
||||||
|
/// used by the diary commands to compute today / next / prev.
|
||||||
|
pub fn diary_calendar(&self) -> nuwiki_core::date::DiaryCalendar {
|
||||||
|
nuwiki_core::date::DiaryCalendar::new(
|
||||||
|
self.frequency(),
|
||||||
|
nuwiki_core::date::WeeklyStyle::parse(&self.diary_weekly_style),
|
||||||
|
nuwiki_core::date::WeekStart::parse(&self.diary_start_week_day),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_diary_rel_path() -> String {
|
fn default_diary_rel_path() -> String {
|
||||||
@@ -263,6 +476,15 @@ fn default_diary_frequency() -> String {
|
|||||||
"daily".to_string()
|
"daily".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_diary_weekly_style() -> String {
|
||||||
|
// nuwiki's original ISO-week labels; opt into `date` for vimwiki parity.
|
||||||
|
"iso".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_diary_start_week_day() -> String {
|
||||||
|
"monday".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_diary_sort() -> String {
|
fn default_diary_sort() -> String {
|
||||||
"desc".to_string()
|
"desc".to_string()
|
||||||
}
|
}
|
||||||
@@ -271,10 +493,47 @@ fn default_diary_header() -> String {
|
|||||||
"Diary".to_string()
|
"Diary".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// vimwiki's default `diary_months` — the English month names, January..December.
|
||||||
|
fn default_diary_months() -> Vec<String> {
|
||||||
|
[
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_listsyms() -> String {
|
fn default_listsyms() -> String {
|
||||||
" .oOX".to_string()
|
" .oOX".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_listsym_rejected() -> String {
|
||||||
|
"-".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_toc_header() -> String {
|
||||||
|
"Contents".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_links_header() -> String {
|
||||||
|
"Generated Links".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_tags_header() -> String {
|
||||||
|
"Generated Tags".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_links_space_char() -> String {
|
fn default_links_space_char() -> String {
|
||||||
" ".to_string()
|
" ".to_string()
|
||||||
}
|
}
|
||||||
@@ -288,30 +547,77 @@ fn wiki_defaults() -> WikiDefaults {
|
|||||||
diary_rel_path: default_diary_rel_path(),
|
diary_rel_path: default_diary_rel_path(),
|
||||||
diary_index: default_diary_index(),
|
diary_index: default_diary_index(),
|
||||||
diary_frequency: default_diary_frequency(),
|
diary_frequency: default_diary_frequency(),
|
||||||
diary_caption_level: 1,
|
diary_weekly_style: default_diary_weekly_style(),
|
||||||
|
diary_start_week_day: default_diary_start_week_day(),
|
||||||
|
// Match upstream vimwiki's `diary_caption_level` default of 0 (year
|
||||||
|
// captions at the top level, months one below). Users override per-wiki.
|
||||||
|
diary_caption_level: 0,
|
||||||
diary_sort: default_diary_sort(),
|
diary_sort: default_diary_sort(),
|
||||||
diary_header: default_diary_header(),
|
diary_header: default_diary_header(),
|
||||||
|
diary_months: default_diary_months(),
|
||||||
listsyms: default_listsyms(),
|
listsyms: default_listsyms(),
|
||||||
|
listsym_rejected: default_listsym_rejected(),
|
||||||
listsyms_propagate: true,
|
listsyms_propagate: true,
|
||||||
list_margin: -1,
|
list_margin: -1,
|
||||||
links_space_char: default_links_space_char(),
|
links_space_char: default_links_space_char(),
|
||||||
auto_toc: false,
|
auto_toc: false,
|
||||||
|
auto_generate_links: false,
|
||||||
|
auto_generate_tags: false,
|
||||||
|
auto_diary_index: false,
|
||||||
|
toc_header: default_toc_header(),
|
||||||
|
toc_header_level: 1,
|
||||||
|
links_header: default_links_header(),
|
||||||
|
links_header_level: 1,
|
||||||
|
tags_header: default_tags_header(),
|
||||||
|
tags_header_level: 1,
|
||||||
|
create_link: true,
|
||||||
|
dir_link: String::new(),
|
||||||
|
bullet_types: default_bullet_types(),
|
||||||
|
cycle_bullets: false,
|
||||||
|
generated_links_caption: false,
|
||||||
|
toc_link_format: 0,
|
||||||
|
table_reduce_last_col: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// vimwiki's default `bullet_types` for the `vimwiki` syntax.
|
||||||
|
fn default_bullet_types() -> Vec<String> {
|
||||||
|
["-", "*", "#"].iter().map(|s| s.to_string()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
struct WikiDefaults {
|
struct WikiDefaults {
|
||||||
index: String,
|
index: String,
|
||||||
diary_rel_path: String,
|
diary_rel_path: String,
|
||||||
diary_index: String,
|
diary_index: String,
|
||||||
diary_frequency: String,
|
diary_frequency: String,
|
||||||
diary_caption_level: u8,
|
diary_weekly_style: String,
|
||||||
|
diary_start_week_day: String,
|
||||||
|
diary_caption_level: i8,
|
||||||
diary_sort: String,
|
diary_sort: String,
|
||||||
diary_header: String,
|
diary_header: String,
|
||||||
|
diary_months: Vec<String>,
|
||||||
listsyms: String,
|
listsyms: String,
|
||||||
|
listsym_rejected: String,
|
||||||
listsyms_propagate: bool,
|
listsyms_propagate: bool,
|
||||||
list_margin: i32,
|
list_margin: i32,
|
||||||
links_space_char: String,
|
links_space_char: String,
|
||||||
auto_toc: bool,
|
auto_toc: bool,
|
||||||
|
auto_generate_links: bool,
|
||||||
|
auto_generate_tags: bool,
|
||||||
|
auto_diary_index: bool,
|
||||||
|
toc_header: String,
|
||||||
|
toc_header_level: u8,
|
||||||
|
links_header: String,
|
||||||
|
links_header_level: u8,
|
||||||
|
tags_header: String,
|
||||||
|
tags_header_level: u8,
|
||||||
|
create_link: bool,
|
||||||
|
dir_link: String,
|
||||||
|
bullet_types: Vec<String>,
|
||||||
|
cycle_bullets: bool,
|
||||||
|
generated_links_caption: bool,
|
||||||
|
toc_link_format: u8,
|
||||||
|
table_reduce_last_col: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -333,14 +639,11 @@ impl Config {
|
|||||||
|
|
||||||
let wikis = if let Some(list) = raw.wikis {
|
let wikis = if let Some(list) = raw.wikis {
|
||||||
list.into_iter().map(WikiConfig::from).collect()
|
list.into_iter().map(WikiConfig::from).collect()
|
||||||
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
|
} else if let Some(wc) = params
|
||||||
let mut wc = WikiConfig::from_root(root);
|
.initialization_options
|
||||||
if let Some(ext) = raw.file_extension {
|
.as_ref()
|
||||||
wc.file_extension = ext;
|
.and_then(single_wiki_from_value)
|
||||||
}
|
{
|
||||||
if let Some(s) = raw.syntax {
|
|
||||||
wc.syntax = s;
|
|
||||||
}
|
|
||||||
vec![wc]
|
vec![wc]
|
||||||
} else if let Some(folder) = first_workspace_folder(params) {
|
} else if let Some(folder) = first_workspace_folder(params) {
|
||||||
vec![WikiConfig::from_root(folder)]
|
vec![WikiConfig::from_root(folder)]
|
||||||
@@ -381,14 +684,7 @@ impl Config {
|
|||||||
};
|
};
|
||||||
if let Some(list) = raw.wikis {
|
if let Some(list) = raw.wikis {
|
||||||
self.wikis = list.into_iter().map(WikiConfig::from).collect();
|
self.wikis = list.into_iter().map(WikiConfig::from).collect();
|
||||||
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
|
} else if let Some(wc) = single_wiki_from_value(inner) {
|
||||||
let mut wc = WikiConfig::from_root(root);
|
|
||||||
if let Some(ext) = raw.file_extension {
|
|
||||||
wc.file_extension = ext;
|
|
||||||
}
|
|
||||||
if let Some(s) = raw.syntax {
|
|
||||||
wc.syntax = s;
|
|
||||||
}
|
|
||||||
self.wikis = vec![wc];
|
self.wikis = vec![wc];
|
||||||
}
|
}
|
||||||
// Only touch diagnostic settings when the payload carries them, so a
|
// Only touch diagnostic settings when the payload carries them, so a
|
||||||
@@ -510,6 +806,32 @@ struct RawWiki {
|
|||||||
exclude_files: Option<Vec<String>>,
|
exclude_files: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
color_dic: Option<std::collections::HashMap<String, String>>,
|
color_dic: Option<std::collections::HashMap<String, String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
custom_wiki2html: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
custom_wiki2html_args: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
base_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
html_header_numbering: Option<u8>,
|
||||||
|
#[serde(default)]
|
||||||
|
html_header_numbering_sym: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
rss_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
rss_max_items: Option<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
valid_html_tags: Option<String>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
list_ignore_newline: Option<bool>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
text_ignore_newline: Option<bool>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
emoji_enable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
user_htmls: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
color_tag_template: Option<String>,
|
||||||
// per-wiki keys mirroring vimwiki globals. All optional so
|
// per-wiki keys mirroring vimwiki globals. All optional so
|
||||||
// existing single-wiki configs migrate without touching anything.
|
// existing single-wiki configs migrate without touching anything.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -517,13 +839,21 @@ struct RawWiki {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_frequency: Option<String>,
|
diary_frequency: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_caption_level: Option<u8>,
|
diary_weekly_style: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
diary_start_week_day: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
diary_caption_level: Option<i8>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_sort: Option<String>,
|
diary_sort: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_header: Option<String>,
|
diary_header: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
diary_months: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
listsyms: Option<String>,
|
listsyms: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
listsym_rejected: Option<String>,
|
||||||
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
listsyms_propagate: Option<bool>,
|
listsyms_propagate: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -532,6 +862,38 @@ struct RawWiki {
|
|||||||
links_space_char: Option<String>,
|
links_space_char: Option<String>,
|
||||||
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
auto_toc: Option<bool>,
|
auto_toc: Option<bool>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
auto_generate_links: Option<bool>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
auto_generate_tags: Option<bool>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
auto_diary_index: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
toc_header: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
toc_header_level: Option<u8>,
|
||||||
|
#[serde(default)]
|
||||||
|
links_header: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
links_header_level: Option<u8>,
|
||||||
|
#[serde(default)]
|
||||||
|
tags_header: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
tags_header_level: Option<u8>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
create_link: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
dir_link: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
bullet_types: Option<Vec<String>>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
cycle_bullets: Option<bool>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
generated_links_caption: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
toc_link_format: Option<u8>,
|
||||||
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
|
table_reduce_last_col: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RawWiki> for WikiConfig {
|
impl From<RawWiki> for WikiConfig {
|
||||||
@@ -572,29 +934,130 @@ impl From<RawWiki> for WikiConfig {
|
|||||||
if let Some(v) = r.color_dic {
|
if let Some(v) = r.color_dic {
|
||||||
html.color_dic = v;
|
html.color_dic = v;
|
||||||
}
|
}
|
||||||
|
if let Some(s) = r.custom_wiki2html {
|
||||||
|
html.custom_wiki2html = s;
|
||||||
|
}
|
||||||
|
if let Some(s) = r.custom_wiki2html_args {
|
||||||
|
html.custom_wiki2html_args = s;
|
||||||
|
}
|
||||||
|
if let Some(s) = r.base_url {
|
||||||
|
html.base_url = s;
|
||||||
|
}
|
||||||
|
if let Some(n) = r.html_header_numbering {
|
||||||
|
html.html_header_numbering = n;
|
||||||
|
}
|
||||||
|
if let Some(s) = r.html_header_numbering_sym {
|
||||||
|
html.html_header_numbering_sym = s;
|
||||||
|
}
|
||||||
|
if let Some(ref s) = r.toc_header {
|
||||||
|
html.toc_header = s.clone();
|
||||||
|
}
|
||||||
|
if let Some(s) = r.rss_name {
|
||||||
|
html.rss_name = s;
|
||||||
|
}
|
||||||
|
if let Some(n) = r.rss_max_items {
|
||||||
|
html.rss_max_items = n;
|
||||||
|
}
|
||||||
|
if let Some(s) = r.valid_html_tags {
|
||||||
|
html.valid_html_tags = s.split(',').map(|t| t.trim().to_string()).collect();
|
||||||
|
}
|
||||||
|
if let Some(b) = r.list_ignore_newline {
|
||||||
|
html.list_ignore_newline = b;
|
||||||
|
}
|
||||||
|
if let Some(b) = r.text_ignore_newline {
|
||||||
|
html.text_ignore_newline = b;
|
||||||
|
}
|
||||||
|
if let Some(b) = r.emoji_enable {
|
||||||
|
html.emoji_enable = b;
|
||||||
|
}
|
||||||
|
if let Some(v) = r.user_htmls {
|
||||||
|
html.user_htmls = v;
|
||||||
|
}
|
||||||
|
if let Some(s) = r.color_tag_template {
|
||||||
|
html.color_tag_template = s;
|
||||||
|
}
|
||||||
let d = wiki_defaults();
|
let d = wiki_defaults();
|
||||||
|
let syntax = r.syntax.unwrap_or_else(|| "vimwiki".into());
|
||||||
|
// vimwiki derives `list_margin = 0` for markdown wikis when the key is
|
||||||
|
// unset (vars.vim:651); other syntaxes keep the `-1` default.
|
||||||
|
let list_margin = r.list_margin.unwrap_or(if syntax == "markdown" {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
d.list_margin
|
||||||
|
});
|
||||||
WikiConfig {
|
WikiConfig {
|
||||||
name: r.name.unwrap_or(derived_name),
|
name: r.name.unwrap_or(derived_name),
|
||||||
root,
|
root,
|
||||||
file_extension: r.file_extension.unwrap_or_else(|| ".wiki".into()),
|
file_extension: r.file_extension.unwrap_or_else(|| ".wiki".into()),
|
||||||
syntax: r.syntax.unwrap_or_else(|| "vimwiki".into()),
|
syntax,
|
||||||
index: r.index.unwrap_or(d.index),
|
index: r.index.unwrap_or(d.index),
|
||||||
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
||||||
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
||||||
diary_frequency: r.diary_frequency.unwrap_or(d.diary_frequency),
|
diary_frequency: r.diary_frequency.unwrap_or(d.diary_frequency),
|
||||||
|
diary_weekly_style: r.diary_weekly_style.unwrap_or(d.diary_weekly_style),
|
||||||
|
diary_start_week_day: r.diary_start_week_day.unwrap_or(d.diary_start_week_day),
|
||||||
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
||||||
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
||||||
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
||||||
|
diary_months: r.diary_months.unwrap_or(d.diary_months),
|
||||||
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
||||||
|
listsym_rejected: r.listsym_rejected.unwrap_or(d.listsym_rejected),
|
||||||
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
||||||
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
list_margin,
|
||||||
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
||||||
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
||||||
|
auto_generate_links: r.auto_generate_links.unwrap_or(d.auto_generate_links),
|
||||||
|
auto_generate_tags: r.auto_generate_tags.unwrap_or(d.auto_generate_tags),
|
||||||
|
auto_diary_index: r.auto_diary_index.unwrap_or(d.auto_diary_index),
|
||||||
|
toc_header: r.toc_header.unwrap_or(d.toc_header),
|
||||||
|
toc_header_level: r.toc_header_level.unwrap_or(d.toc_header_level),
|
||||||
|
links_header: r.links_header.unwrap_or(d.links_header),
|
||||||
|
links_header_level: r.links_header_level.unwrap_or(d.links_header_level),
|
||||||
|
tags_header: r.tags_header.unwrap_or(d.tags_header),
|
||||||
|
tags_header_level: r.tags_header_level.unwrap_or(d.tags_header_level),
|
||||||
|
create_link: r.create_link.unwrap_or(d.create_link),
|
||||||
|
dir_link: r.dir_link.unwrap_or(d.dir_link),
|
||||||
|
bullet_types: r
|
||||||
|
.bullet_types
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.unwrap_or(d.bullet_types),
|
||||||
|
cycle_bullets: r.cycle_bullets.unwrap_or(d.cycle_bullets),
|
||||||
|
generated_links_caption: r
|
||||||
|
.generated_links_caption
|
||||||
|
.unwrap_or(d.generated_links_caption),
|
||||||
|
toc_link_format: r.toc_link_format.unwrap_or(d.toc_link_format),
|
||||||
|
table_reduce_last_col: r.table_reduce_last_col.unwrap_or(d.table_reduce_last_col),
|
||||||
html,
|
html,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the single-wiki [`WikiConfig`] from the flat top-level options object
|
||||||
|
/// (single-wiki shorthand: a `wiki_root` plus any per-wiki keys set alongside
|
||||||
|
/// it). Returns `None` when there's no expandable `wiki_root` (so callers fall
|
||||||
|
/// through to the workspace-folder default) or a `wikis` list is present.
|
||||||
|
///
|
||||||
|
/// Re-uses the full [`RawWiki`] schema so **every** per-wiki key set at the top
|
||||||
|
/// level — `toc_header`/`toc_header_level`, `links_header`, `html_path`,
|
||||||
|
/// `auto_export`, … — is honoured, not just `file_extension`/`syntax`. The
|
||||||
|
/// shorthand keys on `wiki_root`; `RawWiki` keys on `root`, so we inject it.
|
||||||
|
fn single_wiki_from_value(value: &serde_json::Value) -> Option<WikiConfig> {
|
||||||
|
let obj = value.as_object()?;
|
||||||
|
if obj.contains_key("wikis") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let wiki_root = obj.get("wiki_root").and_then(|v| v.as_str())?;
|
||||||
|
// Bail (→ workspace-folder fallback) when the root can't be expanded.
|
||||||
|
expand_path(wiki_root)?;
|
||||||
|
let mut wiki_obj = obj.clone();
|
||||||
|
wiki_obj.insert(
|
||||||
|
"root".to_string(),
|
||||||
|
serde_json::Value::String(wiki_root.to_string()),
|
||||||
|
);
|
||||||
|
let raw: RawWiki = serde_json::from_value(serde_json::Value::Object(wiki_obj)).ok()?;
|
||||||
|
Some(WikiConfig::from(raw))
|
||||||
|
}
|
||||||
|
|
||||||
fn expand_path(s: &str) -> Option<PathBuf> {
|
fn expand_path(s: &str) -> Option<PathBuf> {
|
||||||
let trimmed = s.trim();
|
let trimmed = s.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -633,6 +1096,3 @@ pub fn config_from_json(value: serde_json::Value) -> Config {
|
|||||||
cfg.apply_change(&value);
|
cfg.apply_change(&value);
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn _ensure_url_is_used(_u: &Url) {}
|
|
||||||
|
|||||||
@@ -270,40 +270,11 @@ pub fn classify_outgoing(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Flatten heading inlines down to a plain string. Used by `commands::ops`
|
/// Flatten heading inlines down to a plain string. Used by `commands::ops`
|
||||||
/// to match heading text against the configured TOC/Links section name.
|
/// to match heading text against the configured TOC/Links section name, and
|
||||||
|
/// (via `nuwiki_core::ast::inline_text`) the same text the HTML renderer uses
|
||||||
|
/// for heading `id`s — so anchors validated here match anchors emitted there.
|
||||||
pub fn heading_text(inlines: &[InlineNode]) -> String {
|
pub fn heading_text(inlines: &[InlineNode]) -> String {
|
||||||
let mut s = String::new();
|
nuwiki_core::ast::inline_text(inlines)
|
||||||
push_inline_text(inlines, &mut s);
|
|
||||||
s.trim().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_inline_text(inlines: &[InlineNode], out: &mut String) {
|
|
||||||
for n in inlines {
|
|
||||||
match n {
|
|
||||||
InlineNode::Text(t) => out.push_str(&t.content),
|
|
||||||
InlineNode::Bold(b) => push_inline_text(&b.children, out),
|
|
||||||
InlineNode::Italic(i) => push_inline_text(&i.children, out),
|
|
||||||
InlineNode::BoldItalic(bi) => push_inline_text(&bi.children, out),
|
|
||||||
InlineNode::Strikethrough(s) => push_inline_text(&s.children, out),
|
|
||||||
InlineNode::Code(c) => out.push_str(&c.content),
|
|
||||||
InlineNode::Superscript(s) => push_inline_text(&s.children, out),
|
|
||||||
InlineNode::Subscript(s) => push_inline_text(&s.children, out),
|
|
||||||
InlineNode::Color(c) => push_inline_text(&c.children, out),
|
|
||||||
InlineNode::WikiLink(w) => match &w.description {
|
|
||||||
Some(d) => push_inline_text(d, out),
|
|
||||||
None => {
|
|
||||||
if let Some(p) = &w.target.path {
|
|
||||||
out.push_str(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
InlineNode::ExternalLink(e) => match &e.description {
|
|
||||||
Some(d) => push_inline_text(d, out),
|
|
||||||
None => out.push_str(&e.url),
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a `file:` / `local:` link target to an absolute path.
|
/// Resolve a `file:` / `local:` link target to an absolute path.
|
||||||
|
|||||||
@@ -202,17 +202,27 @@ fn heading(level: u8, text: &str) -> String {
|
|||||||
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
||||||
///
|
///
|
||||||
/// `caption_level` sets the level of the top caption; the year and month
|
/// `caption_level` sets the level of the top caption; the year and month
|
||||||
/// subheadings nest one and two levels below it. `sort` controls whether
|
/// subheadings nest one and two levels below it. A `caption_level < 0`
|
||||||
/// the flat list runs newest- or oldest-first.
|
/// clamps to `0` (nuwiki builds the tree from dates, so upstream's `-1`
|
||||||
|
/// "no per-page captions" mode doesn't apply). `months` supplies the
|
||||||
|
/// month-number → display name table; an empty slice (or a short list)
|
||||||
|
/// falls back to the English `month_name` for any missing slot. `sort`
|
||||||
|
/// controls whether the flat list runs newest- or oldest-first. `margin`
|
||||||
|
/// is the number of leading spaces before each entry bullet (vimwiki's
|
||||||
|
/// `list_margin`).
|
||||||
pub fn build_index_body(
|
pub fn build_index_body(
|
||||||
entries: &[DiaryEntry],
|
entries: &[DiaryEntry],
|
||||||
diary_rel_path: &str,
|
diary_rel_path: &str,
|
||||||
index_heading: &str,
|
index_heading: &str,
|
||||||
sort: DiarySort,
|
sort: DiarySort,
|
||||||
caption_level: u8,
|
caption_level: i8,
|
||||||
|
months: &[String],
|
||||||
|
margin: usize,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
let base_level = caption_level.max(0) as u8;
|
||||||
|
let pad = " ".repeat(margin);
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(&heading(caption_level, index_heading));
|
out.push_str(&heading(base_level, index_heading));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
@@ -225,8 +235,8 @@ pub fn build_index_body(
|
|||||||
DiarySort::Asc => a.date.cmp(&b.date),
|
DiarySort::Asc => a.date.cmp(&b.date),
|
||||||
});
|
});
|
||||||
|
|
||||||
let year_level = caption_level.saturating_add(1);
|
let year_level = base_level.saturating_add(1);
|
||||||
let month_level = caption_level.saturating_add(2);
|
let month_level = base_level.saturating_add(2);
|
||||||
let mut current_year: Option<i32> = None;
|
let mut current_year: Option<i32> = None;
|
||||||
let mut current_month: Option<u8> = None;
|
let mut current_month: Option<u8> = None;
|
||||||
for e in sorted {
|
for e in sorted {
|
||||||
@@ -238,12 +248,17 @@ pub fn build_index_body(
|
|||||||
current_month = None;
|
current_month = None;
|
||||||
}
|
}
|
||||||
if current_month != Some(e.date.month) {
|
if current_month != Some(e.date.month) {
|
||||||
out.push_str(&heading(month_level, month_name(e.date.month)));
|
let label = months
|
||||||
|
.get((e.date.month.saturating_sub(1)) as usize)
|
||||||
|
.map(String::as_str)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| month_name(e.date.month));
|
||||||
|
out.push_str(&heading(month_level, label));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
current_month = Some(e.date.month);
|
current_month = Some(e.date.month);
|
||||||
}
|
}
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"- [[{}/{}]]\n",
|
"{pad}- [[{}/{}]]\n",
|
||||||
diary_rel_path.trim_end_matches('/'),
|
diary_rel_path.trim_end_matches('/'),
|
||||||
e.date.format()
|
e.date.format()
|
||||||
));
|
));
|
||||||
@@ -253,7 +268,8 @@ pub fn build_index_body(
|
|||||||
|
|
||||||
/// Render the diary-index body for the given wiki's currently-indexed
|
/// Render the diary-index body for the given wiki's currently-indexed
|
||||||
/// entries. Convenience wrapper around [`build_index_body`] that honours
|
/// entries. Convenience wrapper around [`build_index_body`] that honours
|
||||||
/// the wiki's `diary_header`, `diary_sort`, and `diary_caption_level`.
|
/// the wiki's `diary_header`, `diary_sort`, `diary_caption_level`, and
|
||||||
|
/// `diary_months`.
|
||||||
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
||||||
build_index_body(
|
build_index_body(
|
||||||
&list_entries(index),
|
&list_entries(index),
|
||||||
@@ -261,6 +277,8 @@ pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
|||||||
&cfg.diary_header,
|
&cfg.diary_header,
|
||||||
DiarySort::parse(&cfg.diary_sort),
|
DiarySort::parse(&cfg.diary_sort),
|
||||||
cfg.diary_caption_level,
|
cfg.diary_caption_level,
|
||||||
|
&cfg.diary_months,
|
||||||
|
cfg.list_margin.max(0) as usize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,14 +88,6 @@ impl WorkspaceEditBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edits<I>(&mut self, uri: Url, edits: I) -> &mut Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = TextEdit>,
|
|
||||||
{
|
|
||||||
self.changes.entry(uri).or_default().extend(edits);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file_op(&mut self, op: DocumentChangeOperation) -> &mut Self {
|
pub fn file_op(&mut self, op: DocumentChangeOperation) -> &mut Self {
|
||||||
self.ops.push(op);
|
self.ops.push(op);
|
||||||
self
|
self
|
||||||
|
|||||||
+236
-68
@@ -16,13 +16,13 @@
|
|||||||
//! above wired up; returns the final HTML string.
|
//! above wired up; returns the final HTML string.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use nuwiki_core::ast::{BlockNode, DocumentNode, HeadingNode, InlineNode, LinkKind};
|
use nuwiki_core::ast::{BlockNode, DocumentNode, LinkKind};
|
||||||
use nuwiki_core::date::DiaryDate;
|
use nuwiki_core::date::DiaryDate;
|
||||||
use nuwiki_core::render::{HtmlRenderer, Renderer};
|
use nuwiki_core::render::{HtmlRenderer, Renderer};
|
||||||
|
|
||||||
use crate::config::{HtmlConfig, WikiConfig};
|
use crate::config::HtmlConfig;
|
||||||
|
|
||||||
/// Map a page name (e.g. `"diary/2026-05-11"`) to its on-disk HTML
|
/// Map a page name (e.g. `"diary/2026-05-11"`) to its on-disk HTML
|
||||||
/// output path under `html_path`. Honours
|
/// output path under `html_path`. Honours
|
||||||
@@ -147,11 +147,10 @@ pub fn build_toc_html(doc: &DocumentNode) -> String {
|
|||||||
out.push_str("</ul>");
|
out.push_str("</ul>");
|
||||||
depth -= 1;
|
depth -= 1;
|
||||||
}
|
}
|
||||||
let anchor = crate::index::slugify(&h.title);
|
// Anchor is the raw heading text (vimwiki scheme), HTML-escaped — must
|
||||||
out.push_str(&format!(
|
// match the `id` the renderer puts on the heading element.
|
||||||
"<li><a href=\"#{anchor}\">{title}</a></li>",
|
let anchor = escape_html(&h.title);
|
||||||
title = escape_html(&h.title)
|
out.push_str(&format!("<li><a href=\"#{anchor}\">{anchor}</a></li>"));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
while depth > 0 {
|
while depth > 0 {
|
||||||
out.push_str("</ul>");
|
out.push_str("</ul>");
|
||||||
@@ -203,7 +202,6 @@ pub fn render_page_html(
|
|||||||
today: DiaryDate,
|
today: DiaryDate,
|
||||||
cfg: &HtmlConfig,
|
cfg: &HtmlConfig,
|
||||||
wiki_extension: Option<&str>,
|
wiki_extension: Option<&str>,
|
||||||
list_margin: i32,
|
|
||||||
extra_vars: HashMap<String, String>,
|
extra_vars: HashMap<String, String>,
|
||||||
) -> std::io::Result<String> {
|
) -> std::io::Result<String> {
|
||||||
let date_str = match doc.metadata.date.as_deref() {
|
let date_str = match doc.metadata.date.as_deref() {
|
||||||
@@ -228,18 +226,33 @@ pub fn render_page_html(
|
|||||||
vars.insert(k, v);
|
vars.insert(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut r = HtmlRenderer::new().with_list_margin(list_margin);
|
let mut r = HtmlRenderer::new();
|
||||||
if let Some(ext) = wiki_extension.filter(|e| !e.trim_start_matches('.').is_empty()) {
|
{
|
||||||
|
// Same-page anchor links (`[[#Section]]`) resolve to the *current*
|
||||||
|
// page's html file plus the fragment (`index.html#Section`), matching
|
||||||
|
// vimwiki — rather than a bare `#Section`.
|
||||||
|
let current_html = format!("{}.html", name.rsplit('/').next().unwrap_or(name));
|
||||||
// Strip the wiki extension from page links before the default resolver
|
// Strip the wiki extension from page links before the default resolver
|
||||||
// turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather
|
// turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather
|
||||||
// than `todo.wiki.html`. Only wiki/interwiki targets are touched;
|
// than `todo.wiki.html`. Only wiki/interwiki targets are touched;
|
||||||
// file:/local: paths keep their literal extension.
|
// file:/local: paths keep their literal extension.
|
||||||
let ext = ext.to_string();
|
let ext = wiki_extension
|
||||||
|
.filter(|e| !e.trim_start_matches('.').is_empty())
|
||||||
|
.map(|e| e.to_string());
|
||||||
r = r.with_link_resolver(move |target| {
|
r = r.with_link_resolver(move |target| {
|
||||||
|
if matches!(target.kind, LinkKind::AnchorOnly) {
|
||||||
|
return match &target.anchor {
|
||||||
|
Some(a) => format!("{current_html}#{a}"),
|
||||||
|
None => current_html.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
let mut t = target.clone();
|
let mut t = target.clone();
|
||||||
if matches!(t.kind, LinkKind::Wiki | LinkKind::Interwiki) {
|
if let Some(ext) = &ext {
|
||||||
if let Some(p) = t.path.take() {
|
if matches!(t.kind, LinkKind::Wiki | LinkKind::Interwiki) {
|
||||||
t.path = Some(crate::index::strip_wiki_extension(&p, Some(&ext)).to_string());
|
if let Some(p) = t.path.take() {
|
||||||
|
t.path =
|
||||||
|
Some(crate::index::strip_wiki_extension(&p, Some(ext)).to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nuwiki_core::render::html::default_link_resolver(&t)
|
nuwiki_core::render::html::default_link_resolver(&t)
|
||||||
@@ -252,6 +265,21 @@ pub fn render_page_html(
|
|||||||
if !cfg.color_dic.is_empty() {
|
if !cfg.color_dic.is_empty() {
|
||||||
r = r.with_colors(cfg.color_dic.clone());
|
r = r.with_colors(cfg.color_dic.clone());
|
||||||
}
|
}
|
||||||
|
if !cfg.color_tag_template.is_empty() {
|
||||||
|
r = r.with_color_template(cfg.color_tag_template.clone());
|
||||||
|
}
|
||||||
|
if cfg.html_header_numbering > 0 {
|
||||||
|
r = r.with_header_numbering(
|
||||||
|
cfg.html_header_numbering,
|
||||||
|
cfg.html_header_numbering_sym.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
r = r.with_toc_header(&cfg.toc_header);
|
||||||
|
if !cfg.valid_html_tags.is_empty() {
|
||||||
|
r = r.with_valid_html_tags(cfg.valid_html_tags.clone());
|
||||||
|
}
|
||||||
|
r = r.with_emoji(cfg.emoji_enable);
|
||||||
|
r = r.with_newline_handling(cfg.text_ignore_newline, cfg.list_ignore_newline);
|
||||||
r.render_to_string(doc)
|
r.render_to_string(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +315,7 @@ fn collect_headings(doc: &DocumentNode) -> Vec<HeadingProj> {
|
|||||||
if let BlockNode::Heading(h) = b {
|
if let BlockNode::Heading(h) = b {
|
||||||
out.push(HeadingProj {
|
out.push(HeadingProj {
|
||||||
level: h.level,
|
level: h.level,
|
||||||
title: inline_to_text(h),
|
title: nuwiki_core::ast::inline_text(&h.children),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,41 +327,6 @@ struct HeadingProj {
|
|||||||
title: String,
|
title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inline_to_text(h: &HeadingNode) -> String {
|
|
||||||
let mut s = String::new();
|
|
||||||
for n in &h.children {
|
|
||||||
push_inline(n, &mut s);
|
|
||||||
}
|
|
||||||
s.trim().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_inline(n: &InlineNode, out: &mut String) {
|
|
||||||
match n {
|
|
||||||
InlineNode::Text(t) => out.push_str(&t.content),
|
|
||||||
InlineNode::Bold(b) => b.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Italic(i) => i.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::BoldItalic(bi) => bi.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Strikethrough(s) => s.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Code(c) => out.push_str(&c.content),
|
|
||||||
InlineNode::Superscript(s) => s.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Subscript(s) => s.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::Color(c) => c.children.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
InlineNode::WikiLink(w) => match &w.description {
|
|
||||||
Some(d) => d.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
None => {
|
|
||||||
if let Some(p) = &w.target.path {
|
|
||||||
out.push_str(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
InlineNode::ExternalLink(e) => match &e.description {
|
|
||||||
Some(d) => d.iter().for_each(|c| push_inline(c, out)),
|
|
||||||
None => out.push_str(&e.url),
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escape_html(s: &str) -> String {
|
fn escape_html(s: &str) -> String {
|
||||||
let mut out = String::with_capacity(s.len());
|
let mut out = String::with_capacity(s.len());
|
||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
@@ -351,7 +344,7 @@ fn escape_html(s: &str) -> String {
|
|||||||
/// Minimal glob match for `exclude_files`. Handles `*`, `**`, `?` and
|
/// Minimal glob match for `exclude_files`. Handles `*`, `**`, `?` and
|
||||||
/// literal characters. No character classes — those aren't needed for
|
/// literal characters. No character classes — those aren't needed for
|
||||||
/// "exclude foo/*.tmp" style patterns.
|
/// "exclude foo/*.tmp" style patterns.
|
||||||
fn glob_match(pattern: &str, text: &str) -> bool {
|
pub(crate) fn glob_match(pattern: &str, text: &str) -> bool {
|
||||||
glob_impl(pattern.as_bytes(), text.as_bytes())
|
glob_impl(pattern.as_bytes(), text.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,20 +397,195 @@ pub fn css_path(cfg: &HtmlConfig) -> PathBuf {
|
|||||||
cfg.html_path.join(&cfg.css_name)
|
cfg.html_path.join(&cfg.css_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimal default CSS. Used by `nuwiki.export.*` commands when no CSS
|
/// Default CSS written by `nuwiki.export.*` commands when no CSS file exists
|
||||||
/// file exists yet at [`css_path`]. Deliberately tiny — users are
|
/// yet at [`css_path`]. This is vimwiki's stock `style.css` verbatim (MIT
|
||||||
/// expected to replace it with their own.
|
/// licensed) so exports look identical to upstream out of the box and the
|
||||||
pub const DEFAULT_CSS: &str =
|
/// `.header` / `.tag` / `.toc` / `done*` classes our HTML emits are styled.
|
||||||
"body{font-family:sans-serif;max-width:46em;margin:2em auto;padding:0 1em;line-height:1.55}\
|
pub const DEFAULT_CSS: &str = r#"body {
|
||||||
h1,h2,h3,h4,h5,h6{font-family:serif;line-height:1.2}\
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;;
|
||||||
pre{background:#f4f4f4;padding:.5em;overflow:auto}\
|
margin: 2em 4em 2em 4em;
|
||||||
code{background:#f4f4f4;padding:.1em .25em;border-radius:3px}\
|
font-size: 120%;
|
||||||
table{border-collapse:collapse}\
|
line-height: 130%;
|
||||||
table td,table th{border:1px solid #ccc;padding:.25em .5em}\
|
|
||||||
ul.toc{padding-left:1.5em}\n";
|
|
||||||
|
|
||||||
/// Convenience wrapper exposed for the dispatcher: convert a wiki config
|
|
||||||
/// to the absolute output dir.
|
|
||||||
pub fn html_output_dir(cfg: &WikiConfig) -> &Path {
|
|
||||||
&cfg.html.html_path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
line-height:100%;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {font-size: 2em; color: #000000;}
|
||||||
|
h2 {font-size: 1.8em; color: #404040;}
|
||||||
|
h3 {font-size: 1.6em; color: #707070;}
|
||||||
|
h4 {font-size: 1.4em; color: #909090;}
|
||||||
|
h5 {font-size: 1.2em; color: #989898;}
|
||||||
|
h6 {font-size: 1em; color: #9c9c9c;}
|
||||||
|
|
||||||
|
p, pre, blockquote, table, ul, ol, dl {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul ul, ul ol, ol ol, ol ul {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li { margin: 0.3em auto; }
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-left: 2em;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt { font-weight: bold; }
|
||||||
|
|
||||||
|
img { border: none; }
|
||||||
|
|
||||||
|
pre {
|
||||||
|
border-left: 5px solid #dcdcdc;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-left: 1em;
|
||||||
|
font-family: Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p > a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.7em;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #1e90ff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p > a:hover {
|
||||||
|
color: #dcdcdc;
|
||||||
|
background-color: #484848;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > a {
|
||||||
|
color: #1e90ff;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > a:hover { color: #ff4500; }
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
color: #686868;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 120%;
|
||||||
|
padding: 0.8em;
|
||||||
|
border-left: 5px solid #dcdcdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th { background-color: #f0f0f0; }
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc li { list-style-type: none; }
|
||||||
|
|
||||||
|
.todo {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #ff4500 ;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justleft { text-align: left; }
|
||||||
|
.justright { text-align: right; }
|
||||||
|
.justcenter { text-align: center; }
|
||||||
|
|
||||||
|
.center {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* classes for items of todo lists */
|
||||||
|
|
||||||
|
.rejected {
|
||||||
|
/* list-style: none; */
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAACXBIWXMAAADFAAAAxQEdzbqoAAAAB3RJTUUH4QgEFhAtuWgv9wAAAPZQTFRFmpqam5iYnJaWnJeXnpSUn5OTopCQpoqKpouLp4iIqIiIrYCAt3V1vW1tv2xsmZmZmpeXnpKS/x4e/x8f/yAg/yIi/yQk/yUl/yYm/ygo/ykp/yws/zAw/zIy/zMz/zQ0/zU1/zY2/zw8/0BA/0ZG/0pK/1FR/1JS/1NT/1RU/1VV/1ZW/1dX/1pa/15e/19f/2Zm/2lp/21t/25u/3R0/3p6/4CA/4GB/4SE/4iI/46O/4+P/52d/6am/6ur/66u/7Oz/7S0/7e3/87O/9fX/9zc/93d/+Dg/+vr/+3t/+/v//Dw//Ly//X1//f3//n5//z8////gzaKowAAAA90Uk5T/Pz8/Pz8/Pz8/Pz8/f39ppQKWQAAAAFiS0dEEnu8bAAAAACuSURBVAhbPY9ZF4FQFEZPSKbIMmWep4gMGTKLkIv6/3/GPbfF97b3w17rA0kQOPgvAeHW6uJ6+5h7HqLdwowgOzejXRXBdx6UdSquml4xuOMBHHNU0clTzeSUA6EhF8V8kqroluMiU6HKcuf4phGPr1o2q9kYZWwNq1qfRRmTaXpqsyjj17KkWCxKBUBgXWueHIyiAIg18gsse4KHkLF5IKIY10WQgv7fOy4ST34BRiopZ8WLNrgAAAAASUVORK5CYII=);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 .2em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
.done0 {
|
||||||
|
/* list-style: none; */
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAA7SURBVCiR7dMxEgAgCANBI3yVRzF5KxNbW6wsuH7LQ2YKQK1mkswBVERYF5Os3UV3gwd/jF2SkXy66gAZkxS6BniubAAAAABJRU5ErkJggg==);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 .2em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
.done1 {
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABtSURBVCiR1ZO7DYAwDER9BDmTeZQMFXmUbGYpOjrEryA0wOvO8itOslFrJYAug5BMM4BeSkmjsrv3aVTa8p48Xw1JSkSsWVUFwD05IqS1tmYzk5zzae9jnVVVzGyXb8sALjse+euRkEzu/uirFomVIdDGOLjuAAAAAElFTkSuQmCC);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 .15em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
.done2 {
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAB1SURBVCiRzdO5DcAgDAVQGxjAYgTvxlDIu1FTIRYAp8qlFISkSH7l5kk+ZIwxKiI2mIyqWoeILYRgZ7GINDOLjnmF3VqklKCUMgTee2DmM661Qs55iI3Zm/1u5h9sm4ig9z4ERHTFzLyd4G4+nFlVrYg8+qoF/c0kdpeMsmcAAAAASUVORK5CYII=);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 .15em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
.done3 {
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAxQAAAMUBHc26qAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABoSURBVCiR7dOxDcAgDATA/0DtUdiKoZC3YhLkHjkVKF3idJHiztKfvrHZWnOSE8Fx95RJzlprimJVnXktvXeY2S0SEZRSAAAbmxnGGKH2I5T+8VfxPhIReQSuuY3XyYWa3T2p6quvOgGrvSFGlewuUAAAAABJRU5ErkJggg==);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 .15em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
.done4 {
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAzgAAAM4BlP6ToAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIISURBVDiNnZQ9SFtRFMd/773kpTaGJoQk1im4VDpWQcTNODhkFBcVTCNCF0NWyeDiIIiCm82QoIMIUkHUxcFBg1SEQoZszSat6cdTn1qNue92CMbEr9Of5+vd8/3nPux/3O+f8h6ukUil3sVg0+M+4cFxk42/jH2wAqqqKSCSiPQdwcHHAnDHH9s/tN1h8V28ETdP+eU8fT9Nt62ancYdIPvJNtsu87bmjrJlrTDVM4RROJs1JrHPrD4Bar7A6cpc54iKOaTdJXCUI2UMVrQZ0Js7YPN18ECKkYNQcJe/OE/4dZsw7VqNXQMvHy3QZXQypQ6ycrtwDjf8aJ+PNEDSCzLpn7+m2pD8ZKHlKarYhy6XjEoCYGcN95qansQeA3fNdki+SaJZGTMQIOoL3W/Z89rxv+tokubNajlvk/vm+LFpF2XnUKZHI0I+QrI7Dw0OZTqdzUkpsM7mZTyfy5OPGyw1tK7AFSvmB/Ks8w8YwbUYbe6/3QEKv0vugfxWPnMLJun+d/kI/WLdizpNjMbAIKrhMF4OuwadBALqqs+RfInwUvuNi+fBd+wjogfogAFVRmffO02q01mZZ0HHdgXIzdz0QQLPezIQygX6llxNKKgOFARYCC49CqhoHIUTlss/Vx2phlYwjw8j1CAlfAiwQiJpiy7o1VHnsG5FISkoJu7Q/2YmmaV+i0ei7v38L2CBguSi5AAAAAElFTkSuQmCC);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 .15em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace;
|
||||||
|
-webkit-border-radius: 1px;
|
||||||
|
-moz-border-radius: 1px;
|
||||||
|
border-radius: 1px;
|
||||||
|
-moz-background-clip: padding;
|
||||||
|
-webkit-background-clip: padding-box;
|
||||||
|
background-clip: padding-box;
|
||||||
|
padding: 0px 3px;
|
||||||
|
display: inline-block;
|
||||||
|
color: #52595d;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ pub struct WorkspaceIndex {
|
|||||||
/// the nav handlers (tag-as-anchor `definition`, `workspace/symbol`
|
/// the nav handlers (tag-as-anchor `definition`, `workspace/symbol`
|
||||||
/// inclusion) and the `nuwiki.tags.search` command.
|
/// inclusion) and the `nuwiki.tags.search` command.
|
||||||
pub tags_by_name: HashMap<String, Vec<TagOccurrence>>,
|
pub tags_by_name: HashMap<String, Vec<TagOccurrence>>,
|
||||||
|
/// For each source URI, the backlink-bucket keys and tag-bucket names it
|
||||||
|
/// contributed. Lets `remove` touch only the affected buckets instead of
|
||||||
|
/// scanning every bucket in the workspace (O(touched), not O(total)).
|
||||||
|
contributions: HashMap<Url, SourceContributions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bucket keys a single source page wrote into, so they can be undone
|
||||||
|
/// in `remove` without a full scan. See [`WorkspaceIndex::contributions`].
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct SourceContributions {
|
||||||
|
backlink_keys: Vec<String>,
|
||||||
|
tag_names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceIndex {
|
impl WorkspaceIndex {
|
||||||
@@ -175,15 +187,20 @@ impl WorkspaceIndex {
|
|||||||
};
|
};
|
||||||
index_blocks(&ast.children, &mut page);
|
index_blocks(&ast.children, &mut page);
|
||||||
|
|
||||||
|
let mut contrib = SourceContributions::default();
|
||||||
let ext = self.file_extension.as_deref();
|
let ext = self.file_extension.as_deref();
|
||||||
for link in &page.outgoing {
|
for link in &page.outgoing {
|
||||||
if let Some(tgt) = &link.target_page {
|
if let Some(tgt) = &link.target_page {
|
||||||
let key = canonical_link_name(tgt, &name, link.is_absolute, ext);
|
let key = canonical_link_name(tgt, &name, link.is_absolute, ext);
|
||||||
self.backlinks.entry(key).or_default().push(Backlink {
|
self.backlinks
|
||||||
source_uri: uri.clone(),
|
.entry(key.clone())
|
||||||
source_span: link.span,
|
.or_default()
|
||||||
target_anchor: link.anchor.clone(),
|
.push(Backlink {
|
||||||
});
|
source_uri: uri.clone(),
|
||||||
|
source_span: link.span,
|
||||||
|
target_anchor: link.anchor.clone(),
|
||||||
|
});
|
||||||
|
contrib.backlink_keys.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +214,9 @@ impl WorkspaceIndex {
|
|||||||
span: tag.span,
|
span: tag.span,
|
||||||
scope: tag.scope,
|
scope: tag.scope,
|
||||||
});
|
});
|
||||||
|
contrib.tag_names.push(tag.name.clone());
|
||||||
}
|
}
|
||||||
|
self.contributions.insert(uri.clone(), contrib);
|
||||||
|
|
||||||
self.pages_by_name.insert(name, uri.clone());
|
self.pages_by_name.insert(name, uri.clone());
|
||||||
self.pages_by_uri.insert(uri, page);
|
self.pages_by_uri.insert(uri, page);
|
||||||
@@ -207,14 +226,26 @@ impl WorkspaceIndex {
|
|||||||
if let Some(page) = self.pages_by_uri.remove(uri) {
|
if let Some(page) = self.pages_by_uri.remove(uri) {
|
||||||
self.pages_by_name.remove(&page.name);
|
self.pages_by_name.remove(&page.name);
|
||||||
}
|
}
|
||||||
for entries in self.backlinks.values_mut() {
|
// Only revisit the buckets this source actually wrote into, instead
|
||||||
entries.retain(|b| &b.source_uri != uri);
|
// of scanning every backlink/tag bucket in the workspace.
|
||||||
|
if let Some(contrib) = self.contributions.remove(uri) {
|
||||||
|
for key in contrib.backlink_keys {
|
||||||
|
if let Some(entries) = self.backlinks.get_mut(&key) {
|
||||||
|
entries.retain(|b| &b.source_uri != uri);
|
||||||
|
if entries.is_empty() {
|
||||||
|
self.backlinks.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name in contrib.tag_names {
|
||||||
|
if let Some(entries) = self.tags_by_name.get_mut(&name) {
|
||||||
|
entries.retain(|t| &t.uri != uri);
|
||||||
|
if entries.is_empty() {
|
||||||
|
self.tags_by_name.remove(&name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.backlinks.retain(|_, v| !v.is_empty());
|
|
||||||
for entries in self.tags_by_name.values_mut() {
|
|
||||||
entries.retain(|t| &t.uri != uri);
|
|
||||||
}
|
|
||||||
self.tags_by_name.retain(|_, v| !v.is_empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All occurrences of a tag across the workspace. Empty slice when the
|
/// All occurrences of a tag across the workspace. Empty slice when the
|
||||||
@@ -306,21 +337,6 @@ impl WorkspaceIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a parsed [`DiaryDate`] when `uri` is a daily diary entry —
|
|
||||||
/// the path lives under `<root>/<diary_rel_path>/` and its stem parses
|
|
||||||
/// as `YYYY-MM-DD`. For weekly/monthly/yearly entries see
|
|
||||||
/// [`diary_period_for_uri`].
|
|
||||||
pub fn diary_date_for_uri(
|
|
||||||
uri: &Url,
|
|
||||||
root: Option<&Path>,
|
|
||||||
diary_rel_path: Option<&str>,
|
|
||||||
) -> Option<DiaryDate> {
|
|
||||||
match diary_period_for_uri(uri, root, diary_rel_path)? {
|
|
||||||
nuwiki_core::date::DiaryPeriod::Day(d) => Some(d),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a parsed [`DiaryPeriod`] when `uri` is a diary entry at any of
|
/// Return a parsed [`DiaryPeriod`] when `uri` is a diary entry at any of
|
||||||
/// the four supported frequencies. Recognises:
|
/// the four supported frequencies. Recognises:
|
||||||
///
|
///
|
||||||
@@ -329,9 +345,8 @@ pub fn diary_date_for_uri(
|
|||||||
/// `YYYY-MM` → Month
|
/// `YYYY-MM` → Month
|
||||||
/// `YYYY` → Year
|
/// `YYYY` → Year
|
||||||
///
|
///
|
||||||
/// As with `diary_date_for_uri`, the path must sit under
|
/// The path must sit under `<root>/<diary_rel_path>/` to be accepted;
|
||||||
/// `<root>/<diary_rel_path>/` to be accepted; without a `root` we accept
|
/// without a `root` we accept any stem (so ad-hoc test setups still work).
|
||||||
/// any stem (so ad-hoc test setups still work).
|
|
||||||
pub fn diary_period_for_uri(
|
pub fn diary_period_for_uri(
|
||||||
uri: &Url,
|
uri: &Url,
|
||||||
root: Option<&Path>,
|
root: Option<&Path>,
|
||||||
@@ -533,6 +548,7 @@ fn inline_to_text_into(inlines: &[InlineNode], out: &mut String) {
|
|||||||
InlineNode::Keyword(_) => {}
|
InlineNode::Keyword(_) => {}
|
||||||
InlineNode::Transclusion(t) => out.push_str(t.alt.as_deref().unwrap_or(&t.url)),
|
InlineNode::Transclusion(t) => out.push_str(t.alt.as_deref().unwrap_or(&t.url)),
|
||||||
InlineNode::RawUrl(r) => out.push_str(&r.url),
|
InlineNode::RawUrl(r) => out.push_str(&r.url),
|
||||||
|
InlineNode::SoftBreak(_) => out.push(' '),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,9 +110,8 @@ pub async fn run_stdio() -> io::Result<()> {
|
|||||||
struct DocumentState {
|
struct DocumentState {
|
||||||
text: String,
|
text: String,
|
||||||
ast: DocumentNode,
|
ast: DocumentNode,
|
||||||
/// Last version we observed from the client. Held even
|
/// Last version we observed from the client. Echoed back on the
|
||||||
/// though nothing reads it back yet.
|
/// diagnostics we publish for this document.
|
||||||
#[allow(dead_code)]
|
|
||||||
version: i32,
|
version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +192,7 @@ impl Backend {
|
|||||||
fn parse_for_uri(&self, uri: &Url, text: &str) -> nuwiki_core::ast::DocumentNode {
|
fn parse_for_uri(&self, uri: &Url, text: &str) -> nuwiki_core::ast::DocumentNode {
|
||||||
let syms = self
|
let syms = self
|
||||||
.wiki_for_uri(uri)
|
.wiki_for_uri(uri)
|
||||||
.map(|w| nuwiki_core::listsyms::ListSyms::new(&w.config.listsyms))
|
.map(|w| w.config.list_syms())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
nuwiki_core::syntax::vimwiki::VimwikiSyntax::new().parse_with_listsyms(text, &syms)
|
nuwiki_core::syntax::vimwiki::VimwikiSyntax::new().parse_with_listsyms(text, &syms)
|
||||||
}
|
}
|
||||||
@@ -269,7 +268,29 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let path = target.path.as_deref()?;
|
let path = target.path.as_deref()?;
|
||||||
return synthesise_page_uri(&source_wiki.config, path);
|
// vimwiki `dir_link`: a `[[dir/]]` link opens `<dir>/<dir_link>`
|
||||||
|
// (e.g. `index` → `dir/index.wiki`); empty → the directory itself.
|
||||||
|
if target.is_directory {
|
||||||
|
let dl = source_wiki.config.dir_link.trim();
|
||||||
|
if dl.is_empty() {
|
||||||
|
let mut p = source_wiki.config.root.clone();
|
||||||
|
for seg in path.split('/').filter(|s| !s.is_empty()) {
|
||||||
|
p.push(seg);
|
||||||
|
}
|
||||||
|
return Url::from_file_path(p).ok();
|
||||||
|
}
|
||||||
|
let joined = format!("{}/{dl}", path.trim_end_matches('/'));
|
||||||
|
return synthesise_page_uri(&source_wiki.config, &joined);
|
||||||
|
}
|
||||||
|
// vimwiki `create_link`: when off, only follow to a page that
|
||||||
|
// already exists on disk — never synthesise a new one.
|
||||||
|
let uri = synthesise_page_uri(&source_wiki.config, path)?;
|
||||||
|
if !source_wiki.config.create_link
|
||||||
|
&& !uri.to_file_path().map(|p| p.exists()).unwrap_or(false)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(uri);
|
||||||
}
|
}
|
||||||
// No wiki registered (no `initializationOptions`, no
|
// No wiki registered (no `initializationOptions`, no
|
||||||
// workspace folder). Fall back to the source buffer's
|
// workspace folder). Fall back to the source buffer's
|
||||||
@@ -625,6 +646,10 @@ impl LanguageServer for Backend {
|
|||||||
&doc.ast,
|
&doc.ast,
|
||||||
&uri,
|
&uri,
|
||||||
self.use_utf8.load(Ordering::Relaxed),
|
self.use_utf8.load(Ordering::Relaxed),
|
||||||
|
&wiki.config.toc_header,
|
||||||
|
wiki.config.toc_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
|
wiki.config.toc_link_format,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -633,6 +658,93 @@ impl LanguageServer for Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let utf8 = self.use_utf8.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
// `auto_generate_links`: rebuild an existing `Generated Links` section.
|
||||||
|
if wiki.config.auto_generate_links {
|
||||||
|
let edit = self.documents.get(&uri).and_then(|doc| {
|
||||||
|
let current = index::page_name_from_uri(&uri, Some(&wiki.config.root));
|
||||||
|
let (pages, captions) = wiki
|
||||||
|
.index
|
||||||
|
.read()
|
||||||
|
.ok()
|
||||||
|
.map(|i| {
|
||||||
|
let caps = wiki
|
||||||
|
.config
|
||||||
|
.generated_links_caption
|
||||||
|
.then(|| commands::ops::page_captions(&i));
|
||||||
|
(i.page_names(), caps)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
commands::ops::links_rebuild_edit(
|
||||||
|
&doc.text,
|
||||||
|
&doc.ast,
|
||||||
|
&uri,
|
||||||
|
¤t,
|
||||||
|
&pages,
|
||||||
|
utf8,
|
||||||
|
&wiki.config.links_header,
|
||||||
|
wiki.config.links_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
|
captions.as_ref(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if let Some(edit) = edit {
|
||||||
|
let _ = self.client.apply_edit(edit).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `auto_generate_tags`: rebuild an existing `Generated Tags` index.
|
||||||
|
if wiki.config.auto_generate_tags {
|
||||||
|
let edit = self.documents.get(&uri).and_then(|doc| {
|
||||||
|
let snap = wiki
|
||||||
|
.index
|
||||||
|
.read()
|
||||||
|
.ok()
|
||||||
|
.map(|i| commands::ops::tag_pages_snapshot(&i))
|
||||||
|
.unwrap_or_default();
|
||||||
|
commands::ops::tag_links_rebuild_edit(
|
||||||
|
&doc.text,
|
||||||
|
&doc.ast,
|
||||||
|
&uri,
|
||||||
|
&snap,
|
||||||
|
utf8,
|
||||||
|
&wiki.config.tags_header,
|
||||||
|
wiki.config.tags_header_level,
|
||||||
|
wiki.config.list_margin.max(0) as usize,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if let Some(edit) = edit {
|
||||||
|
let _ = self.client.apply_edit(edit).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `auto_diary_index`: when a diary *entry* (a dated page, not the
|
||||||
|
// index itself) is saved, regenerate the diary index page.
|
||||||
|
if wiki.config.auto_diary_index {
|
||||||
|
if let Some(index_uri) = crate::diary::index_uri(&wiki.config) {
|
||||||
|
let is_diary_entry = uri != index_uri
|
||||||
|
&& wiki
|
||||||
|
.index
|
||||||
|
.read()
|
||||||
|
.ok()
|
||||||
|
.and_then(|i| i.page(&uri).and_then(|p| p.diary_period))
|
||||||
|
.is_some();
|
||||||
|
if is_diary_entry {
|
||||||
|
let body = wiki
|
||||||
|
.index
|
||||||
|
.read()
|
||||||
|
.ok()
|
||||||
|
.map(|i| crate::diary::render_index_body(&wiki.config, &i));
|
||||||
|
if let Some(body) = body {
|
||||||
|
let edit =
|
||||||
|
commands::ops::diary_generate_index_edit(self, &index_uri, &body, utf8);
|
||||||
|
let _ = self.client.apply_edit(edit).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When `auto_export` is set on the resolved wiki, run
|
// When `auto_export` is set on the resolved wiki, run
|
||||||
// the equivalent of `nuwiki.export.currentToHtml` after the file
|
// the equivalent of `nuwiki.export.currentToHtml` after the file
|
||||||
// hits disk. Best-effort — failures are logged but do not bubble
|
// hits disk. Best-effort — failures are logged but do not bubble
|
||||||
@@ -648,7 +760,7 @@ impl LanguageServer for Backend {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let name = index::page_name_from_uri(&uri, Some(&wiki.config.root));
|
let name = index::page_name_from_uri(&uri, Some(&wiki.config.root));
|
||||||
let outcome = commands::export_ops::write_page(&doc.ast, &name, &wiki.config);
|
let outcome = commands::export_ops::write_page(&doc.ast, &name, &wiki.config, false);
|
||||||
drop(doc);
|
drop(doc);
|
||||||
match outcome {
|
match outcome {
|
||||||
Ok(out) => {
|
Ok(out) => {
|
||||||
@@ -1351,6 +1463,7 @@ fn inline_to_text_into(inlines: &[InlineNode], out: &mut String) {
|
|||||||
},
|
},
|
||||||
InlineNode::Transclusion(t) => out.push_str(t.alt.as_deref().unwrap_or(&t.url)),
|
InlineNode::Transclusion(t) => out.push_str(t.alt.as_deref().unwrap_or(&t.url)),
|
||||||
InlineNode::RawUrl(r) => out.push_str(&r.url),
|
InlineNode::RawUrl(r) => out.push_str(&r.url),
|
||||||
|
InlineNode::SoftBreak(_) => out.push(' '),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fn find_in_list_item(item: &ListItemNode, line: u32, col: u32) -> Option<&Inline
|
|||||||
|
|
||||||
fn find_in_inlines(inlines: &[InlineNode], line: u32, col: u32) -> Option<&InlineNode> {
|
fn find_in_inlines(inlines: &[InlineNode], line: u32, col: u32) -> Option<&InlineNode> {
|
||||||
for n in inlines {
|
for n in inlines {
|
||||||
if !span_contains(span_of_inline(n), line, col) {
|
if !span_contains(n.span(), line, col) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Descend into inline containers so we return the *innermost* hit.
|
// Descend into inline containers so we return the *innermost* hit.
|
||||||
@@ -106,14 +106,11 @@ fn find_in_inlines(inlines: &[InlineNode], line: u32, col: u32) -> Option<&Inlin
|
|||||||
InlineNode::Superscript(s) => find_in_inlines(&s.children, line, col),
|
InlineNode::Superscript(s) => find_in_inlines(&s.children, line, col),
|
||||||
InlineNode::Subscript(s) => find_in_inlines(&s.children, line, col),
|
InlineNode::Subscript(s) => find_in_inlines(&s.children, line, col),
|
||||||
InlineNode::Color(c) => find_in_inlines(&c.children, line, col),
|
InlineNode::Color(c) => find_in_inlines(&c.children, line, col),
|
||||||
InlineNode::WikiLink(w) => w
|
// Links are an atomic unit for navigation: a cursor anywhere in
|
||||||
.description
|
// `[[target|description]]` (or `[desc](url)`) should resolve the
|
||||||
.as_ref()
|
// link itself. Don't descend into the description — returning its
|
||||||
.and_then(|d| find_in_inlines(d, line, col)),
|
// inner Text node leaves goto-definition/references/hover with
|
||||||
InlineNode::ExternalLink(e) => e
|
// nothing to act on ("No locations found" when on the title).
|
||||||
.description
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|d| find_in_inlines(d, line, col)),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
return Some(deeper.unwrap_or(n));
|
return Some(deeper.unwrap_or(n));
|
||||||
@@ -127,23 +124,3 @@ fn span_contains(s: Span, line: u32, col: u32) -> bool {
|
|||||||
let p = (line, col);
|
let p = (line, col);
|
||||||
p >= start && p < end
|
p >= start && p < end
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn span_of_inline(node: &InlineNode) -> Span {
|
|
||||||
match node {
|
|
||||||
InlineNode::Text(n) => n.span,
|
|
||||||
InlineNode::Bold(n) => n.span,
|
|
||||||
InlineNode::Italic(n) => n.span,
|
|
||||||
InlineNode::BoldItalic(n) => n.span,
|
|
||||||
InlineNode::Strikethrough(n) => n.span,
|
|
||||||
InlineNode::Code(n) => n.span,
|
|
||||||
InlineNode::Superscript(n) => n.span,
|
|
||||||
InlineNode::Subscript(n) => n.span,
|
|
||||||
InlineNode::MathInline(n) => n.span,
|
|
||||||
InlineNode::Keyword(n) => n.span,
|
|
||||||
InlineNode::Color(n) => n.span,
|
|
||||||
InlineNode::WikiLink(n) => n.span,
|
|
||||||
InlineNode::ExternalLink(n) => n.span,
|
|
||||||
InlineNode::Transclusion(n) => n.span,
|
|
||||||
InlineNode::RawUrl(n) => n.span,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ fn emit_block(block: &BlockNode, text: &str, idx: &LineIndex, out: &mut Vec<RawT
|
|||||||
for item in &dl.items {
|
for item in &dl.items {
|
||||||
if let Some(term) = &item.term {
|
if let Some(term) = &item.term {
|
||||||
if let (Some(first), Some(last)) = (term.first(), term.last()) {
|
if let (Some(first), Some(last)) = (term.first(), term.last()) {
|
||||||
let s = Span::new(span_of_inline(first).start, span_of_inline(last).end);
|
let s = Span::new(first.span().start, last.span().end);
|
||||||
split_span(s, text, idx, TOKEN_DEFINITION_TERM, 0, out);
|
split_span(s, text, idx, TOKEN_DEFINITION_TERM, 0, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ fn emit_list_item(item: &ListItemNode, text: &str, idx: &LineIndex, out: &mut Ve
|
|||||||
fn emit_inlines(inlines: &[InlineNode], text: &str, idx: &LineIndex, out: &mut Vec<RawToken>) {
|
fn emit_inlines(inlines: &[InlineNode], text: &str, idx: &LineIndex, out: &mut Vec<RawToken>) {
|
||||||
for n in inlines {
|
for n in inlines {
|
||||||
let (kind, span) = match n {
|
let (kind, span) = match n {
|
||||||
InlineNode::Text(_) => continue,
|
InlineNode::Text(_) | InlineNode::SoftBreak(_) => continue,
|
||||||
InlineNode::Bold(b) => (TOKEN_BOLD, b.span),
|
InlineNode::Bold(b) => (TOKEN_BOLD, b.span),
|
||||||
InlineNode::Italic(i) => (TOKEN_ITALIC, i.span),
|
InlineNode::Italic(i) => (TOKEN_ITALIC, i.span),
|
||||||
InlineNode::BoldItalic(bi) => (TOKEN_BOLD_ITALIC, bi.span),
|
InlineNode::BoldItalic(bi) => (TOKEN_BOLD_ITALIC, bi.span),
|
||||||
@@ -251,26 +251,6 @@ fn emit_inlines(inlines: &[InlineNode], text: &str, idx: &LineIndex, out: &mut V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn span_of_inline(node: &InlineNode) -> Span {
|
|
||||||
match node {
|
|
||||||
InlineNode::Text(n) => n.span,
|
|
||||||
InlineNode::Bold(n) => n.span,
|
|
||||||
InlineNode::Italic(n) => n.span,
|
|
||||||
InlineNode::BoldItalic(n) => n.span,
|
|
||||||
InlineNode::Strikethrough(n) => n.span,
|
|
||||||
InlineNode::Code(n) => n.span,
|
|
||||||
InlineNode::Superscript(n) => n.span,
|
|
||||||
InlineNode::Subscript(n) => n.span,
|
|
||||||
InlineNode::MathInline(n) => n.span,
|
|
||||||
InlineNode::Keyword(n) => n.span,
|
|
||||||
InlineNode::Color(n) => n.span,
|
|
||||||
InlineNode::WikiLink(n) => n.span,
|
|
||||||
InlineNode::ExternalLink(n) => n.span,
|
|
||||||
InlineNode::Transclusion(n) => n.span,
|
|
||||||
InlineNode::RawUrl(n) => n.span,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a `Span` (potentially multi-line) into one or more single-line
|
/// Convert a `Span` (potentially multi-line) into one or more single-line
|
||||||
/// `RawToken`s. The LSP wire format can't express tokens that span newlines,
|
/// `RawToken`s. The LSP wire format can't express tokens that span newlines,
|
||||||
/// so we split on line boundaries.
|
/// so we split on line boundaries.
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ pub struct WikiId(pub u32);
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Wiki {
|
pub struct Wiki {
|
||||||
pub id: WikiId,
|
pub id: WikiId,
|
||||||
pub config: WikiConfig,
|
// `Arc` so cloning a `Wiki` (done on every handler entry that picks a
|
||||||
|
// wiki out of the list) is a refcount bump, not a deep copy of the whole
|
||||||
|
// config — color_dic, templates, header strings, and so on.
|
||||||
|
pub config: Arc<WikiConfig>,
|
||||||
pub index: Arc<RwLock<WorkspaceIndex>>,
|
pub index: Arc<RwLock<WorkspaceIndex>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +32,11 @@ impl Wiki {
|
|||||||
.with_diary_rel_path(Some(config.diary_rel_path.clone()))
|
.with_diary_rel_path(Some(config.diary_rel_path.clone()))
|
||||||
.with_file_extension(Some(config.file_extension.clone()));
|
.with_file_extension(Some(config.file_extension.clone()));
|
||||||
let index = Arc::new(RwLock::new(index));
|
let index = Arc::new(RwLock::new(index));
|
||||||
Self { id, config, index }
|
Self {
|
||||||
|
id,
|
||||||
|
config: Arc::new(config),
|
||||||
|
index,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when `uri` resolves to a file under this wiki's root.
|
/// True when `uri` resolves to a file under this wiki's root.
|
||||||
|
|||||||
@@ -262,7 +262,15 @@ fn nuwiki_diary_index_resolves_index_uri() {
|
|||||||
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
||||||
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 1);
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
1,
|
||||||
|
&[],
|
||||||
|
0,
|
||||||
|
);
|
||||||
assert!(body.starts_with("= Diary ="));
|
assert!(body.starts_with("= Diary ="));
|
||||||
assert!(body.contains("2026-05-12"));
|
assert!(body.contains("2026-05-12"));
|
||||||
assert!(body.contains("2026-05-10"));
|
assert!(body.contains("2026-05-10"));
|
||||||
@@ -298,7 +306,8 @@ fn nuwiki_toc_generates_nested_contents() {
|
|||||||
let src = "= Top =\n== Sub ==\ntext\n";
|
let src = "= Top =\n== Sub ==\ntext\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
||||||
let edit = ops::toc_edit(src, &doc, &uri, true).expect("toc edit produced");
|
let edit =
|
||||||
|
ops::toc_edit(src, &doc, &uri, true, "Contents", 1, 0, 0, None).expect("toc edit produced");
|
||||||
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
||||||
|
|
||||||
let toc = ops::build_toc_text(
|
let toc = ops::build_toc_text(
|
||||||
@@ -307,6 +316,9 @@ fn nuwiki_toc_generates_nested_contents() {
|
|||||||
(2, "Sub".into(), "sub".into()),
|
(2, "Sub".into(), "sub".into()),
|
||||||
],
|
],
|
||||||
"Contents",
|
"Contents",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
assert!(toc.contains("= Contents ="));
|
assert!(toc.contains("= Contents ="));
|
||||||
assert!(toc.contains("- [[#top|Top]]"));
|
assert!(toc.contains("- [[#top|Top]]"));
|
||||||
@@ -317,7 +329,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||||
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
||||||
let text = ops::build_links_text(&pages, "Links", Some("Home"));
|
let text = ops::build_links_text(&pages, "Links", 1, Some("Home"), 0, None);
|
||||||
assert!(text.contains("= Links ="));
|
assert!(text.contains("= Links ="));
|
||||||
assert!(text.contains("- [[About]]"));
|
assert!(text.contains("- [[About]]"));
|
||||||
assert!(text.contains("- [[Notes]]"));
|
assert!(text.contains("- [[Notes]]"));
|
||||||
@@ -326,7 +338,20 @@ fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
|||||||
let src = "= Existing =\n";
|
let src = "= Existing =\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||||
assert!(ops::links_edit(src, &doc, &uri, "Home", &pages, true).is_some());
|
assert!(ops::links_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
// :NuwikiCheckLinks — surface broken links across the workspace.
|
// :NuwikiCheckLinks — surface broken links across the workspace.
|
||||||
@@ -383,21 +408,33 @@ fn nuwiki_generate_tag_links_builds_section() {
|
|||||||
);
|
);
|
||||||
let by_tag = ops::tag_pages_snapshot(&idx);
|
let by_tag = ops::tag_pages_snapshot(&idx);
|
||||||
|
|
||||||
let single = ops::build_tag_links_text(&by_tag, Some("alpha")).unwrap();
|
let single = ops::build_tag_links_text(&by_tag, Some("alpha"), "Generated Tags", 1, 0).unwrap();
|
||||||
assert!(single.starts_with("= Tag: alpha ="));
|
assert!(single.starts_with("= Tag: alpha ="));
|
||||||
assert!(single.contains("- [[Home]]"));
|
assert!(single.contains("- [[Home]]"));
|
||||||
assert!(single.contains("- [[Work]]"));
|
assert!(single.contains("- [[Work]]"));
|
||||||
|
|
||||||
// No-arg variant emits a section per tag.
|
// No-arg variant emits a section per tag.
|
||||||
let all = ops::build_tag_links_text(&by_tag, None).unwrap();
|
let all = ops::build_tag_links_text(&by_tag, None, "Generated Tags", 1, 0).unwrap();
|
||||||
assert!(all.starts_with("= Tags ="));
|
assert!(all.starts_with("= Generated Tags ="));
|
||||||
assert!(all.contains("== alpha =="));
|
assert!(all.contains("== alpha =="));
|
||||||
assert!(all.contains("== beta =="));
|
assert!(all.contains("== beta =="));
|
||||||
|
|
||||||
let src = "= Home =\n";
|
let src = "= Home =\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||||
assert!(ops::tag_links_edit(src, &doc, &uri, Some("alpha"), &by_tag, true).is_some());
|
assert!(ops::tag_links_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&uri,
|
||||||
|
Some("alpha"),
|
||||||
|
&by_tag,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
// :NuwikiRebuildTags — force a full workspace re-index. A rebuild must
|
// :NuwikiRebuildTags — force a full workspace re-index. A rebuild must
|
||||||
@@ -431,7 +468,6 @@ fn nuwiki_2html_renders_page() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -453,7 +489,6 @@ fn nuwiki_2html_browse_shares_render_and_is_advertised() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -500,7 +535,7 @@ fn nuwiki_rss_writes_feed_file() {
|
|||||||
let path = export_ops::write_rss(&cfg, &entries).unwrap();
|
let path = export_ops::write_rss(&cfg, &entries).unwrap();
|
||||||
assert!(path.ends_with("rss.xml"));
|
assert!(path.ends_with("rss.xml"));
|
||||||
let xml = std::fs::read_to_string(&path).unwrap();
|
let xml = std::fs::read_to_string(&path).unwrap();
|
||||||
assert!(xml.contains("<rss version=\"2.0\">"));
|
assert!(xml.contains("<rss version=\"2.0\""));
|
||||||
// Newest entry listed first.
|
// Newest entry listed first.
|
||||||
let p12 = xml.find("2026-05-12").unwrap();
|
let p12 = xml.find("2026-05-12").unwrap();
|
||||||
let p10 = xml.find("2026-05-10").unwrap();
|
let p10 = xml.find("2026-05-10").unwrap();
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ fn commands_list_advertises_link_helpers() {
|
|||||||
"nuwiki.link.pasteWikilink",
|
"nuwiki.link.pasteWikilink",
|
||||||
"nuwiki.link.pasteUrl",
|
"nuwiki.link.pasteUrl",
|
||||||
"nuwiki.link.catUrl",
|
"nuwiki.link.catUrl",
|
||||||
|
// Both GenerateLinks forms must be advertised so coc/vim-lsp accept
|
||||||
|
// them (the scoped form was previously sent but unregistered).
|
||||||
|
"nuwiki.links.generate",
|
||||||
|
"nuwiki.links.generateForPath",
|
||||||
] {
|
] {
|
||||||
assert!(names.contains(&name), "missing: {name}");
|
assert!(names.contains(&name), "missing: {name}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ fn render_marker_handles_each_symbol() {
|
|||||||
fn remove_done_strips_done_and_rejected_items() {
|
fn remove_done_strips_done_and_rejected_items() {
|
||||||
let src = "- [ ] todo\n- [X] done\n- [-] rejected\n- [ ] also todo\n";
|
let src = "- [ ] todo\n- [X] done\n- [-] rejected\n- [ ] also todo\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, true).expect("edit");
|
let edit = ops::remove_done_edit(src, &ast, &uri(), None, None, true).expect("edit");
|
||||||
let changes = edit.changes.expect("changes");
|
let changes = edit.changes.expect("changes");
|
||||||
let edits = &changes[&uri()];
|
let edits = &changes[&uri()];
|
||||||
assert_eq!(edits.len(), 2, "expected 2 deletions");
|
assert_eq!(edits.len(), 2, "expected 2 deletions");
|
||||||
@@ -75,14 +75,14 @@ fn remove_done_strips_done_and_rejected_items() {
|
|||||||
fn remove_done_leaves_open_items_alone() {
|
fn remove_done_leaves_open_items_alone() {
|
||||||
let src = "- [ ] todo\n- [.] partial\n- [o] more\n";
|
let src = "- [ ] todo\n- [.] partial\n- [o] more\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, true).is_none());
|
assert!(ops::remove_done_edit(src, &ast, &uri(), None, None, true).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remove_done_returns_none_when_no_lists() {
|
fn remove_done_returns_none_when_no_lists() {
|
||||||
let src = "= heading =\nparagraph\n";
|
let src = "= heading =\nparagraph\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
assert!(ops::remove_done_edit(src, &ast, &uri(), None, true).is_none());
|
assert!(ops::remove_done_edit(src, &ast, &uri(), None, None, true).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -96,7 +96,7 @@ fn remove_done_scoped_by_position_to_current_list() {
|
|||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
});
|
});
|
||||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, None, true).expect("edit");
|
||||||
let edits = &edit.changes.unwrap()[&uri()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 2, "both done items in the current list match");
|
assert_eq!(edits.len(), 2, "both done items in the current list match");
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ fn remove_done_position_leaves_other_lists_untouched() {
|
|||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
});
|
});
|
||||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, None, true).expect("edit");
|
||||||
let edits = &edit.changes.unwrap()[&uri()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 1, "only the first list's done item is removed");
|
assert_eq!(edits.len(), 1, "only the first list's done item is removed");
|
||||||
}
|
}
|
||||||
@@ -126,11 +126,32 @@ fn remove_done_position_cascades_into_sublists() {
|
|||||||
line: 0,
|
line: 0,
|
||||||
character: 0,
|
character: 0,
|
||||||
});
|
});
|
||||||
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, true).expect("edit");
|
let edit = ops::remove_done_edit(src, &ast, &uri(), pos, None, true).expect("edit");
|
||||||
let edits = &edit.changes.unwrap()[&uri()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 1, "the nested done child is removed");
|
assert_eq!(edits.len(), 1, "the nested done child is removed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_done_ranged_restricts_to_selected_lines() {
|
||||||
|
// The `:'<,'>VimwikiRemoveDone` form: a 0-indexed inclusive line range
|
||||||
|
// sweeps the whole doc but only deletes done/rejected items inside it.
|
||||||
|
// Lines: 0 `[X]`, 1 `[ ]`, 2 `[X]`, 3 `[-]`. Range 0..=1 keeps only the
|
||||||
|
// line-0 done item; the line-2/3 done+rejected items survive.
|
||||||
|
let src = "- [X] a\n- [ ] b\n- [X] c\n- [-] d\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let edit = ops::remove_done_edit(src, &ast, &uri(), None, Some((0, 1)), true).expect("edit");
|
||||||
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
|
assert_eq!(edits.len(), 1, "only the done item on line 0 is in range");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_done_ranged_returns_none_when_range_has_no_done() {
|
||||||
|
// Range covering only open items → nothing to delete.
|
||||||
|
let src = "- [X] a\n- [ ] b\n- [ ] c\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
assert!(ops::remove_done_edit(src, &ast, &uri(), None, Some((1, 2)), true).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
// ===== renumber =====
|
// ===== renumber =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -233,7 +254,8 @@ fn remove_checkbox_none_without_checkbox() {
|
|||||||
fn change_level_indents_single_item() {
|
fn change_level_indents_single_item() {
|
||||||
let src = "- one\n- two\n";
|
let src = "- one\n- two\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let edit = ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true).expect("edit");
|
let edit =
|
||||||
|
ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true, false, &[]).expect("edit");
|
||||||
let edits = &edit.changes.unwrap()[&uri()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
assert_eq!(edits.len(), 1);
|
assert_eq!(edits.len(), 1);
|
||||||
// delta=1 → +2 spaces
|
// delta=1 → +2 spaces
|
||||||
@@ -244,16 +266,57 @@ fn change_level_indents_single_item() {
|
|||||||
fn change_level_dedents_clamps_to_zero() {
|
fn change_level_dedents_clamps_to_zero() {
|
||||||
let src = "- root\n";
|
let src = "- root\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let edit = ops::change_level_edit(src, &ast, &uri(), 0, -1, false, true);
|
let edit = ops::change_level_edit(src, &ast, &uri(), 0, -1, false, true, false, &[]);
|
||||||
// Already at column 0 — dedent is a no-op.
|
// Already at column 0 — dedent is a no-op.
|
||||||
assert!(edit.is_none());
|
assert!(edit.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_bullets_rotates_glyph_on_indent() {
|
||||||
|
// With cycle_bullets + bullet_types [-,*,#], indenting a `-` item (depth 0
|
||||||
|
// → 1) rotates the glyph to `*` (and dedenting back to `-`).
|
||||||
|
let bullets: Vec<String> = ["-", "*", "#"].iter().map(|s| s.to_string()).collect();
|
||||||
|
// Put the list well into the document so depth must come from the item's
|
||||||
|
// indentation, not its absolute byte offset (regression guard).
|
||||||
|
let src = "some intro paragraph text here\n\n- one\n- two\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let edit =
|
||||||
|
ops::change_level_edit(src, &ast, &uri(), 3, 1, false, true, true, &bullets).expect("edit");
|
||||||
|
let texts: Vec<String> = edit.changes.unwrap()[&uri()]
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.new_text.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(texts.contains(&" ".to_string()), "indent edit: {texts:?}");
|
||||||
|
assert!(
|
||||||
|
texts.contains(&"*".to_string()),
|
||||||
|
"glyph rotated to *: {texts:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_bullets_off_leaves_glyph() {
|
||||||
|
let bullets: Vec<String> = ["-", "*", "#"].iter().map(|s| s.to_string()).collect();
|
||||||
|
let src = "- one\n- two\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let edit = ops::change_level_edit(src, &ast, &uri(), 1, 1, false, true, false, &bullets)
|
||||||
|
.expect("edit");
|
||||||
|
let texts: Vec<String> = edit.changes.unwrap()[&uri()]
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.new_text.clone())
|
||||||
|
.collect();
|
||||||
|
// Only the indent edit; no glyph rewrite.
|
||||||
|
assert!(
|
||||||
|
!texts.iter().any(|t| t == "*"),
|
||||||
|
"no glyph change: {texts:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn change_level_whole_subtree_walks_descendants() {
|
fn change_level_whole_subtree_walks_descendants() {
|
||||||
let src = "- parent\n - child a\n - child b\n- sibling\n";
|
let src = "- parent\n - child a\n - child b\n- sibling\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let edit = ops::change_level_edit(src, &ast, &uri(), 0, 1, true, true).expect("edit");
|
let edit =
|
||||||
|
ops::change_level_edit(src, &ast, &uri(), 0, 1, true, true, false, &[]).expect("edit");
|
||||||
let edits = &edit.changes.unwrap()[&uri()];
|
let edits = &edit.changes.unwrap()[&uri()];
|
||||||
// Parent + 2 children get indented. Sibling stays.
|
// Parent + 2 children get indented. Sibling stays.
|
||||||
assert!(edits.len() >= 3, "got {} edits", edits.len());
|
assert!(edits.len() >= 3, "got {} edits", edits.len());
|
||||||
@@ -280,6 +343,6 @@ fn commands_list_includes_cluster_a() {
|
|||||||
fn one_edit_text_round_trip() {
|
fn one_edit_text_round_trip() {
|
||||||
let src = "- [X] done\n";
|
let src = "- [X] done\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let edit = ops::remove_done_edit(src, &ast, &uri(), None, true).unwrap();
|
let edit = ops::remove_done_edit(src, &ast, &uri(), None, None, true).unwrap();
|
||||||
let _ = one_edit_text(edit);
|
let _ = one_edit_text(edit);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ fn blank_table_handles_single_cell_grid() {
|
|||||||
fn align_pads_short_cells_to_widest() {
|
fn align_pads_short_cells_to_widest() {
|
||||||
let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n";
|
let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let edit = ops::table_align_edit(src, &ast, &uri(), 0, true).expect("edit");
|
let edit = ops::table_align_edit(src, &ast, &uri(), 0, true, false).expect("edit");
|
||||||
let body = one_text(edit);
|
let body = one_text(edit);
|
||||||
let lines: Vec<&str> = body.lines().collect();
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
eprintln!("rendered table:");
|
eprintln!("rendered table:");
|
||||||
@@ -64,7 +64,23 @@ fn align_pads_short_cells_to_widest() {
|
|||||||
fn align_returns_none_when_cursor_off_table() {
|
fn align_returns_none_when_cursor_off_table() {
|
||||||
let src = "paragraph\n";
|
let src = "paragraph\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
assert!(ops::table_align_edit(src, &ast, &uri(), 0, true).is_none());
|
assert!(ops::table_align_edit(src, &ast, &uri(), 0, true, false).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_reduce_last_col_keeps_last_column_minimal() {
|
||||||
|
// With reduce_last_col=true, the last column isn't padded to its content
|
||||||
|
// width — it stays at the minimum (1), so `three` is not surrounded by
|
||||||
|
// alignment padding on the right edge.
|
||||||
|
let src = "|a|bbb|c|\n|--|--|--|\n|1|2|three|\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let edit = ops::table_align_edit(src, &ast, &uri(), 0, true, true).expect("edit");
|
||||||
|
let body = one_text(edit);
|
||||||
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
|
// First two columns still align; the last column stays at width 1 (not 5),
|
||||||
|
// so the header's last cell renders `| c |` not `| c |`.
|
||||||
|
assert_eq!(lines[0], "| a | bbb | c |", "got: {:?}", lines[0]);
|
||||||
|
assert_eq!(lines[2], "| 1 | 2 | three |", "got: {:?}", lines[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== moveColumn =====
|
// ===== moveColumn =====
|
||||||
@@ -74,7 +90,7 @@ fn move_column_right_swaps_with_neighbour() {
|
|||||||
let src = "|a|b|c|\n|--|--|--|\n|1|2|3|\n";
|
let src = "|a|b|c|\n|--|--|--|\n|1|2|3|\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
// Cursor on first column → swap right.
|
// Cursor on first column → swap right.
|
||||||
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, 1, true).expect("edit");
|
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, 1, true, false).expect("edit");
|
||||||
let body = one_text(edit);
|
let body = one_text(edit);
|
||||||
let lines: Vec<&str> = body.lines().collect();
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
assert!(lines[0].contains("b") && lines[0].contains("a"));
|
assert!(lines[0].contains("b") && lines[0].contains("a"));
|
||||||
@@ -90,7 +106,7 @@ fn move_column_left_at_zero_is_noop() {
|
|||||||
let src = "|a|b|\n|--|--|\n|1|2|\n";
|
let src = "|a|b|\n|--|--|\n|1|2|\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
// Cursor in column 0; left would be -1 → returns None.
|
// Cursor in column 0; left would be -1 → returns None.
|
||||||
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, -1, true);
|
let edit = ops::table_move_column_edit(src, &ast, &uri(), 0, 1, -1, true, false);
|
||||||
assert!(edit.is_none());
|
assert!(edit.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +114,7 @@ fn move_column_left_at_zero_is_noop() {
|
|||||||
fn move_column_returns_none_off_table() {
|
fn move_column_returns_none_off_table() {
|
||||||
let src = "paragraph\n";
|
let src = "paragraph\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
assert!(ops::table_move_column_edit(src, &ast, &uri(), 0, 0, 1, true).is_none());
|
assert!(ops::table_move_column_edit(src, &ast, &uri(), 0, 0, 1, true, false).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== COMMANDS list =====
|
// ===== COMMANDS list =====
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ fn tag_pages_snapshot_sorts_pages_alphabetically() {
|
|||||||
fn build_tag_links_for_single_tag() {
|
fn build_tag_links_for_single_tag() {
|
||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("release".to_string(), vec!["Alpha".into(), "Beta".into()]);
|
snap.insert("release".to_string(), vec!["Alpha".into(), "Beta".into()]);
|
||||||
let out = ops::build_tag_links_text(&snap, Some("release")).unwrap();
|
let out = ops::build_tag_links_text(&snap, Some("release"), "Generated Tags", 1, 0).unwrap();
|
||||||
let lines: Vec<&str> = out.lines().collect();
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
assert_eq!(lines[0], "= Tag: release =");
|
assert_eq!(lines[0], "= Tag: release =");
|
||||||
assert_eq!(lines[1], "- [[Alpha]]");
|
assert_eq!(lines[1], "- [[Alpha]]");
|
||||||
@@ -213,7 +213,7 @@ fn build_tag_links_for_single_tag() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_tag_links_for_missing_tag_returns_none() {
|
fn build_tag_links_for_missing_tag_returns_none() {
|
||||||
let snap = BTreeMap::new();
|
let snap = BTreeMap::new();
|
||||||
assert!(ops::build_tag_links_text(&snap, Some("ghost")).is_none());
|
assert!(ops::build_tag_links_text(&snap, Some("ghost"), "Generated Tags", 1, 0).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -221,8 +221,8 @@ fn build_tag_links_full_index_groups_by_tag() {
|
|||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("a".to_string(), vec!["P1".into()]);
|
snap.insert("a".to_string(), vec!["P1".into()]);
|
||||||
snap.insert("b".to_string(), vec!["P2".into(), "P3".into()]);
|
snap.insert("b".to_string(), vec!["P2".into(), "P3".into()]);
|
||||||
let out = ops::build_tag_links_text(&snap, None).unwrap();
|
let out = ops::build_tag_links_text(&snap, None, "Generated Tags", 1, 0).unwrap();
|
||||||
assert!(out.starts_with("= Tags =\n"));
|
assert!(out.starts_with("= Generated Tags =\n"));
|
||||||
assert!(out.contains("== a =="));
|
assert!(out.contains("== a =="));
|
||||||
assert!(out.contains("== b =="));
|
assert!(out.contains("== b =="));
|
||||||
assert!(out.contains("- [[P1]]"));
|
assert!(out.contains("- [[P1]]"));
|
||||||
@@ -233,7 +233,7 @@ fn build_tag_links_full_index_groups_by_tag() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_tag_links_full_index_returns_none_when_no_tags() {
|
fn build_tag_links_full_index_returns_none_when_no_tags() {
|
||||||
let snap = BTreeMap::new();
|
let snap = BTreeMap::new();
|
||||||
assert!(ops::build_tag_links_text(&snap, None).is_none());
|
assert!(ops::build_tag_links_text(&snap, None, "Generated Tags", 1, 0).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== `nuwiki.tags.generateLinks` — tag_links_edit (in-buffer rewrite) =====
|
// ===== `nuwiki.tags.generateLinks` — tag_links_edit (in-buffer rewrite) =====
|
||||||
@@ -245,8 +245,19 @@ fn tag_links_edit_inserts_when_section_missing() {
|
|||||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("release".to_string(), vec!["Alpha".into()]);
|
snap.insert("release".to_string(), vec!["Alpha".into()]);
|
||||||
let edit =
|
let edit = ops::tag_links_edit(
|
||||||
ops::tag_links_edit(src, &ast, &uri, Some("release"), &snap, true).expect("got an edit");
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
Some("release"),
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("got an edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
// Insertion at start.
|
// Insertion at start.
|
||||||
assert_eq!(te.range.start, te.range.end);
|
assert_eq!(te.range.start, te.range.end);
|
||||||
@@ -254,6 +265,37 @@ fn tag_links_edit_inserts_when_section_missing() {
|
|||||||
assert!(te.new_text.contains("[[Alpha]]"));
|
assert!(te.new_text.contains("[[Alpha]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tag_links_edit_inserts_at_cursor_line() {
|
||||||
|
// A fresh tags section goes at the cursor line, not the EOF.
|
||||||
|
let src = "= Notes =\nbody\nmore\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
|
let mut snap = BTreeMap::new();
|
||||||
|
snap.insert("release".to_string(), vec!["Alpha".into()]);
|
||||||
|
let edit = ops::tag_links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
Some("release"),
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
Some(1),
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
|
assert_eq!(te.range.start.line, 1);
|
||||||
|
assert_eq!(te.range.start.character, 0);
|
||||||
|
assert!(
|
||||||
|
te.new_text.starts_with("\n= Tag: release ="),
|
||||||
|
"got: {:?}",
|
||||||
|
te.new_text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag_links_edit_replaces_existing_section() {
|
fn tag_links_edit_replaces_existing_section() {
|
||||||
let src = "= Tag: release =\n- [[Stale]]\n\n= Other =\n";
|
let src = "= Tag: release =\n- [[Stale]]\n\n= Other =\n";
|
||||||
@@ -261,7 +303,19 @@ fn tag_links_edit_replaces_existing_section() {
|
|||||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("release".to_string(), vec!["Fresh".into()]);
|
snap.insert("release".to_string(), vec!["Fresh".into()]);
|
||||||
let edit = ops::tag_links_edit(src, &ast, &uri, Some("release"), &snap, true).expect("edit");
|
let edit = ops::tag_links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
Some("release"),
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert_eq!(te.range.start.line, 0);
|
assert_eq!(te.range.start.line, 0);
|
||||||
assert!(te.new_text.contains("[[Fresh]]"));
|
assert!(te.new_text.contains("[[Fresh]]"));
|
||||||
@@ -270,24 +324,82 @@ fn tag_links_edit_replaces_existing_section() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag_links_edit_full_index_replaces_existing_tags_section() {
|
fn tag_links_edit_full_index_replaces_existing_tags_section() {
|
||||||
let src = "= Tags =\n- [[old]]\n";
|
let src = "= Generated Tags =\n- [[old]]\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
let mut snap = BTreeMap::new();
|
let mut snap = BTreeMap::new();
|
||||||
snap.insert("alpha".to_string(), vec!["P".into()]);
|
snap.insert("alpha".to_string(), vec!["P".into()]);
|
||||||
let edit = ops::tag_links_edit(src, &ast, &uri, None, &snap, true).expect("edit");
|
let edit = ops::tag_links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
None,
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("== alpha =="));
|
assert!(te.new_text.contains("== alpha =="));
|
||||||
assert!(!te.new_text.contains("[[old]]"));
|
assert!(!te.new_text.contains("[[old]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tag_links_rebuild_edit_only_acts_when_index_present() {
|
||||||
|
let mut snap = BTreeMap::new();
|
||||||
|
snap.insert("a".to_string(), vec!["P".into()]);
|
||||||
|
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
|
let without = "= Home =\nbody\n";
|
||||||
|
assert!(
|
||||||
|
ops::tag_links_rebuild_edit(
|
||||||
|
without,
|
||||||
|
&parse(without),
|
||||||
|
&uri,
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
.is_none(),
|
||||||
|
"auto_generate_tags must not insert an index into a page that lacks one"
|
||||||
|
);
|
||||||
|
let with = "= Generated Tags =\n- [[old]]\n";
|
||||||
|
assert!(ops::tag_links_rebuild_edit(
|
||||||
|
with,
|
||||||
|
&parse(with),
|
||||||
|
&uri,
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag_links_edit_returns_none_for_unknown_tag() {
|
fn tag_links_edit_returns_none_for_unknown_tag() {
|
||||||
let src = "hi\n";
|
let src = "hi\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||||
let snap = BTreeMap::new();
|
let snap = BTreeMap::new();
|
||||||
assert!(ops::tag_links_edit(src, &ast, &uri, Some("ghost"), &snap, true).is_none());
|
assert!(ops::tag_links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
Some("ghost"),
|
||||||
|
&snap,
|
||||||
|
true,
|
||||||
|
"Generated Tags",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== smoke: COMMANDS list advertises tag commands =====
|
// ===== smoke: COMMANDS list advertises tag commands =====
|
||||||
|
|||||||
@@ -274,7 +274,15 @@ fn build_index_body_is_newest_first_grouped_by_year_month() {
|
|||||||
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
||||||
);
|
);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 1);
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
1,
|
||||||
|
&[],
|
||||||
|
0,
|
||||||
|
);
|
||||||
let lines: Vec<&str> = body.lines().collect();
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
assert_eq!(lines[0], "= Diary =");
|
assert_eq!(lines[0], "= Diary =");
|
||||||
// First date in output should be the latest: 2026-05-12
|
// First date in output should be the latest: 2026-05-12
|
||||||
@@ -292,7 +300,7 @@ fn build_index_body_is_newest_first_grouped_by_year_month() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_index_body_empty_when_no_entries() {
|
fn build_index_body_empty_when_no_entries() {
|
||||||
let body = diary::build_index_body(&[], "diary", "Diary", diary::DiarySort::Desc, 1);
|
let body = diary::build_index_body(&[], "diary", "Diary", diary::DiarySort::Desc, 1, &[], 0);
|
||||||
assert_eq!(body, "= Diary =\n");
|
assert_eq!(body, "= Diary =\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +308,8 @@ fn build_index_body_empty_when_no_entries() {
|
|||||||
fn build_index_body_ascending_sort_lists_oldest_first() {
|
fn build_index_body_ascending_sort_lists_oldest_first() {
|
||||||
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Asc, 1);
|
let body =
|
||||||
|
diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Asc, 1, &[], 0);
|
||||||
let older = body.find("2026-05-11").unwrap();
|
let older = body.find("2026-05-11").unwrap();
|
||||||
let newer = body.find("2026-05-12").unwrap();
|
let newer = body.find("2026-05-12").unwrap();
|
||||||
assert!(older < newer, "ascending sort lists oldest first");
|
assert!(older < newer, "ascending sort lists oldest first");
|
||||||
@@ -310,7 +319,15 @@ fn build_index_body_ascending_sort_lists_oldest_first() {
|
|||||||
fn build_index_body_caption_level_shifts_all_headings() {
|
fn build_index_body_caption_level_shifts_all_headings() {
|
||||||
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 2);
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
2,
|
||||||
|
&[],
|
||||||
|
0,
|
||||||
|
);
|
||||||
assert!(body.contains("== Diary =="), "caption at level 2");
|
assert!(body.contains("== Diary =="), "caption at level 2");
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("=== 2026 ==="),
|
body.contains("=== 2026 ==="),
|
||||||
@@ -327,11 +344,102 @@ fn build_index_body_clamps_caption_level_to_six() {
|
|||||||
let idx = build_index_with_entries("/tmp/diaryClamp", &["2026-05-11"]);
|
let idx = build_index_with_entries("/tmp/diaryClamp", &["2026-05-11"]);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
// caption_level 6 → year/month would be 7/8 but clamp to 6.
|
// caption_level 6 → year/month would be 7/8 but clamp to 6.
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 6);
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
6,
|
||||||
|
&[],
|
||||||
|
0,
|
||||||
|
);
|
||||||
assert!(body.contains("====== Diary ======"));
|
assert!(body.contains("====== Diary ======"));
|
||||||
assert!(!body.contains("======="), "no heading exceeds level 6");
|
assert!(!body.contains("======="), "no heading exceeds level 6");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_index_body_uses_custom_diary_months() {
|
||||||
|
let idx = build_index_with_entries("/tmp/diaryMonths", &["2026-05-11", "2026-12-01"]);
|
||||||
|
let entries = diary::list_entries(&idx);
|
||||||
|
// A localized month table (Portuguese) replaces the English labels.
|
||||||
|
let months: Vec<String> = [
|
||||||
|
"Janeiro",
|
||||||
|
"Fevereiro",
|
||||||
|
"Março",
|
||||||
|
"Abril",
|
||||||
|
"Maio",
|
||||||
|
"Junho",
|
||||||
|
"Julho",
|
||||||
|
"Agosto",
|
||||||
|
"Setembro",
|
||||||
|
"Outubro",
|
||||||
|
"Novembro",
|
||||||
|
"Dezembro",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
1,
|
||||||
|
&months,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert!(body.contains("=== Maio ==="), "May → Maio: {body}");
|
||||||
|
assert!(
|
||||||
|
body.contains("=== Dezembro ==="),
|
||||||
|
"December → Dezembro: {body}"
|
||||||
|
);
|
||||||
|
assert!(!body.contains("May"), "English label leaked: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_index_body_falls_back_per_missing_month_slot() {
|
||||||
|
let idx = build_index_with_entries("/tmp/diaryShort", &["2026-05-11"]);
|
||||||
|
let entries = diary::list_entries(&idx);
|
||||||
|
// A too-short table only covers Jan–Mar; May falls back to English.
|
||||||
|
let months: Vec<String> = ["Jan", "Feb", "Mar"]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
1,
|
||||||
|
&months,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains("=== May ==="),
|
||||||
|
"missing slot → English: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_index_body_clamps_negative_caption_level_to_zero() {
|
||||||
|
let idx = build_index_with_entries("/tmp/diaryNeg", &["2026-05-11"]);
|
||||||
|
let entries = diary::list_entries(&idx);
|
||||||
|
// vimwiki allows diary_caption_level = -1; nuwiki clamps it to 0 (same as
|
||||||
|
// a caption_level of 0: caption h1, year h1, month h2).
|
||||||
|
let body = diary::build_index_body(
|
||||||
|
&entries,
|
||||||
|
"diary",
|
||||||
|
"Diary",
|
||||||
|
diary::DiarySort::Desc,
|
||||||
|
-1,
|
||||||
|
&[],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert!(body.contains("= Diary ="), "caption at level 1: {body}");
|
||||||
|
assert!(body.contains("= 2026 ="), "year at level 1: {body}");
|
||||||
|
assert!(body.contains("== May =="), "month at level 2: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_index_body_round_trip() {
|
fn render_index_body_round_trip() {
|
||||||
let cfg = wiki_cfg("/tmp/diaryA");
|
let cfg = wiki_cfg("/tmp/diaryA");
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ fn html_config_defaults_match_vimwiki() {
|
|||||||
assert!(!c.auto_export);
|
assert!(!c.auto_export);
|
||||||
assert!(!c.html_filename_parameterization);
|
assert!(!c.html_filename_parameterization);
|
||||||
assert!(c.exclude_files.is_empty());
|
assert!(c.exclude_files.is_empty());
|
||||||
|
assert_eq!(c.html_header_numbering, 0); // upstream default: numbering off
|
||||||
|
assert!(c.html_header_numbering_sym.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -230,6 +232,104 @@ fn template_for_falls_back_when_no_directive() {
|
|||||||
|
|
||||||
// ===== render_page_html =====
|
// ===== render_page_html =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_passes_through_valid_html_tags() {
|
||||||
|
// `<sub>`/`<kbd>` are in the default valid_html_tags → verbatim; `<script>`
|
||||||
|
// is not → escaped.
|
||||||
|
let ast = parse("H<sub>2</sub>O press <kbd>Ctrl</kbd> <script>x</script>\n");
|
||||||
|
let c = cfg("/tmp/x");
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"Home",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("H<sub>2</sub>O"), "sub verbatim: {html}");
|
||||||
|
assert!(html.contains("<kbd>Ctrl</kbd>"), "kbd verbatim: {html}");
|
||||||
|
assert!(html.contains("<script>"), "script escaped: {html}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_ignore_newline_controls_soft_breaks() {
|
||||||
|
// Paragraph + list item, each with a soft (single) newline inside.
|
||||||
|
let ast = parse("alpha\nbeta\n\n- one\n two\n");
|
||||||
|
// Default (true/true): soft breaks render as spaces, no <br>.
|
||||||
|
let c = cfg("/tmp/x");
|
||||||
|
let def = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"Home",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!def.contains("<br"), "default joins with spaces: {def}");
|
||||||
|
assert!(def.contains("alpha beta"), "para space: {def}");
|
||||||
|
|
||||||
|
// false/false: soft breaks become <br /> in both paragraph and list item.
|
||||||
|
let mut c2 = cfg("/tmp/x");
|
||||||
|
c2.html.text_ignore_newline = false;
|
||||||
|
c2.html.list_ignore_newline = false;
|
||||||
|
let br = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"Home",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c2.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(br.contains("alpha<br />beta"), "para <br>: {br}");
|
||||||
|
assert!(
|
||||||
|
br.contains("one<br />two") || br.contains("one<br /> two"),
|
||||||
|
"list <br>: {br}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_substitutes_emoji_when_enabled() {
|
||||||
|
let ast = parse("ship it :rocket: and :tada:\n");
|
||||||
|
let c = cfg("/tmp/x"); // emoji_enable defaults true
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"Home",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("🚀"), "rocket: {html}");
|
||||||
|
assert!(html.contains("🎉"), "tada: {html}");
|
||||||
|
assert!(!html.contains(":rocket:"), "shortcode replaced: {html}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_emoji_off_leaves_shortcodes() {
|
||||||
|
let ast = parse("plain :rocket: here\n");
|
||||||
|
let mut c = cfg("/tmp/x");
|
||||||
|
c.html.emoji_enable = false;
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"Home",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains(":rocket:"), "left as-is: {html}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_page_html_substitutes_title_content_and_extras() {
|
fn render_page_html_substitutes_title_content_and_extras() {
|
||||||
let ast = parse("%title My Page\n= One =\nbody text\n");
|
let ast = parse("%title My Page\n= One =\nbody text\n");
|
||||||
@@ -243,7 +343,6 @@ fn render_page_html_substitutes_title_content_and_extras() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
extras,
|
extras,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -263,13 +362,140 @@ fn render_page_html_uses_metadata_date_when_set() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(html.contains("DATE=2025-01-02"), "got: {html}");
|
assert!(html.contains("DATE=2025-01-02"), "got: {html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_numbers_headers_when_enabled() {
|
||||||
|
let ast = parse("= Intro =\n== Background ==\n== Methods ==\n= Results =\n");
|
||||||
|
let mut c = cfg("/tmp/x");
|
||||||
|
c.html.html_header_numbering = 1; // number from h1
|
||||||
|
c.html.html_header_numbering_sym = ".".into();
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
None,
|
||||||
|
"Home",
|
||||||
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Intro\"><h1 id=\"Intro\" class=\"header\"><a href=\"#Intro\">1. Intro</a></h1></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Intro-Background\"><h2 id=\"Background\" class=\"header\"><a href=\"#Intro-Background\">1.1. Background</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Intro-Methods\"><h2 id=\"Methods\" class=\"header\"><a href=\"#Intro-Methods\">1.2. Methods</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Results\"><h1 id=\"Results\" class=\"header\"><a href=\"#Results\">2. Results</a></h1></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_numbering_skips_headers_above_start_level() {
|
||||||
|
let ast = parse("= Title =\n== Section ==\n=== Sub ===\n");
|
||||||
|
let mut c = cfg("/tmp/x");
|
||||||
|
c.html.html_header_numbering = 2; // start at h2; h1 stays unnumbered
|
||||||
|
// empty sym → number followed by a bare space
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
None,
|
||||||
|
"Home",
|
||||||
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Title\"><h1 id=\"Title\" class=\"header\"><a href=\"#Title\">Title</a></h1></div>"
|
||||||
|
),
|
||||||
|
"h1 unnumbered, got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Title-Section\"><h2 id=\"Section\" class=\"header\"><a href=\"#Title-Section\">1 Section</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Title-Section-Sub\"><h3 id=\"Sub\" class=\"header\"><a href=\"#Title-Section-Sub\">1.1 Sub</a></h3></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_same_page_anchor_uses_current_filename() {
|
||||||
|
// vimwiki parity (#4): `[[#Section]]` resolves to `<page>.html#Section`,
|
||||||
|
// not a bare `#Section`, where <page> is the file being rendered.
|
||||||
|
let ast = parse("= Contents =\n\n[[#Contents]]\n");
|
||||||
|
let c = cfg("/tmp/x");
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
None,
|
||||||
|
"index",
|
||||||
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
html.contains("<a href=\"index.html#Contents\">"),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_no_header_numbering_by_default() {
|
||||||
|
let ast = parse("= Intro =\n== Sub ==\n");
|
||||||
|
let c = cfg("/tmp/x");
|
||||||
|
let html = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
None,
|
||||||
|
"Home",
|
||||||
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Intro\"><h1 id=\"Intro\" class=\"header\"><a href=\"#Intro\">Intro</a></h1></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(
|
||||||
|
"<div id=\"Intro-Sub\"><h2 id=\"Sub\" class=\"header\"><a href=\"#Intro-Sub\">Sub</a></h2></div>"
|
||||||
|
),
|
||||||
|
"got: {html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_page_html_root_path_for_nested_pages() {
|
fn render_page_html_root_path_for_nested_pages() {
|
||||||
let ast = parse("= D =\n");
|
let ast = parse("= D =\n");
|
||||||
@@ -281,7 +507,6 @@ fn render_page_html_root_path_for_nested_pages() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -301,7 +526,6 @@ fn render_page_html_extra_var_overrides_default() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
extras,
|
extras,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -324,7 +548,6 @@ fn render_page_html_substitutes_vimwiki_percent_template() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -351,7 +574,6 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -361,49 +583,6 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
|||||||
assert!(html.contains("href=\"posts/index.html\""), "plain: {html}");
|
assert!(html.contains("href=\"posts/index.html\""), "plain: {html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_page_html_list_margin_only_styles_top_level_list() {
|
|
||||||
// `list_margin >= 0` forces a `margin-left:<n>em` on the outermost
|
|
||||||
// list; nested lists and the default of `-1` stay unstyled.
|
|
||||||
let ast = parse("- a\n - b\n");
|
|
||||||
let c = cfg("/tmp/x");
|
|
||||||
|
|
||||||
let styled = export::render_page_html(
|
|
||||||
&ast,
|
|
||||||
Some("{{content}}".into()),
|
|
||||||
"index",
|
|
||||||
DiaryDate::today_utc(),
|
|
||||||
&c.html,
|
|
||||||
None,
|
|
||||||
2,
|
|
||||||
HashMap::new(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
styled.contains("<ul style=\"margin-left:2em\">"),
|
|
||||||
"top-level margin: {styled}"
|
|
||||||
);
|
|
||||||
// Exactly one styled list — the nested `<ul>` is plain.
|
|
||||||
assert_eq!(
|
|
||||||
styled.matches("margin-left").count(),
|
|
||||||
1,
|
|
||||||
"only top: {styled}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let plain = export::render_page_html(
|
|
||||||
&ast,
|
|
||||||
Some("{{content}}".into()),
|
|
||||||
"index",
|
|
||||||
DiaryDate::today_utc(),
|
|
||||||
&c.html,
|
|
||||||
None,
|
|
||||||
-1,
|
|
||||||
HashMap::new(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!plain.contains("margin-left"), "default unstyled: {plain}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== fallback_template + DEFAULT_CSS =====
|
// ===== fallback_template + DEFAULT_CSS =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -428,7 +607,7 @@ fn write_page_writes_html_to_disk() {
|
|||||||
let mut c = WikiConfig::from_root(root.clone());
|
let mut c = WikiConfig::from_root(root.clone());
|
||||||
c.html = HtmlConfig::from_root(&root);
|
c.html = HtmlConfig::from_root(&root);
|
||||||
let ast = parse("= Hello =\nworld\n");
|
let ast = parse("= Hello =\nworld\n");
|
||||||
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "Hello", &c).unwrap();
|
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "Hello", &c, false).unwrap();
|
||||||
assert_eq!(outcome.page, "Hello");
|
assert_eq!(outcome.page, "Hello");
|
||||||
assert!(outcome.output_path.exists());
|
assert!(outcome.output_path.exists());
|
||||||
let body = std::fs::read_to_string(&outcome.output_path).unwrap();
|
let body = std::fs::read_to_string(&outcome.output_path).unwrap();
|
||||||
@@ -442,7 +621,7 @@ fn write_page_creates_subdirs() {
|
|||||||
c.html = HtmlConfig::from_root(&root);
|
c.html = HtmlConfig::from_root(&root);
|
||||||
let ast = parse("= Daily =\n");
|
let ast = parse("= Daily =\n");
|
||||||
let outcome =
|
let outcome =
|
||||||
nuwiki_lsp::commands::export_ops::write_page(&ast, "diary/2026-05-11", &c).unwrap();
|
nuwiki_lsp::commands::export_ops::write_page(&ast, "diary/2026-05-11", &c, false).unwrap();
|
||||||
assert!(outcome.output_path.exists());
|
assert!(outcome.output_path.exists());
|
||||||
assert!(outcome
|
assert!(outcome
|
||||||
.output_path
|
.output_path
|
||||||
@@ -463,7 +642,7 @@ fn write_page_uses_template_file_when_present() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let ast = parse("%title T\n= H =\nbody\n");
|
let ast = parse("%title T\n= H =\nbody\n");
|
||||||
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "P", &c).unwrap();
|
let outcome = nuwiki_lsp::commands::export_ops::write_page(&ast, "P", &c, false).unwrap();
|
||||||
let body = std::fs::read_to_string(outcome.output_path).unwrap();
|
let body = std::fs::read_to_string(outcome.output_path).unwrap();
|
||||||
assert!(body.starts_with("<x>T|"));
|
assert!(body.starts_with("<x>T|"));
|
||||||
assert!(body.ends_with("</x>"));
|
assert!(body.ends_with("</x>"));
|
||||||
@@ -518,6 +697,124 @@ fn write_rss_emits_valid_xml_with_entries() {
|
|||||||
let i11 = xml.find("2026-05-11").unwrap();
|
let i11 = xml.find("2026-05-11").unwrap();
|
||||||
let i10 = xml.find("2026-05-10").unwrap();
|
let i10 = xml.find("2026-05-10").unwrap();
|
||||||
assert!(i11 < i10);
|
assert!(i11 < i10);
|
||||||
|
// No base_url → item links keep the file:// URI.
|
||||||
|
assert!(xml.contains("<link>file:///tmp/diary/2026-05-11.wiki</link>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_rss_uses_base_url_for_public_links() {
|
||||||
|
use nuwiki_lsp::diary::DiaryEntry;
|
||||||
|
use tower_lsp::lsp_types::Url;
|
||||||
|
|
||||||
|
let root = temp_root("rss_base");
|
||||||
|
let mut c = WikiConfig::from_root(root.clone());
|
||||||
|
c.html = HtmlConfig::from_root(&root);
|
||||||
|
c.html.base_url = "https://example.com/wiki/".into();
|
||||||
|
c.diary_rel_path = "diary".into();
|
||||||
|
let entries = vec![DiaryEntry {
|
||||||
|
date: DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
|
uri: Url::parse("file:///tmp/diary/2026-05-11.wiki").unwrap(),
|
||||||
|
}];
|
||||||
|
let path = nuwiki_lsp::commands::export_ops::write_rss(&c, &entries).unwrap();
|
||||||
|
let xml = std::fs::read_to_string(&path).unwrap();
|
||||||
|
// Channel link points at the diary index page (upstream fidelity); item
|
||||||
|
// link is the public per-entry URL.
|
||||||
|
assert!(
|
||||||
|
xml.contains("<link>https://example.com/wiki/diary/diary.html</link>"),
|
||||||
|
"channel link: {xml}"
|
||||||
|
);
|
||||||
|
assert!(xml.contains("<link>https://example.com/wiki/diary/2026-05-11.html</link>"));
|
||||||
|
// guid is the bare date, isPermaLink=false.
|
||||||
|
assert!(
|
||||||
|
xml.contains("<guid isPermaLink=\"false\">2026-05-11</guid>"),
|
||||||
|
"guid: {xml}"
|
||||||
|
);
|
||||||
|
// atom:link self + a pubDate are present.
|
||||||
|
assert!(xml.contains("rel=\"self\""), "atom:link: {xml}");
|
||||||
|
assert!(xml.contains("<pubDate>"), "pubDate: {xml}");
|
||||||
|
assert!(!xml.contains("file:///tmp/diary"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prune_orphan_html_deletes_sourceless_keeps_protected() {
|
||||||
|
let root = temp_root("prune");
|
||||||
|
let mut c = WikiConfig::from_root(root.clone());
|
||||||
|
c.html = HtmlConfig::from_root(&root);
|
||||||
|
c.html.user_htmls = vec!["keep.html".into()];
|
||||||
|
let html = &c.html.html_path;
|
||||||
|
std::fs::create_dir_all(html).unwrap();
|
||||||
|
// A sourced page (Home.wiki exists) → kept.
|
||||||
|
std::fs::write(root.join("Home.wiki"), "= Home =\n").unwrap();
|
||||||
|
std::fs::write(html.join("Home.html"), "<h1>Home</h1>").unwrap();
|
||||||
|
// An orphan (no source) → pruned.
|
||||||
|
std::fs::write(html.join("Ghost.html"), "<h1>Ghost</h1>").unwrap();
|
||||||
|
// A user_htmls-protected orphan → kept.
|
||||||
|
std::fs::write(html.join("keep.html"), "<p>manual</p>").unwrap();
|
||||||
|
// The CSS file → never pruned.
|
||||||
|
std::fs::write(html.join(&c.html.css_name), "body{}").unwrap();
|
||||||
|
|
||||||
|
let pruned = nuwiki_lsp::commands::export_ops::prune_orphan_html(&c);
|
||||||
|
assert_eq!(pruned.len(), 1, "only the orphan: {pruned:?}");
|
||||||
|
assert!(!html.join("Ghost.html").exists(), "orphan deleted");
|
||||||
|
assert!(html.join("Home.html").exists(), "sourced kept");
|
||||||
|
assert!(html.join("keep.html").exists(), "user_htmls kept");
|
||||||
|
assert!(html.join(&c.html.css_name).exists(), "css kept");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_rss_honours_rss_name_and_max_items() {
|
||||||
|
use nuwiki_lsp::diary::DiaryEntry;
|
||||||
|
use tower_lsp::lsp_types::Url;
|
||||||
|
let root = temp_root("rss_cap");
|
||||||
|
let mut c = WikiConfig::from_root(root.clone());
|
||||||
|
c.html = HtmlConfig::from_root(&root);
|
||||||
|
c.html.rss_name = "feed.xml".into();
|
||||||
|
c.html.rss_max_items = 2;
|
||||||
|
let entries: Vec<DiaryEntry> = (1..=5)
|
||||||
|
.map(|d| DiaryEntry {
|
||||||
|
date: DiaryDate::from_ymd(2026, 5, d).unwrap(),
|
||||||
|
uri: Url::parse(&format!("file:///tmp/diary/2026-05-0{d}.wiki")).unwrap(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let path = nuwiki_lsp::commands::export_ops::write_rss(&c, &entries).unwrap();
|
||||||
|
assert!(path.ends_with("feed.xml"), "rss_name: {path:?}");
|
||||||
|
let xml = std::fs::read_to_string(&path).unwrap();
|
||||||
|
// Capped at 2 items (newest first: 05-05, 05-04).
|
||||||
|
assert_eq!(xml.matches("<item>").count(), 2, "cap: {xml}");
|
||||||
|
assert!(xml.contains("2026-05-05") && xml.contains("2026-05-04"));
|
||||||
|
assert!(!xml.contains("2026-05-03"), "older items dropped: {xml}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_page_invokes_custom_wiki2html_converter() {
|
||||||
|
let root = temp_root("custom_w2h");
|
||||||
|
let mut c = WikiConfig::from_root(root.clone());
|
||||||
|
c.html = HtmlConfig::from_root(&root);
|
||||||
|
// Source file must exist on disk — the converter receives its path.
|
||||||
|
std::fs::write(root.join("Hello.wiki"), "= Hello =\nworld\n").unwrap();
|
||||||
|
// A tiny shell "converter": writes a marker into the output path it is told
|
||||||
|
// to produce. nuwiki appends args positionally after `_` ($0), so
|
||||||
|
// $1=force, $2=syntax, $3=ext, $4=output_dir, $5=input_file, …
|
||||||
|
let script = "mkdir -p \"$4\" && printf 'CUSTOM:%s' \"$5\" > \"$4\"/Hello.html";
|
||||||
|
c.html.custom_wiki2html = format!("sh -c {} _", shell_words_quote(script));
|
||||||
|
let outcome =
|
||||||
|
nuwiki_lsp::commands::export_ops::write_page(&parse("= Hello =\n"), "Hello", &c, true)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(outcome.page, "Hello");
|
||||||
|
assert!(
|
||||||
|
outcome.output_path.exists(),
|
||||||
|
"converter must produce output"
|
||||||
|
);
|
||||||
|
let body = std::fs::read_to_string(&outcome.output_path).unwrap();
|
||||||
|
assert!(
|
||||||
|
body.starts_with("CUSTOM:"),
|
||||||
|
"marker from converter, got {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal single-quote shell escaping for the test command above.
|
||||||
|
fn shell_words_quote(s: &str) -> String {
|
||||||
|
format!("'{}'", s.replace('\'', "'\\''"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== COMMANDS list completeness =====
|
// ===== COMMANDS list completeness =====
|
||||||
|
|||||||
@@ -152,6 +152,28 @@ fn v1_0_single_wiki_desugars_to_one_entry() {
|
|||||||
assert_eq!(cfg.wikis[0].syntax, "vimwiki");
|
assert_eq!(cfg.wikis[0].syntax, "vimwiki");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_wiki_shorthand_honours_top_level_per_wiki_keys() {
|
||||||
|
// Regression: a single-wiki user sets per-wiki keys at the top level
|
||||||
|
// (alongside wiki_root); they must reach the synthesized wiki, not just
|
||||||
|
// file_extension/syntax. Previously toc_header_level etc. were dropped.
|
||||||
|
let cfg = config_from_json(json!({
|
||||||
|
"wiki_root": "/tmp/vimwiki",
|
||||||
|
"toc_header": "Table of Contents",
|
||||||
|
"toc_header_level": 2,
|
||||||
|
"links_header_level": 3,
|
||||||
|
"auto_export": true,
|
||||||
|
"html_path": "/tmp/out",
|
||||||
|
}));
|
||||||
|
assert_eq!(cfg.wikis.len(), 1);
|
||||||
|
let w = &cfg.wikis[0];
|
||||||
|
assert_eq!(w.toc_header, "Table of Contents");
|
||||||
|
assert_eq!(w.toc_header_level, 2);
|
||||||
|
assert_eq!(w.links_header_level, 3);
|
||||||
|
assert!(w.html.auto_export);
|
||||||
|
assert_eq!(w.html.html_path, PathBuf::from("/tmp/out"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_wikis_list_overrides_legacy_shape() {
|
fn explicit_wikis_list_overrides_legacy_shape() {
|
||||||
let cfg = config_from_json(json!({
|
let cfg = config_from_json(json!({
|
||||||
@@ -267,14 +289,91 @@ fn defaults_carry_vimwiki_per_wiki_keys() {
|
|||||||
let cfg = WikiConfig::empty();
|
let cfg = WikiConfig::empty();
|
||||||
assert_eq!(cfg.index, "index");
|
assert_eq!(cfg.index, "index");
|
||||||
assert_eq!(cfg.diary_frequency, "daily");
|
assert_eq!(cfg.diary_frequency, "daily");
|
||||||
assert_eq!(cfg.diary_caption_level, 1);
|
assert_eq!(cfg.diary_caption_level, 0); // upstream default
|
||||||
assert_eq!(cfg.diary_sort, "desc");
|
assert_eq!(cfg.diary_sort, "desc");
|
||||||
assert_eq!(cfg.diary_header, "Diary");
|
assert_eq!(cfg.diary_header, "Diary");
|
||||||
|
// diary_months defaults to the 12 English month names.
|
||||||
|
assert_eq!(cfg.diary_months.len(), 12);
|
||||||
|
assert_eq!(cfg.diary_months[0], "January");
|
||||||
|
assert_eq!(cfg.diary_months[11], "December");
|
||||||
assert_eq!(cfg.listsyms, " .oOX");
|
assert_eq!(cfg.listsyms, " .oOX");
|
||||||
assert!(cfg.listsyms_propagate);
|
assert!(cfg.listsyms_propagate);
|
||||||
assert_eq!(cfg.list_margin, -1);
|
assert_eq!(cfg.list_margin, -1);
|
||||||
assert_eq!(cfg.links_space_char, " ");
|
assert_eq!(cfg.links_space_char, " ");
|
||||||
assert!(!cfg.auto_toc);
|
assert!(!cfg.auto_toc);
|
||||||
|
assert!(!cfg.auto_generate_links);
|
||||||
|
assert!(!cfg.auto_generate_tags);
|
||||||
|
assert!(!cfg.auto_diary_index);
|
||||||
|
// Generated-section captions match upstream defaults.
|
||||||
|
assert_eq!(cfg.toc_header, "Contents");
|
||||||
|
assert_eq!(cfg.toc_header_level, 1);
|
||||||
|
assert_eq!(cfg.links_header, "Generated Links");
|
||||||
|
assert_eq!(cfg.tags_header, "Generated Tags");
|
||||||
|
assert_eq!(cfg.listsym_rejected, "-");
|
||||||
|
// No external converter / public base URL by default.
|
||||||
|
assert_eq!(cfg.html.custom_wiki2html, "");
|
||||||
|
assert_eq!(cfg.html.custom_wiki2html_args, "");
|
||||||
|
assert_eq!(cfg.html.base_url, "");
|
||||||
|
// HTML section numbering is off by default (vimwiki's `0`).
|
||||||
|
assert_eq!(cfg.html.html_header_numbering, 0);
|
||||||
|
assert_eq!(cfg.html.html_header_numbering_sym, "");
|
||||||
|
// New config defaults match upstream.
|
||||||
|
assert!(cfg.create_link);
|
||||||
|
assert_eq!(cfg.dir_link, "");
|
||||||
|
assert_eq!(cfg.bullet_types, vec!["-", "*", "#"]);
|
||||||
|
assert!(!cfg.cycle_bullets);
|
||||||
|
assert!(!cfg.generated_links_caption);
|
||||||
|
assert_eq!(cfg.toc_link_format, 0);
|
||||||
|
assert!(!cfg.table_reduce_last_col);
|
||||||
|
assert_eq!(cfg.html.rss_name, "rss.xml");
|
||||||
|
assert_eq!(cfg.html.rss_max_items, 10);
|
||||||
|
assert!(cfg.html.list_ignore_newline);
|
||||||
|
assert!(cfg.html.text_ignore_newline);
|
||||||
|
assert!(cfg.html.emoji_enable);
|
||||||
|
assert!(cfg.html.user_htmls.is_empty());
|
||||||
|
assert!(cfg.html.valid_html_tags.contains(&"sub".to_string()));
|
||||||
|
// color_dic ships a populated palette (vimwiki seeds one).
|
||||||
|
assert!(cfg.html.color_dic.contains_key("red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raw_wiki_parses_config_batch_keys() {
|
||||||
|
let cfg = config_from_json(serde_json::json!({
|
||||||
|
"wikis": [{
|
||||||
|
"root": "/tmp/w",
|
||||||
|
"create_link": false,
|
||||||
|
"dir_link": "index",
|
||||||
|
"bullet_types": ["+", "-"],
|
||||||
|
"cycle_bullets": true,
|
||||||
|
"generated_links_caption": 1,
|
||||||
|
"toc_link_format": 1,
|
||||||
|
"table_reduce_last_col": true,
|
||||||
|
"rss_name": "feed.xml",
|
||||||
|
"rss_max_items": 5,
|
||||||
|
"valid_html_tags": "b,mark",
|
||||||
|
"list_ignore_newline": 0,
|
||||||
|
"text_ignore_newline": false,
|
||||||
|
"emoji_enable": 0,
|
||||||
|
"user_htmls": ["404.html"],
|
||||||
|
"color_tag_template": "<u>__CONTENT__</u>",
|
||||||
|
}],
|
||||||
|
}));
|
||||||
|
let w = &cfg.wikis[0];
|
||||||
|
assert!(!w.create_link);
|
||||||
|
assert_eq!(w.dir_link, "index");
|
||||||
|
assert_eq!(w.bullet_types, vec!["+", "-"]);
|
||||||
|
assert!(w.cycle_bullets);
|
||||||
|
assert!(w.generated_links_caption);
|
||||||
|
assert_eq!(w.toc_link_format, 1);
|
||||||
|
assert!(w.table_reduce_last_col);
|
||||||
|
assert_eq!(w.html.rss_name, "feed.xml");
|
||||||
|
assert_eq!(w.html.rss_max_items, 5);
|
||||||
|
assert_eq!(w.html.valid_html_tags, vec!["b", "mark"]);
|
||||||
|
assert!(!w.html.list_ignore_newline);
|
||||||
|
assert!(!w.html.text_ignore_newline);
|
||||||
|
assert!(!w.html.emoji_enable);
|
||||||
|
assert_eq!(w.html.user_htmls, vec!["404.html"]);
|
||||||
|
assert_eq!(w.html.color_tag_template, "<u>__CONTENT__</u>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -284,27 +383,113 @@ fn raw_wiki_parses_every_new_key() {
|
|||||||
"root": "/tmp/w",
|
"root": "/tmp/w",
|
||||||
"index": "home",
|
"index": "home",
|
||||||
"diary_frequency": "weekly",
|
"diary_frequency": "weekly",
|
||||||
|
"diary_weekly_style": "date",
|
||||||
|
"diary_start_week_day": "sunday",
|
||||||
"diary_caption_level": 2,
|
"diary_caption_level": 2,
|
||||||
"diary_sort": "asc",
|
"diary_sort": "asc",
|
||||||
"diary_header": "Journal",
|
"diary_header": "Journal",
|
||||||
|
"diary_months": ["Jan","Feb","Mar","Apr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],
|
||||||
"listsyms": "abcde",
|
"listsyms": "abcde",
|
||||||
|
"listsym_rejected": "✗",
|
||||||
"listsyms_propagate": false,
|
"listsyms_propagate": false,
|
||||||
"list_margin": 2,
|
"list_margin": 2,
|
||||||
"links_space_char": "_",
|
"links_space_char": "_",
|
||||||
"auto_toc": true,
|
"auto_toc": true,
|
||||||
|
"auto_generate_links": true,
|
||||||
|
"auto_generate_tags": 1,
|
||||||
|
"auto_diary_index": true,
|
||||||
|
"toc_header": "Table of Contents",
|
||||||
|
"toc_header_level": 2,
|
||||||
|
"links_header": "All Pages",
|
||||||
|
"tags_header": "Tag Index",
|
||||||
|
"tags_header_level": 3,
|
||||||
|
"custom_wiki2html": "~/bin/my_wiki2html.sh",
|
||||||
|
"custom_wiki2html_args": "--flag",
|
||||||
|
"base_url": "https://example.com/wiki/",
|
||||||
|
"html_header_numbering": 2,
|
||||||
|
"html_header_numbering_sym": ".",
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
let w = &cfg.wikis[0];
|
let w = &cfg.wikis[0];
|
||||||
assert_eq!(w.index, "home");
|
assert_eq!(w.index, "home");
|
||||||
assert_eq!(w.diary_frequency, "weekly");
|
assert_eq!(w.diary_frequency, "weekly");
|
||||||
|
assert_eq!(w.diary_weekly_style, "date");
|
||||||
|
assert_eq!(w.diary_start_week_day, "sunday");
|
||||||
assert_eq!(w.diary_caption_level, 2);
|
assert_eq!(w.diary_caption_level, 2);
|
||||||
assert_eq!(w.diary_sort, "asc");
|
assert_eq!(w.diary_sort, "asc");
|
||||||
assert_eq!(w.diary_header, "Journal");
|
assert_eq!(w.diary_header, "Journal");
|
||||||
|
assert_eq!(w.diary_months[4], "Mai");
|
||||||
|
assert_eq!(w.diary_months[11], "Dez");
|
||||||
assert_eq!(w.listsyms, "abcde");
|
assert_eq!(w.listsyms, "abcde");
|
||||||
|
assert_eq!(w.listsym_rejected, "✗");
|
||||||
|
assert_eq!(w.list_syms().rejected_glyph(), '✗');
|
||||||
assert!(!w.listsyms_propagate);
|
assert!(!w.listsyms_propagate);
|
||||||
assert_eq!(w.list_margin, 2);
|
assert_eq!(w.list_margin, 2);
|
||||||
assert_eq!(w.links_space_char, "_");
|
assert_eq!(w.links_space_char, "_");
|
||||||
assert!(w.auto_toc);
|
assert!(w.auto_toc);
|
||||||
|
assert!(w.auto_generate_links);
|
||||||
|
assert!(w.auto_generate_tags); // int 1 → true
|
||||||
|
assert!(w.auto_diary_index);
|
||||||
|
assert_eq!(w.toc_header, "Table of Contents");
|
||||||
|
assert_eq!(w.toc_header_level, 2);
|
||||||
|
assert_eq!(w.links_header, "All Pages");
|
||||||
|
assert_eq!(w.links_header_level, 1); // unspecified → default
|
||||||
|
assert_eq!(w.tags_header, "Tag Index");
|
||||||
|
assert_eq!(w.tags_header_level, 3);
|
||||||
|
// custom_wiki2html keeps the literal command string (tilde-expansion is
|
||||||
|
// the converter's job, not ours); args + base_url round-trip verbatim.
|
||||||
|
assert_eq!(w.html.custom_wiki2html, "~/bin/my_wiki2html.sh");
|
||||||
|
assert_eq!(w.html.custom_wiki2html_args, "--flag");
|
||||||
|
assert_eq!(w.html.base_url, "https://example.com/wiki/");
|
||||||
|
assert_eq!(w.html.html_header_numbering, 2);
|
||||||
|
assert_eq!(w.html.html_header_numbering_sym, ".");
|
||||||
|
|
||||||
|
// The parsed keys drive a date-mode, Sunday-start diary calendar:
|
||||||
|
// a weekly note is the week-start date (YYYY-MM-DD), stepping ±7 days.
|
||||||
|
use nuwiki_core::date::{DiaryDate, DiaryPeriod};
|
||||||
|
let cal = w.diary_calendar();
|
||||||
|
let wed = DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 27).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
cal.next(wed),
|
||||||
|
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 31).unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn markdown_wiki_derives_list_margin_zero_when_unset() {
|
||||||
|
// vimwiki sets list_margin = 0 for markdown wikis (vs -1 elsewhere) when
|
||||||
|
// the key is absent; an explicit value still wins.
|
||||||
|
let cfg = config_from_json(serde_json::json!({
|
||||||
|
"wikis": [{ "root": "/tmp/md", "syntax": "markdown" }],
|
||||||
|
}));
|
||||||
|
assert_eq!(cfg.wikis[0].list_margin, 0);
|
||||||
|
|
||||||
|
let vw = config_from_json(serde_json::json!({
|
||||||
|
"wikis": [{ "root": "/tmp/vw", "syntax": "vimwiki" }],
|
||||||
|
}));
|
||||||
|
assert_eq!(vw.wikis[0].list_margin, -1);
|
||||||
|
|
||||||
|
let explicit = config_from_json(serde_json::json!({
|
||||||
|
"wikis": [{ "root": "/tmp/md2", "syntax": "markdown", "list_margin": 4 }],
|
||||||
|
}));
|
||||||
|
assert_eq!(explicit.wikis[0].list_margin, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diary_caption_level_accepts_negative() {
|
||||||
|
// vimwiki allows diary_caption_level = -1 (min: -1); the field is i8 so it
|
||||||
|
// parses instead of failing deserialization.
|
||||||
|
let cfg = config_from_json(serde_json::json!({
|
||||||
|
"wikis": [{ "root": "/tmp/w", "diary_caption_level": -1 }],
|
||||||
|
}));
|
||||||
|
assert_eq!(cfg.wikis[0].diary_caption_level, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diary_calendar_defaults_to_iso_weeks() {
|
||||||
|
let cfg = WikiConfig::empty();
|
||||||
|
assert_eq!(cfg.diary_weekly_style, "iso");
|
||||||
|
assert_eq!(cfg.diary_start_week_day, "monday");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -489,3 +674,20 @@ fn legacy_single_wiki_shape_picks_up_defaults() {
|
|||||||
assert_eq!(cfg.wikis[0].index, "index");
|
assert_eq!(cfg.wikis[0].index, "index");
|
||||||
assert_eq!(cfg.wikis[0].diary_frequency, "daily");
|
assert_eq!(cfg.wikis[0].diary_frequency, "daily");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vimwiki_compat_payload_sets_toc_level_per_wiki() {
|
||||||
|
// The exact shape the Vim client emits when translating g:vimwiki_list +
|
||||||
|
// g:vimwiki_toc_header_level=2 (drop-in vimwiki compat).
|
||||||
|
let cfg = config_from_json(json!({
|
||||||
|
"wiki_root": "~/vimwiki",
|
||||||
|
"wikis": [
|
||||||
|
{ "root": "/tmp/personal", "toc_header_level": 2, "html_header_numbering": 2 },
|
||||||
|
{ "root": "/tmp/ifood", "toc_header_level": 2 },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
assert_eq!(cfg.wikis.len(), 2);
|
||||||
|
assert_eq!(cfg.wikis[0].toc_header_level, 2);
|
||||||
|
assert_eq!(cfg.wikis[0].html.html_header_numbering, 2);
|
||||||
|
assert_eq!(cfg.wikis[1].toc_header_level, 2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ fn build_toc_text_nests_by_level() {
|
|||||||
(2u8, "Sub".into(), "sub".into()),
|
(2u8, "Sub".into(), "sub".into()),
|
||||||
(1u8, "Other".into(), "other".into()),
|
(1u8, "Other".into(), "other".into()),
|
||||||
];
|
];
|
||||||
let out = ops::build_toc_text(&items, "Contents");
|
let out = ops::build_toc_text(&items, "Contents", 1, 0, 0);
|
||||||
assert!(out.starts_with("= Contents =\n"));
|
assert!(out.starts_with("= Contents =\n"));
|
||||||
let lines: Vec<&str> = out.lines().collect();
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
assert_eq!(lines[0], "= Contents =");
|
assert_eq!(lines[0], "= Contents =");
|
||||||
@@ -301,16 +301,141 @@ fn build_toc_text_nests_by_level() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_toc_text_with_no_headings_is_just_the_heading() {
|
fn build_toc_text_with_no_headings_is_just_the_heading() {
|
||||||
let out = ops::build_toc_text(&[], "Contents");
|
let out = ops::build_toc_text(&[], "Contents", 1, 0, 0);
|
||||||
assert_eq!(out, "= Contents =\n");
|
assert_eq!(out, "= Contents =\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toc_link_format_1_drops_the_description() {
|
||||||
|
// vimwiki toc_link_format: 1 = `[[#anchor]]` (no `|title`); 0 = with title.
|
||||||
|
let items = vec![(1u8, "Top".into(), "top".into())];
|
||||||
|
let f0 = ops::build_toc_text(&items, "Contents", 1, 0, 0);
|
||||||
|
assert!(f0.contains("- [[#top|Top]]"), "format 0: {f0}");
|
||||||
|
let f1 = ops::build_toc_text(&items, "Contents", 1, 0, 1);
|
||||||
|
assert!(f1.contains("- [[#top]]"), "format 1: {f1}");
|
||||||
|
assert!(!f1.contains("|Top"), "format 1 has no description: {f1}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_links_caption_emits_first_heading() {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
let pages = vec!["About".to_string(), "Notes".to_string()];
|
||||||
|
let mut caps = BTreeMap::new();
|
||||||
|
caps.insert("About".to_string(), "About Us".to_string());
|
||||||
|
// Notes has no caption → falls back to bare [[Notes]].
|
||||||
|
let out = ops::build_links_text(&pages, "Generated Links", 1, None, 0, Some(&caps));
|
||||||
|
assert!(out.contains("- [[About|About Us]]"), "captioned: {out}");
|
||||||
|
assert!(out.contains("- [[Notes]]"), "fallback: {out}");
|
||||||
|
// Without the caption map, both are bare.
|
||||||
|
let bare = ops::build_links_text(&pages, "Generated Links", 1, None, 0, None);
|
||||||
|
assert!(
|
||||||
|
bare.contains("- [[About]]") && !bare.contains("About Us"),
|
||||||
|
"bare: {bare}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn links_rebuild_edit_only_acts_when_section_present() {
|
||||||
|
let pages = vec!["Home".to_string(), "About".to_string()];
|
||||||
|
let uri = Url::from_file_path("/w/Home.wiki").unwrap();
|
||||||
|
let without = "= Home =\nbody\n";
|
||||||
|
assert!(
|
||||||
|
ops::links_rebuild_edit(
|
||||||
|
without,
|
||||||
|
&parse(without),
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.is_none(),
|
||||||
|
"must not insert a links section into a page that lacks one"
|
||||||
|
);
|
||||||
|
let with = "= Generated Links =\n- [[old]]\n";
|
||||||
|
assert!(ops::links_rebuild_edit(
|
||||||
|
with,
|
||||||
|
&parse(with),
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_section_range_stops_at_same_level_sibling() {
|
||||||
|
// A non-level-1 generated section (e.g. a level-2 tags index) must not
|
||||||
|
// swallow the following same-level section when regenerated.
|
||||||
|
let src = "== Foo ==\n- a\n== Bar ==\n- b\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let (start, end) = ops::find_section_range(&ast, "Foo").expect("section found");
|
||||||
|
let bar_off = src.find("== Bar ==").unwrap();
|
||||||
|
assert_eq!(start.offset, 0);
|
||||||
|
assert!(
|
||||||
|
end.offset <= bar_off,
|
||||||
|
"section absorbed the sibling: end={} bar={}",
|
||||||
|
end.offset,
|
||||||
|
bar_off
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captions_honour_custom_header_and_level() {
|
||||||
|
// toc_header="Table of Contents", level 2 → `== Table of Contents ==`.
|
||||||
|
let out = ops::build_toc_text(&[], "Table of Contents", 2, 0, 0);
|
||||||
|
assert_eq!(out, "== Table of Contents ==\n");
|
||||||
|
// links_header at level 3.
|
||||||
|
let pages = vec!["A".to_string(), "B".to_string()];
|
||||||
|
let links = ops::build_links_text(&pages, "All Pages", 3, None, 0, None);
|
||||||
|
assert!(links.starts_with("=== All Pages ===\n"));
|
||||||
|
// level clamps to 1..=6.
|
||||||
|
assert!(ops::build_toc_text(&[], "X", 9, 0, 0).starts_with("====== X ======"));
|
||||||
|
assert!(ops::build_toc_text(&[], "X", 0, 0, 0).starts_with("= X ="));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_margin_indents_generated_bullets_not_headings() {
|
||||||
|
// vimwiki `list_margin` = leading spaces before each generated bullet.
|
||||||
|
// The heading stays at column 0; bullets get `margin` spaces (TOC keeps
|
||||||
|
// its per-depth nesting *after* the base margin).
|
||||||
|
let pages = vec!["A".to_string(), "B".to_string()];
|
||||||
|
let links = ops::build_links_text(&pages, "Generated Links", 1, None, 2, None);
|
||||||
|
assert!(links.contains("\n - [[A]]\n"), "2-space margin: {links:?}");
|
||||||
|
assert!(
|
||||||
|
links.starts_with("= Generated Links =\n"),
|
||||||
|
"heading unindented: {links:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let toc = ops::build_toc_text(
|
||||||
|
&[
|
||||||
|
(1, "Top".into(), "top".into()),
|
||||||
|
(2, "Sub".into(), "sub".into()),
|
||||||
|
],
|
||||||
|
"Contents",
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
// Top bullet: 2-space margin; Sub bullet: margin + one nesting level.
|
||||||
|
assert!(toc.contains("\n - [[#top|Top]]\n"), "top margin: {toc:?}");
|
||||||
|
assert!(toc.contains("\n - [[#sub|Sub]]\n"), "nested: {toc:?}");
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Links text generation =====
|
// ===== Links text generation =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_links_text_excludes_current_page() {
|
fn build_links_text_excludes_current_page() {
|
||||||
let pages = vec!["A".to_string(), "Home".to_string(), "B".to_string()];
|
let pages = vec!["A".to_string(), "Home".to_string(), "B".to_string()];
|
||||||
let out = ops::build_links_text(&pages, "Generated Links", Some("Home"));
|
let out = ops::build_links_text(&pages, "Generated Links", 1, Some("Home"), 0, None);
|
||||||
let lines: Vec<&str> = out.lines().collect();
|
let lines: Vec<&str> = out.lines().collect();
|
||||||
assert_eq!(lines[0], "= Generated Links =");
|
assert_eq!(lines[0], "= Generated Links =");
|
||||||
assert!(lines.contains(&"- [[A]]"));
|
assert!(lines.contains(&"- [[A]]"));
|
||||||
@@ -321,7 +446,7 @@ fn build_links_text_excludes_current_page() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_links_text_no_excludes_includes_all() {
|
fn build_links_text_no_excludes_includes_all() {
|
||||||
let pages = vec!["A".to_string(), "B".to_string()];
|
let pages = vec!["A".to_string(), "B".to_string()];
|
||||||
let out = ops::build_links_text(&pages, "Generated Links", None);
|
let out = ops::build_links_text(&pages, "Generated Links", 1, None, 0, None);
|
||||||
assert!(out.contains("[[A]]"));
|
assert!(out.contains("[[A]]"));
|
||||||
assert!(out.contains("[[B]]"));
|
assert!(out.contains("[[B]]"));
|
||||||
}
|
}
|
||||||
@@ -359,7 +484,8 @@ fn toc_edit_inserts_at_top_when_no_existing_toc() {
|
|||||||
let src = "= One =\n== Two ==\n";
|
let src = "= One =\n== Two ==\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
let edit = ops::toc_edit(src, &ast, &uri, true).expect("got an edit");
|
let edit =
|
||||||
|
ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0, None).expect("got an edit");
|
||||||
let changes = edit.changes.expect("changes map");
|
let changes = edit.changes.expect("changes map");
|
||||||
let edits = &changes[&uri];
|
let edits = &changes[&uri];
|
||||||
assert_eq!(edits.len(), 1);
|
assert_eq!(edits.len(), 1);
|
||||||
@@ -367,7 +493,43 @@ fn toc_edit_inserts_at_top_when_no_existing_toc() {
|
|||||||
// Insertion at position 0,0 → zero-width range.
|
// Insertion at position 0,0 → zero-width range.
|
||||||
assert_eq!(te.range.start, te.range.end);
|
assert_eq!(te.range.start, te.range.end);
|
||||||
assert!(te.new_text.starts_with("= Contents =\n"));
|
assert!(te.new_text.starts_with("= Contents =\n"));
|
||||||
assert!(te.new_text.contains("[[#one|One]]"));
|
// Anchor is the raw heading text (vimwiki scheme), matching the heading id
|
||||||
|
// the HTML renderer emits on export.
|
||||||
|
assert!(te.new_text.contains("[[#One|One]]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toc_edit_inserts_at_cursor_line() {
|
||||||
|
// A fresh TOC goes at the cursor line (0-based) the client sends.
|
||||||
|
let src = "= One =\nbody text\n== Two ==\nmore\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
|
// Cursor on line 1 ("body text").
|
||||||
|
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0, Some(1)).expect("edit");
|
||||||
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
|
// Zero-width insert at line 1, column 0.
|
||||||
|
assert_eq!(te.range.start, te.range.end);
|
||||||
|
assert_eq!(te.range.start.line, 1);
|
||||||
|
assert_eq!(te.range.start.character, 0);
|
||||||
|
// A leading blank separates it from the preceding text.
|
||||||
|
assert!(
|
||||||
|
te.new_text.starts_with("\n= Contents ="),
|
||||||
|
"got: {:?}",
|
||||||
|
te.new_text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toc_edit_cursor_line_clamped_and_existing_replaced_in_place() {
|
||||||
|
// An existing TOC is replaced in place regardless of the cursor line.
|
||||||
|
let src = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
|
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0, Some(99)).expect("edit");
|
||||||
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
|
// Replacement starts at line 0 (the existing section), not the cursor.
|
||||||
|
assert_eq!(te.range.start.line, 0);
|
||||||
|
assert!(te.new_text.contains("[[#Real|Real]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -375,11 +537,12 @@ fn toc_edit_replaces_existing_toc() {
|
|||||||
let src = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
let src = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
let edit = ops::toc_edit(src, &ast, &uri, true).expect("got an edit");
|
let edit =
|
||||||
|
ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0, None).expect("got an edit");
|
||||||
let changes = edit.changes.expect("changes map");
|
let changes = edit.changes.expect("changes map");
|
||||||
let edits = &changes[&uri];
|
let edits = &changes[&uri];
|
||||||
let te = &edits[0];
|
let te = &edits[0];
|
||||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
assert!(te.new_text.contains("[[#Real|Real]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
// The replacement range must start at line 0.
|
// The replacement range must start at line 0.
|
||||||
assert_eq!(te.range.start.line, 0);
|
assert_eq!(te.range.start.line, 0);
|
||||||
@@ -390,7 +553,7 @@ fn toc_edit_returns_none_for_empty_doc() {
|
|||||||
let src = "";
|
let src = "";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/empty.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/empty.wiki").unwrap();
|
||||||
assert!(ops::toc_edit(src, &ast, &uri, true).is_none());
|
assert!(ops::toc_edit(src, &ast, &uri, true, "Contents", 1, 0, 0, None).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -401,16 +564,17 @@ fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
|||||||
let without = "= One =\n== Two ==\n";
|
let without = "= One =\n== Two ==\n";
|
||||||
let ast = parse(without);
|
let ast = parse(without);
|
||||||
assert!(
|
assert!(
|
||||||
ops::toc_rebuild_edit(without, &ast, &uri, true).is_none(),
|
ops::toc_rebuild_edit(without, &ast, &uri, true, "Contents", 1, 0, 0).is_none(),
|
||||||
"auto_toc should not insert a TOC where none existed"
|
"auto_toc should not insert a TOC where none existed"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Existing TOC → rebuild refreshes it.
|
// Existing TOC → rebuild refreshes it.
|
||||||
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||||
let ast = parse(with);
|
let ast = parse(with);
|
||||||
let edit = ops::toc_rebuild_edit(with, &ast, &uri, true).expect("rebuild edit");
|
let edit =
|
||||||
|
ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1, 0, 0).expect("rebuild edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
assert!(te.new_text.contains("[[#Real|Real]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,20 +586,77 @@ fn links_edit_inserts_when_section_absent() {
|
|||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
let pages = vec!["A".into(), "B".into(), "Home".into()];
|
let pages = vec!["A".into(), "B".into(), "Home".into()];
|
||||||
let edit = ops::links_edit(src, &ast, &uri, "Home", &pages, true).expect("edit");
|
let edit = ops::links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[A]]"));
|
assert!(te.new_text.contains("[[A]]"));
|
||||||
assert!(te.new_text.contains("[[B]]"));
|
assert!(te.new_text.contains("[[B]]"));
|
||||||
assert!(!te.new_text.contains("[[Home]]"));
|
assert!(!te.new_text.contains("[[Home]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn links_edit_inserts_at_cursor_line() {
|
||||||
|
// A fresh Generated Links section goes at the cursor line, not the EOF.
|
||||||
|
let src = "= Home =\nbody\nmore\n";
|
||||||
|
let ast = parse(src);
|
||||||
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
|
let pages = vec!["A".into()];
|
||||||
|
let edit = ops::links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
|
assert_eq!(te.range.start.line, 1);
|
||||||
|
assert_eq!(te.range.start.character, 0);
|
||||||
|
assert!(
|
||||||
|
te.new_text.starts_with("\n= Generated Links ="),
|
||||||
|
"got: {:?}",
|
||||||
|
te.new_text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn links_edit_replaces_when_section_present() {
|
fn links_edit_replaces_when_section_present() {
|
||||||
let src = "= Generated Links =\n- [[Stale]]\n\n= Real =\n";
|
let src = "= Generated Links =\n- [[Stale]]\n\n= Real =\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
let pages = vec!["Fresh".into()];
|
let pages = vec!["Fresh".into()];
|
||||||
let edit = ops::links_edit(src, &ast, &uri, "Home", &pages, true).expect("edit");
|
let edit = ops::links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&pages,
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("edit");
|
||||||
let te = &edit.changes.unwrap()[&uri][0];
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
assert!(te.new_text.contains("[[Fresh]]"));
|
assert!(te.new_text.contains("[[Fresh]]"));
|
||||||
assert!(!te.new_text.contains("Stale"));
|
assert!(!te.new_text.contains("Stale"));
|
||||||
@@ -447,7 +668,20 @@ fn links_edit_returns_none_for_empty_page_list() {
|
|||||||
let src = "Hi\n";
|
let src = "Hi\n";
|
||||||
let ast = parse(src);
|
let ast = parse(src);
|
||||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
assert!(ops::links_edit(src, &ast, &uri, "Home", &[], true).is_none());
|
assert!(ops::links_edit(
|
||||||
|
src,
|
||||||
|
&ast,
|
||||||
|
&uri,
|
||||||
|
"Home",
|
||||||
|
&[],
|
||||||
|
true,
|
||||||
|
"Generated Links",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== find_orphans =====
|
// ===== find_orphans =====
|
||||||
|
|||||||
@@ -201,6 +201,18 @@ fn find_inline_at_returns_wikilink_under_cursor() {
|
|||||||
assert!(matches!(node, InlineNode::WikiLink(_)));
|
assert!(matches!(node, InlineNode::WikiLink(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_inline_at_returns_wikilink_when_cursor_on_description() {
|
||||||
|
// Regression: pressing <CR> on the *title* part of a piped link used to
|
||||||
|
// descend into the description's Text node, so goto-definition saw a Text
|
||||||
|
// (not a WikiLink) and reported "No locations found".
|
||||||
|
let src = "see [[Other|My Title]] now\n";
|
||||||
|
let doc = parse(src);
|
||||||
|
// Byte col 16 is inside "Title" (the description), past the '|' at 11.
|
||||||
|
let node = find_inline_at(&doc, 0, 16).expect("wikilink at cursor");
|
||||||
|
assert!(matches!(node, InlineNode::WikiLink(_)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_inline_at_returns_none_for_plain_text() {
|
fn find_inline_at_returns_none_for_plain_text() {
|
||||||
let src = "just text\n";
|
let src = "just text\n";
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ One page per feature group:
|
|||||||
- :NuwikiGenerateLinks
|
- :NuwikiGenerateLinks
|
||||||
- :NuwikiCheckLinks
|
- :NuwikiCheckLinks
|
||||||
- :NuwikiMakeDiaryNote
|
- :NuwikiMakeDiaryNote
|
||||||
|
- :VimwikiColorize red — on a word in [[Syntax]] (or <Leader>wc)
|
||||||
|
|
||||||
:sample:smoke-test:sandbox:
|
:sample:smoke-test:sandbox:
|
||||||
EOF
|
EOF
|
||||||
@@ -92,6 +93,20 @@ Back to [[index]].
|
|||||||
*bold*, _italic_, *_bold italic_*, ~~strikethrough~~, `inline code`,
|
*bold*, _italic_, *_bold italic_*, ~~strikethrough~~, `inline code`,
|
||||||
super^script^ and sub,,script,,, and inline math $e^{i\pi} + 1 = 0$.
|
super^script^ and sub,,script,,, and inline math $e^{i\pi} + 1 = 0$.
|
||||||
|
|
||||||
|
== Colour spans (:VimwikiColorize) ==
|
||||||
|
|
||||||
|
Put the cursor on a word and run `:VimwikiColorize red` (or press
|
||||||
|
`<Leader>wc` and type a colour); or visually select a phrase and press
|
||||||
|
`<Leader>wc`. Each wraps the target in an inline colour-span tag whose
|
||||||
|
markup is concealed, so you see just the word painted in that colour (move
|
||||||
|
the cursor onto it to reveal the raw markup).
|
||||||
|
|
||||||
|
- colorize this important word
|
||||||
|
- now select and colour this whole phrase
|
||||||
|
- hex works too: paint deepskyblue on this token
|
||||||
|
|
||||||
|
Already colorized: <span style="color:#1e90ff">dodger blue</span>.
|
||||||
|
|
||||||
== Keywords ==
|
== Keywords ==
|
||||||
|
|
||||||
TODO STARTED FIXME XXX DONE FIXED STOPPED
|
TODO STARTED FIXME XXX DONE FIXED STOPPED
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ vim.g.nuwiki_diary_rel_path = 'diary'
|
|||||||
|
|
||||||
require('nuwiki').setup({
|
require('nuwiki').setup({
|
||||||
wiki_root = '$WIKI_DIR',
|
wiki_root = '$WIKI_DIR',
|
||||||
|
-- Exercise the per-wiki display settings via the global shorthand: these
|
||||||
|
-- fold into the single wiki, so :NuwikiTOC writes `== Contents ==` (level 2)
|
||||||
|
-- and HTML export numbers headings.
|
||||||
|
toc_header_level = 2,
|
||||||
|
html_header_numbering = 2,
|
||||||
|
html_header_numbering_sym = ' -',
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Surface log lines (the LSP logs via window/logMessage). Without this
|
-- Surface log lines (the LSP logs via window/logMessage). Without this
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
# NUWIKI_DEV_NO_SEED=1 ./development/start-vim-coc.sh # use WIKI_DIR as-is (no scratch seeding)
|
# NUWIKI_DEV_NO_SEED=1 ./development/start-vim-coc.sh # use WIKI_DIR as-is (no scratch seeding)
|
||||||
# NUWIKI_DEV_SKIP_BUILD=1 ./development/start-vim-coc.sh # reuse the cached binary
|
# NUWIKI_DEV_SKIP_BUILD=1 ./development/start-vim-coc.sh # reuse the cached binary
|
||||||
# NUWIKI_DEV_COC_JUMP=edit ./development/start-vim-coc.sh # change coc.preferences.jumpCommand
|
# NUWIKI_DEV_COC_JUMP=edit ./development/start-vim-coc.sh # change coc.preferences.jumpCommand
|
||||||
|
# NUWIKI_DEV_VIMWIKI=1 ./development/start-vim-coc.sh # drive nuwiki from a g:vimwiki_list config (drop-in)
|
||||||
#
|
#
|
||||||
# The release binary is rebuilt on every launch so source changes always reach
|
# The release binary is rebuilt on every launch so source changes always reach
|
||||||
# the running LSP. `cargo build --release` is incremental, so this is a fast
|
# the running LSP. `cargo build --release` is incremental, so this is a fast
|
||||||
@@ -72,42 +73,43 @@ ensure_coc() {
|
|||||||
|
|
||||||
write_coc_settings() {
|
write_coc_settings() {
|
||||||
mkdir -p "$DEV_DIR"
|
mkdir -p "$DEV_DIR"
|
||||||
# Mirror what the vim-lsp client sends (autoload/nuwiki/lsp.vim s:settings()):
|
# No `languageserver.nuwiki` block here on purpose: this dogfoods nuwiki's
|
||||||
# both `initializationOptions` (sent on initialize) and `settings.nuwiki`
|
# *programmatic* coc registration (autoload/nuwiki/lsp.vim → coc#config). The
|
||||||
# (served via workspace/configuration). Without these the server falls back
|
# plugin registers the server itself from g:nuwiki_* (set in the vimrc),
|
||||||
# to its own default wiki_root (~/vimwiki) instead of $WIKI_DIR, so it can't
|
# exactly the "no hand-written coc-settings.json" flow real users get. Only
|
||||||
# resolve links (e.g. Notes shows as broken) or create-on-follow correctly.
|
# the jump-command preference (the <CR>-opens-a-tab reproducer) lives here.
|
||||||
cat > "$COC_SETTINGS" <<EOF
|
cat > "$COC_SETTINGS" <<EOF
|
||||||
{
|
{
|
||||||
"coc.preferences.jumpCommand": "$COC_JUMP",
|
"coc.preferences.jumpCommand": "$COC_JUMP"
|
||||||
"languageserver": {
|
|
||||||
"nuwiki": {
|
|
||||||
"command": "$REPO_ROOT/bin/nuwiki-ls",
|
|
||||||
"filetypes": ["vimwiki"],
|
|
||||||
"initializationOptions": {
|
|
||||||
"wiki_root": "$WIKI_DIR",
|
|
||||||
"file_extension": ".wiki",
|
|
||||||
"syntax": "vimwiki",
|
|
||||||
"log_level": "info",
|
|
||||||
"diagnostic": { "link_severity": "warn" }
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"nuwiki": {
|
|
||||||
"wiki_root": "$WIKI_DIR",
|
|
||||||
"file_extension": ".wiki",
|
|
||||||
"syntax": "vimwiki",
|
|
||||||
"log_level": "info",
|
|
||||||
"diagnostic": { "link_severity": "warn" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
write_vimrc() {
|
write_vimrc() {
|
||||||
mkdir -p "$DEV_DIR"
|
mkdir -p "$DEV_DIR"
|
||||||
|
|
||||||
|
# Wiki config block. Default: native g:nuwiki_* (single wiki). With
|
||||||
|
# NUWIKI_DEV_VIMWIKI=1: an upstream g:vimwiki_list config instead, to dogfood
|
||||||
|
# the g:vimwiki_* drop-in translation (the path real vimwiki migrants use).
|
||||||
|
local WIKI_CFG
|
||||||
|
if [[ "${NUWIKI_DEV_VIMWIKI:-0}" == "1" ]]; then
|
||||||
|
log "NUWIKI_DEV_VIMWIKI=1 — driving nuwiki from an upstream g:vimwiki_list config"
|
||||||
|
WIKI_CFG="\" Upstream vimwiki config (drop-in). nuwiki translates these.
|
||||||
|
let g:vimwiki_toc_header_level = 2
|
||||||
|
let g:vimwiki_html_header_numbering = 2
|
||||||
|
let g:vimwiki_html_header_numbering_sym = ' -'
|
||||||
|
let g:vimwiki_list = [{'path': '${WIKI_DIR}', 'path_html': '${DEV_DIR}/html', 'ext': '.wiki'}]"
|
||||||
|
else
|
||||||
|
WIKI_CFG="let g:nuwiki_wiki_root = '${WIKI_DIR}'
|
||||||
|
let g:nuwiki_file_extension = '.wiki'
|
||||||
|
let g:nuwiki_diary_rel_path = 'diary'
|
||||||
|
\" Display settings exercise the global shorthand: :NuwikiTOC writes
|
||||||
|
\" \`== Contents ==\` (level 2) and HTML export numbers headings.
|
||||||
|
let g:nuwiki_toc_header_level = 2
|
||||||
|
let g:nuwiki_html_header_numbering = 2
|
||||||
|
let g:nuwiki_html_header_numbering_sym = ' -'"
|
||||||
|
fi
|
||||||
|
|
||||||
cat > "$VIMRC" <<EOF
|
cat > "$VIMRC" <<EOF
|
||||||
" start-vim-coc.sh: generated minimal config for nuwiki development (coc.nvim).
|
" start-vim-coc.sh: generated minimal config for nuwiki development (coc.nvim).
|
||||||
" Regenerated on every launch; edit start-vim-coc.sh, not this file.
|
" Regenerated on every launch; edit start-vim-coc.sh, not this file.
|
||||||
@@ -131,11 +133,11 @@ let g:coc_config_home = '${DEV_DIR}'
|
|||||||
let g:coc_data_home = '${COC_DATA}'
|
let g:coc_data_home = '${COC_DATA}'
|
||||||
|
|
||||||
let g:nuwiki_binary_path = '${REPO_ROOT}/bin/nuwiki-ls'
|
let g:nuwiki_binary_path = '${REPO_ROOT}/bin/nuwiki-ls'
|
||||||
let g:nuwiki_wiki_root = '${WIKI_DIR}'
|
|
||||||
let g:nuwiki_file_extension = '.wiki'
|
|
||||||
let g:nuwiki_syntax = 'vimwiki'
|
let g:nuwiki_syntax = 'vimwiki'
|
||||||
let g:nuwiki_diary_rel_path = 'diary'
|
|
||||||
let g:nuwiki_log_level = 'info'
|
let g:nuwiki_log_level = 'info'
|
||||||
|
" nuwiki auto-registers itself with coc from these globals (no coc-settings.json
|
||||||
|
" languageserver block).
|
||||||
|
${WIKI_CFG}
|
||||||
|
|
||||||
filetype plugin indent on
|
filetype plugin indent on
|
||||||
syntax enable
|
syntax enable
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ let g:nuwiki_wiki_root = '${WIKI_DIR}'
|
|||||||
let g:nuwiki_file_extension = '.wiki'
|
let g:nuwiki_file_extension = '.wiki'
|
||||||
let g:nuwiki_syntax = 'vimwiki'
|
let g:nuwiki_syntax = 'vimwiki'
|
||||||
let g:nuwiki_log_level = 'info'
|
let g:nuwiki_log_level = 'info'
|
||||||
|
" Exercise the per-wiki display settings via the global shorthand: these fold
|
||||||
|
" into the single wiki, so :NuwikiTOC writes `== Contents ==` (level 2) and
|
||||||
|
" HTML export numbers headings.
|
||||||
|
let g:nuwiki_toc_header_level = 2
|
||||||
|
let g:nuwiki_html_header_numbering = 2
|
||||||
|
let g:nuwiki_html_header_numbering_sym = ' -'
|
||||||
|
|
||||||
filetype plugin indent on
|
filetype plugin indent on
|
||||||
syntax enable
|
syntax enable
|
||||||
|
|||||||
@@ -89,6 +89,24 @@ call s:record(winnr('$') == s:wins_before - 1, 'action.window_count_decremented'
|
|||||||
call s:record(&filetype ==# 'vimwiki', 'action.sets_filetype',
|
call s:record(&filetype ==# 'vimwiki', 'action.sets_filetype',
|
||||||
\ 'filetype=' . &filetype)
|
\ 'filetype=' . &filetype)
|
||||||
|
|
||||||
|
" ===== calendar follows the current wiki (multi-wiki) =====
|
||||||
|
" Two wikis, each with its own diary entry. track_wiki() (driven by the
|
||||||
|
" BufEnter autocmd in real use) switches which wiki the calendar reads.
|
||||||
|
let s:wA = tempname() | call mkdir(s:wA . '/diary', 'p')
|
||||||
|
let s:wB = tempname() | call mkdir(s:wB . '/diary', 'p')
|
||||||
|
call writefile(['a'], s:wA . '/diary/2026-06-02.wiki')
|
||||||
|
call writefile(['b'], s:wB . '/diary/2026-06-10.wiki')
|
||||||
|
let g:nuwiki_wikis = [{'root': s:wA}, {'root': s:wB}]
|
||||||
|
|
||||||
|
call nuwiki#diary#track_wiki(s:wA . '/index.wiki')
|
||||||
|
call s:record(nuwiki#diary#calendar_sign(2, 6, 2026) ==# '*', 'track.wikiA_own_entry', '')
|
||||||
|
call s:record(nuwiki#diary#calendar_sign(10, 6, 2026) ==# '', 'track.wikiA_not_wikiB_entry', '')
|
||||||
|
|
||||||
|
call nuwiki#diary#track_wiki(s:wB . '/index.wiki')
|
||||||
|
call s:record(nuwiki#diary#calendar_sign(10, 6, 2026) ==# '*', 'track.wikiB_own_entry', '')
|
||||||
|
call s:record(nuwiki#diary#calendar_sign(2, 6, 2026) ==# '', 'track.wikiB_not_wikiA_entry', '')
|
||||||
|
unlet g:nuwiki_wikis
|
||||||
|
|
||||||
" ===== Wrap up =====
|
" ===== Wrap up =====
|
||||||
|
|
||||||
call add(s:results, '')
|
call add(s:results, '')
|
||||||
|
|||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# development/tests/test-coc-register-vim.sh — programmatic coc.nvim
|
||||||
|
# registration for the Vim client.
|
||||||
|
#
|
||||||
|
# nuwiki#lsp#start() registers the language server with coc via coc#config so
|
||||||
|
# users don't hand-maintain coc-settings.json. This harness stubs coc#config
|
||||||
|
# (a real autoload/coc.vim on the runtimepath, since Vim's exists('*coc#config')
|
||||||
|
# goes through the autoload mechanism) to record what nuwiki injects, then runs
|
||||||
|
# test-coc-register-vim.vim and asserts the payload.
|
||||||
|
#
|
||||||
|
# Exit code: 0 when every check passes, 1 on any FAIL or missing output.
|
||||||
|
# Skips (exit 0) when vim is not installed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m[coc-register]\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
|
if ! command -v vim >/dev/null 2>&1; then
|
||||||
|
log 'vim not installed — skipping'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="$(mktemp -d -t nuwiki-coc-test-XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
# Stub coc#config as a real autoload function so exists('*coc#config') resolves
|
||||||
|
# it. It records the last call into g:_recorded for the test to assert.
|
||||||
|
mkdir -p "$TMP/stub/autoload"
|
||||||
|
cat > "$TMP/stub/autoload/coc.vim" <<'EOF'
|
||||||
|
function! coc#config(section, value) abort
|
||||||
|
let g:_recorded = {'section': a:section, 'value': deepcopy(a:value)}
|
||||||
|
endfunction
|
||||||
|
EOF
|
||||||
|
|
||||||
|
OUT="$TMP/coc.out"
|
||||||
|
VIMRC="$TMP/vimrc"
|
||||||
|
cat > "$VIMRC" <<EOF
|
||||||
|
set nocompatible
|
||||||
|
" Stub dir first so its autoload/coc.vim is the one that resolves.
|
||||||
|
let &runtimepath = '$TMP/stub' . ',' . '$REPO_ROOT' . ',' . &runtimepath
|
||||||
|
" coc.nvim defines :CocConfig at startup; nuwiki uses that as the reliable
|
||||||
|
" 'coc is present' signal. Stub it so the coc branch is taken.
|
||||||
|
command! -nargs=0 CocConfig echo ''
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log 'running Vim coc-registration harness…'
|
||||||
|
NUWIKI_COC_OUT="$OUT" \
|
||||||
|
timeout 30 vim -e -s -u "$VIMRC" \
|
||||||
|
-c "source $REPO_ROOT/development/tests/test-coc-register-vim.vim" \
|
||||||
|
</dev/null >"$TMP/vim.log" 2>&1 || true
|
||||||
|
|
||||||
|
if [[ ! -f "$OUT" ]]; then
|
||||||
|
echo 'coc-registration harness produced no output' >&2
|
||||||
|
echo '--- vim log ---' >&2
|
||||||
|
cat "$TMP/vim.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$OUT"
|
||||||
|
PASSED="$(grep -c '^PASS ' "$OUT" || true)"
|
||||||
|
FAILED="$(grep -c '^FAIL ' "$OUT" || true)"
|
||||||
|
echo "SUMMARY: ${PASSED} passed, ${FAILED} failed"
|
||||||
|
|
||||||
|
if [[ "$FAILED" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log 'coc registration OK ✓'
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
" development/tests/test-coc-register-vim.vim — programmatic coc.nvim
|
||||||
|
" registration. Relies on a stub autoload/coc.vim (created by the wrapper) that
|
||||||
|
" records coc#config calls into g:_recorded. Drives nuwiki#lsp#start()'s coc
|
||||||
|
" branch and asserts the exact languageserver config nuwiki injects (command,
|
||||||
|
" filetypes, and the s:settings() payload with the global shorthand folded in).
|
||||||
|
" Writes PASS/FAIL to $NUWIKI_COC_OUT.
|
||||||
|
|
||||||
|
let s:out = []
|
||||||
|
function! s:check(desc, cond) abort
|
||||||
|
call add(s:out, (a:cond ? 'PASS ' : 'FAIL ') . a:desc)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Pretend coc's node service is already up so registration runs synchronously.
|
||||||
|
let g:coc_service_initialized = 1
|
||||||
|
let g:_recorded = {}
|
||||||
|
|
||||||
|
" Make sure the vim-lsp branch is NOT taken.
|
||||||
|
if exists('g:lsp_loaded') | unlet g:lsp_loaded | endif
|
||||||
|
|
||||||
|
" A readable, executable 'binary' so nuwiki#lsp#start() gets past its check.
|
||||||
|
let g:nuwiki_binary_path = executable('true') ? exepath('true') : '/usr/bin/true'
|
||||||
|
|
||||||
|
" Config: single-wiki shorthand + a global display setting that must fold into
|
||||||
|
" the wiki the server sees (the global shorthand).
|
||||||
|
let g:nuwiki_wiki_root = '/tmp/realwiki'
|
||||||
|
let g:nuwiki_toc_header_level = 2
|
||||||
|
|
||||||
|
call nuwiki#lsp#start()
|
||||||
|
|
||||||
|
let s:r = get(g:, '_recorded', {})
|
||||||
|
call s:check('coc#config was called', !empty(s:r))
|
||||||
|
call s:check('section is languageserver.nuwiki', get(s:r, 'section', '') ==# 'languageserver.nuwiki')
|
||||||
|
let s:v = get(s:r, 'value', {})
|
||||||
|
call s:check('command is the binary', get(s:v, 'command', '') ==# g:nuwiki_binary_path)
|
||||||
|
call s:check('filetypes = [vimwiki]', get(s:v, 'filetypes', []) == ['vimwiki'])
|
||||||
|
let s:io = get(s:v, 'initializationOptions', {})
|
||||||
|
call s:check('initOpts has wiki_root', get(s:io, 'wiki_root', '') ==# '/tmp/realwiki')
|
||||||
|
call s:check('global toc_header_level folded into payload', get(s:io, 'toc_header_level', 0) == 2)
|
||||||
|
call s:check('settings.nuwiki mirrors initOpts', get(get(s:v, 'settings', {}), 'nuwiki', {}) ==# s:io)
|
||||||
|
|
||||||
|
call writefile(s:out, $NUWIKI_COC_OUT)
|
||||||
|
qa!
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- development/tests/test-global-shorthand.lua — Neovim client global
|
||||||
|
-- shorthand: a per-wiki setting given once at the top level (setup() or
|
||||||
|
-- g:nuwiki_<key>) is folded into every wiki as a default, and a per-wiki
|
||||||
|
-- value overrides it. Mirror of the Vim test-vimwiki-compat checks.
|
||||||
|
--
|
||||||
|
-- Writes PASS/FAIL lines to $NUWIKI_GS_OUT; the wrapper fails on any FAIL.
|
||||||
|
|
||||||
|
local config = require('nuwiki.config')
|
||||||
|
local lsp = require('nuwiki.lsp')
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
local function check(desc, cond)
|
||||||
|
table.insert(out, (cond and 'PASS ' or 'FAIL ') .. desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ----- setup() top-level scalar folds into every wiki; per-wiki wins -----
|
||||||
|
config.apply({
|
||||||
|
toc_header_level = 2,
|
||||||
|
html_header_numbering = 2,
|
||||||
|
wikis = {
|
||||||
|
{ root = '/tmp/one', name = 'One' },
|
||||||
|
{ root = '/tmp/two', name = 'Two', toc_header_level = 3 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
local io1 = lsp.init_options()
|
||||||
|
check('w0 inherits global level', io1.wikis[1].toc_header_level == 2)
|
||||||
|
check('w0 inherits numbering', io1.wikis[1].html_header_numbering == 2)
|
||||||
|
check('w1 per-wiki override wins', io1.wikis[2].toc_header_level == 3)
|
||||||
|
check('w1 still inherits numbering', io1.wikis[2].html_header_numbering == 2)
|
||||||
|
|
||||||
|
-- The user's setup table must not be mutated by the fold.
|
||||||
|
check('no mutation of user wikis', config.user.wikis[1].toc_header_level == nil)
|
||||||
|
|
||||||
|
-- ----- built-in defaults are NOT folded (only explicit user values) -----
|
||||||
|
config.apply({
|
||||||
|
wikis = { { root = '/tmp/x', name = 'X' } },
|
||||||
|
})
|
||||||
|
local io2 = lsp.init_options()
|
||||||
|
check('default toc_header not folded', io2.wikis[1].toc_header == nil)
|
||||||
|
check('default level not folded', io2.wikis[1].toc_header_level == nil)
|
||||||
|
|
||||||
|
-- ----- g:nuwiki_<key> acts as a global too -----
|
||||||
|
config.apply({
|
||||||
|
wikis = { { root = '/tmp/y', name = 'Y' } },
|
||||||
|
})
|
||||||
|
vim.g.nuwiki_toc_header_level = 5
|
||||||
|
local io3 = lsp.init_options()
|
||||||
|
check('g:nuwiki_ global folds', io3.wikis[1].toc_header_level == 5)
|
||||||
|
vim.g.nuwiki_toc_header_level = nil
|
||||||
|
|
||||||
|
-- ----- upstream vim.g.vimwiki_list drop-in (the lazy.nvim vimwiki-swap case) --
|
||||||
|
config.apply({}) -- setup() with no native config
|
||||||
|
vim.g.vimwiki_list = {
|
||||||
|
{ path = '~/.vimwiki/personal', path_html = '~/public_html/personal', auto_export = 1 },
|
||||||
|
{ path = '~/.vimwiki/work' },
|
||||||
|
}
|
||||||
|
vim.g.vimwiki_toc_header_level = 2
|
||||||
|
local io4 = lsp.init_options()
|
||||||
|
check('vimwiki_list -> 2 wikis', io4.wikis ~= nil and #io4.wikis == 2)
|
||||||
|
check('path -> root', io4.wikis[1].root == '~/.vimwiki/personal')
|
||||||
|
check('path_html -> html_path', io4.wikis[1].html_path == '~/public_html/personal')
|
||||||
|
check('vimwiki global folds', io4.wikis[1].toc_header_level == 2)
|
||||||
|
check('w2 root', io4.wikis[2].root == '~/.vimwiki/work')
|
||||||
|
-- Buffer-side commands (<Leader>ww) resolve the same wikis.
|
||||||
|
check('wiki_list reads vimwiki_list', config.wiki_list()[1].root == '~/.vimwiki/personal')
|
||||||
|
-- Native g:nuwiki_wikis wins over g:vimwiki_list.
|
||||||
|
vim.g.nuwiki_wikis = { { root = '~/native', name = 'native' } }
|
||||||
|
local io5 = lsp.init_options()
|
||||||
|
check('native g:nuwiki_wikis wins', #io5.wikis == 1 and io5.wikis[1].root == '~/native')
|
||||||
|
vim.g.nuwiki_wikis = nil
|
||||||
|
vim.g.vimwiki_list = nil
|
||||||
|
vim.g.vimwiki_toc_header_level = nil
|
||||||
|
|
||||||
|
-- ----- setup({ wikis }) bridges the Lua config to VimL (g:nuwiki_wikis) -----
|
||||||
|
-- So buffer-side VimL helpers + the vimwiki-sync compat shim see the same
|
||||||
|
-- wikis as the LSP, even though setup() config lives only in Lua.
|
||||||
|
require('nuwiki').setup({ wikis = { { root = '/tmp/a', name = 'A' }, { root = '/tmp/b' } } })
|
||||||
|
check('setup() bridges to g:nuwiki_wikis',
|
||||||
|
vim.g.nuwiki_wikis ~= nil and #vim.g.nuwiki_wikis == 2)
|
||||||
|
check('bridged wiki root visible to VimL',
|
||||||
|
vim.g.nuwiki_wikis and vim.g.nuwiki_wikis[1].root == '/tmp/a')
|
||||||
|
vim.g.nuwiki_wikis = nil
|
||||||
|
|
||||||
|
local f = assert(io.open(os.getenv('NUWIKI_GS_OUT'), 'w'))
|
||||||
|
f:write(table.concat(out, '\n') .. '\n')
|
||||||
|
f:close()
|
||||||
|
vim.cmd('qa!')
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# development/tests/test-global-shorthand.sh — Neovim client global-shorthand
|
||||||
|
# folding (vimwiki-style per-wiki defaults). Runs test-global-shorthand.lua
|
||||||
|
# under headless Neovim; that script asserts the init_options payload folds a
|
||||||
|
# top-level / g:nuwiki_* setting into every wiki, lets a per-wiki value
|
||||||
|
# override it, and does NOT fold built-in defaults.
|
||||||
|
#
|
||||||
|
# Exit code: 0 when every check passes, 1 on any FAIL or missing output.
|
||||||
|
# Skips (exit 0) when nvim is not installed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m[global-shorthand]\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
|
if ! command -v nvim >/dev/null 2>&1; then
|
||||||
|
log 'nvim not installed — skipping'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="$(mktemp -d -t nuwiki-gs-test-XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
OUT="$TMP/gs.out"
|
||||||
|
INIT="$TMP/init.lua"
|
||||||
|
cat > "$INIT" <<EOF
|
||||||
|
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log 'running Neovim global-shorthand harness…'
|
||||||
|
NUWIKI_GS_OUT="$OUT" \
|
||||||
|
timeout 30 nvim --clean -u "$INIT" --headless \
|
||||||
|
-c "luafile $REPO_ROOT/development/tests/test-global-shorthand.lua" \
|
||||||
|
>"$TMP/nvim.log" 2>&1 || true
|
||||||
|
|
||||||
|
if [[ ! -f "$OUT" ]]; then
|
||||||
|
echo 'global-shorthand harness produced no output' >&2
|
||||||
|
echo '--- nvim log ---' >&2
|
||||||
|
cat "$TMP/nvim.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$OUT"
|
||||||
|
PASSED="$(grep -c '^PASS ' "$OUT" || true)"
|
||||||
|
FAILED="$(grep -c '^FAIL ' "$OUT" || true)"
|
||||||
|
echo "SUMMARY: ${PASSED} passed, ${FAILED} failed"
|
||||||
|
|
||||||
|
if [[ "$FAILED" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log 'global shorthand OK ✓'
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
" development/tests/test-keymaps-vim-prefix.vim — driven by
|
||||||
|
" development/tests/test-keymaps-vim.sh.
|
||||||
|
"
|
||||||
|
" Custom-prefix regression. The shell driver sets
|
||||||
|
" `g:nuwiki_map_prefix = '<Leader>n'` *before* the ftplugin loads, so this
|
||||||
|
" harness asserts the whole wiki command family relocates under `<Leader>n*`
|
||||||
|
" while nothing remains under the default `<Leader>w*`. Mirrors vimwiki's
|
||||||
|
" `g:vimwiki_map_prefix` and the Lua-side `map_prefix` option.
|
||||||
|
|
||||||
|
let s:results = []
|
||||||
|
let s:pass = 0
|
||||||
|
let s:fail = 0
|
||||||
|
let s:out = $NUWIKI_KEYMAP_RESULTS
|
||||||
|
|
||||||
|
function! s:record(ok, name, detail) abort
|
||||||
|
let l:line = (a:ok ? 'PASS' : 'FAIL') . ' ' . a:name
|
||||||
|
if a:detail !=# ''
|
||||||
|
let l:line .= ' — ' . a:detail
|
||||||
|
endif
|
||||||
|
call add(s:results, l:line)
|
||||||
|
if a:ok
|
||||||
|
let s:pass += 1
|
||||||
|
else
|
||||||
|
let s:fail += 1
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! s:has_map(lhs, mode) abort
|
||||||
|
let l:d = maparg(a:lhs, a:mode, 0, 1)
|
||||||
|
return !empty(l:d) && get(l:d, 'buffer', 0)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Every <prefix>* mapping must now live under <Leader>n (prefix = <Leader>n).
|
||||||
|
let s:relocated = [
|
||||||
|
\ ['<Leader>nw', 'n'], ['<Leader>nt', 'n'], ['<Leader>ns', 'n'], ['<Leader>ni', 'n'],
|
||||||
|
\ ['<Leader>n<Leader>w', 'n'], ['<Leader>n<Leader>y', 'n'], ['<Leader>n<Leader>t', 'n'],
|
||||||
|
\ ['<Leader>n<Leader>m', 'n'], ['<Leader>n<Leader>i', 'n'],
|
||||||
|
\ ['<Leader>nn', 'n'], ['<Leader>nd', 'n'], ['<Leader>nr', 'n'], ['<Leader>nc', 'n'],
|
||||||
|
\ ['<Leader>nh', 'n'], ['<Leader>nhh', 'n'], ['<Leader>nha', 'n'],
|
||||||
|
\ ]
|
||||||
|
for s:e in s:relocated
|
||||||
|
let s:ok = s:has_map(s:e[0], s:e[1])
|
||||||
|
call s:record(s:ok ? 1 : 0, 'prefix.relocated.' . s:e[1] . '.' . s:e[0],
|
||||||
|
\ s:ok ? '' : s:e[0] . ' not bound under custom prefix')
|
||||||
|
endfor
|
||||||
|
|
||||||
|
" The default <Leader>w* family must be GONE (relocated, not duplicated).
|
||||||
|
let s:gone = [
|
||||||
|
\ ['<Leader>ww', 'n'], ['<Leader>wr', 'n'], ['<Leader>wh', 'n'],
|
||||||
|
\ ['<Leader>w<Leader>w', 'n'],
|
||||||
|
\ ]
|
||||||
|
for s:e in s:gone
|
||||||
|
let s:ok = !s:has_map(s:e[0], s:e[1])
|
||||||
|
call s:record(s:ok ? 1 : 0, 'prefix.default_gone.' . s:e[1] . '.' . s:e[0],
|
||||||
|
\ s:ok ? '' : s:e[0] . ' still mapped despite custom prefix')
|
||||||
|
endfor
|
||||||
|
|
||||||
|
" Non-prefix maps (links group) are unaffected by the prefix change.
|
||||||
|
let s:cr = s:has_map('<CR>', 'n')
|
||||||
|
call s:record(s:cr ? 1 : 0, 'prefix.cr_unaffected', s:cr ? '' : '<CR> lost')
|
||||||
|
|
||||||
|
call add(s:results, '')
|
||||||
|
call add(s:results, printf('SUMMARY: %d passed, %d failed', s:pass, s:fail))
|
||||||
|
call writefile(s:results, s:out)
|
||||||
|
execute (s:fail == 0 ? 'qall!' : 'cquit!')
|
||||||
@@ -96,7 +96,41 @@ fi
|
|||||||
|
|
||||||
cat "$RESULTS_OPTOUT"
|
cat "$RESULTS_OPTOUT"
|
||||||
|
|
||||||
if grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$RESULTS_OPTOUT"; then
|
if ! grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$RESULTS_OPTOUT"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Custom map_prefix run =====
|
||||||
|
# Fresh Vim instance with g:nuwiki_map_prefix set *before* the ftplugin
|
||||||
|
# loads, so we can assert the whole wiki command family relocates under the
|
||||||
|
# custom prefix and nothing remains under the default <Leader>w*.
|
||||||
|
|
||||||
|
VIMRC_PREFIX="$TMP/vimrc-prefix"
|
||||||
|
cat > "$VIMRC_PREFIX" <<EOF
|
||||||
|
set nocompatible
|
||||||
|
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
|
||||||
|
let g:nuwiki_map_prefix = '<Leader>n'
|
||||||
|
filetype plugin indent on
|
||||||
|
syntax enable
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESULTS_PREFIX="$TMP/results-prefix.txt"
|
||||||
|
|
||||||
|
log "running custom-prefix harness…"
|
||||||
|
NUWIKI_KEYMAP_RESULTS="$RESULTS_PREFIX" \
|
||||||
|
timeout 30 vim -e -s -u "$VIMRC_PREFIX" -c "edit $SEED" -c "source $REPO_ROOT/development/tests/test-keymaps-vim-prefix.vim" \
|
||||||
|
</dev/null >"$TMP/vim-prefix.log" 2>&1 || true
|
||||||
|
|
||||||
|
if [[ ! -f "$RESULTS_PREFIX" ]]; then
|
||||||
|
echo 'keymap custom-prefix harness produced no output' >&2
|
||||||
|
echo '--- vim log ---' >&2
|
||||||
|
cat "$TMP/vim-prefix.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$RESULTS_PREFIX"
|
||||||
|
|
||||||
|
if grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$RESULTS_PREFIX"; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ let s:both_forms = [
|
|||||||
\ 'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
\ 'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
||||||
\ 'NormalizeLink', 'RenumberList', 'RenumberAllLists',
|
\ 'NormalizeLink', 'RenumberList', 'RenumberAllLists',
|
||||||
\ 'ListToggle', 'IncrementListItem', 'DecrementListItem',
|
\ 'ListToggle', 'IncrementListItem', 'DecrementListItem',
|
||||||
\ 'CatUrl',
|
\ 'CatUrl', 'ShowVersion', 'Return', 'Var', 'Search',
|
||||||
\ ]
|
\ ]
|
||||||
for s:suffix in s:both_forms
|
for s:suffix in s:both_forms
|
||||||
for s:prefix in ['Nuwiki', 'Vimwiki']
|
for s:prefix in ['Nuwiki', 'Vimwiki']
|
||||||
@@ -184,10 +184,13 @@ endfunction
|
|||||||
|
|
||||||
let s:mapping_surface = [
|
let s:mapping_surface = [
|
||||||
\ ['<CR>', 'n'], ['<S-CR>', 'n'], ['<C-CR>', 'n'], ['<C-S-CR>', 'n'],
|
\ ['<CR>', 'n'], ['<S-CR>', 'n'], ['<C-CR>', 'n'], ['<C-S-CR>', 'n'],
|
||||||
|
\ ['<M-CR>', 'n'], ['<D-CR>', 'n'],
|
||||||
\ ['<BS>', 'n'], ['<Tab>', 'n'], ['<S-Tab>', 'n'], ['+', 'nx'],
|
\ ['<BS>', 'n'], ['<Tab>', 'n'], ['<S-Tab>', 'n'], ['+', 'nx'],
|
||||||
|
\ ['<CR>', 'x'],
|
||||||
\ ['<C-Space>', 'nx'], ['<C-@>', 'nx'], ['<Nul>', 'nx'],
|
\ ['<C-Space>', 'nx'], ['<C-@>', 'nx'], ['<Nul>', 'nx'],
|
||||||
\ ['gnt', 'n'], ['gln', 'nx'], ['glp', 'nx'], ['glx', 'nx'],
|
\ ['gnt', 'n'], ['gln', 'nx'], ['glp', 'nx'], ['glx', 'nx'],
|
||||||
\ ['glh', 'n'], ['gll', 'n'], ['gLh', 'n'], ['gLl', 'n'],
|
\ ['glh', 'n'], ['gll', 'n'], ['gLh', 'n'], ['gLl', 'n'],
|
||||||
|
\ ['gLH', 'n'], ['gLL', 'n'], ['gLR', 'n'],
|
||||||
\ ['glr', 'n'], ['gLr', 'n'], ['gl', 'n'], ['gL', 'n'],
|
\ ['glr', 'n'], ['gLr', 'n'], ['gl', 'n'], ['gL', 'n'],
|
||||||
\ ['gl-', 'n'], ['gL-', 'n'], ['gl*', 'n'], ['gL*', 'n'],
|
\ ['gl-', 'n'], ['gL-', 'n'], ['gl*', 'n'], ['gL*', 'n'],
|
||||||
\ ['gl#', 'n'], ['gL#', 'n'], ['gl1', 'n'], ['gL1', 'n'],
|
\ ['gl#', 'n'], ['gL#', 'n'], ['gl1', 'n'], ['gL1', 'n'],
|
||||||
@@ -200,7 +203,8 @@ let s:mapping_surface = [
|
|||||||
\ ['<A-Left>', 'n'], ['<A-Right>', 'n'],
|
\ ['<A-Left>', 'n'], ['<A-Right>', 'n'],
|
||||||
\ ['<Leader>ww', 'n'], ['<Leader>ws', 'n'], ['<Leader>wi', 'n'],
|
\ ['<Leader>ww', 'n'], ['<Leader>ws', 'n'], ['<Leader>wi', 'n'],
|
||||||
\ ['<Leader>w<Leader>w', 'n'], ['<Leader>w<Leader>y', 'n'],
|
\ ['<Leader>w<Leader>w', 'n'], ['<Leader>w<Leader>y', 'n'],
|
||||||
\ ['<Leader>w<Leader>t', 'n'], ['<Leader>w<Leader>i', 'n'],
|
\ ['<Leader>w<Leader>t', 'n'], ['<Leader>w<Leader>m', 'n'],
|
||||||
|
\ ['<Leader>w<Leader>i', 'n'],
|
||||||
\ ['<Leader>wn', 'n'], ['<Leader>wd', 'n'], ['<Leader>wr', 'n'],
|
\ ['<Leader>wn', 'n'], ['<Leader>wd', 'n'], ['<Leader>wr', 'n'],
|
||||||
\ ['<Leader>wh', 'n'], ['<Leader>whh', 'n'], ['<Leader>wha', 'n'],
|
\ ['<Leader>wh', 'n'], ['<Leader>whh', 'n'], ['<Leader>wha', 'n'],
|
||||||
\ ['<Leader>wc', 'nx'], ['<C-Down>', 'n'], ['<C-Up>', 'n'],
|
\ ['<Leader>wc', 'nx'], ['<C-Down>', 'n'], ['<C-Up>', 'n'],
|
||||||
@@ -209,7 +213,7 @@ let s:mapping_surface = [
|
|||||||
\ ['ah', 'ox'], ['ih', 'ox'], ['aH', 'ox'], ['iH', 'ox'],
|
\ ['ah', 'ox'], ['ih', 'ox'], ['aH', 'ox'], ['iH', 'ox'],
|
||||||
\ ['al', 'ox'], ['il', 'ox'], ['a\', 'ox'], ['i\', 'ox'],
|
\ ['al', 'ox'], ['il', 'ox'], ['a\', 'ox'], ['i\', 'ox'],
|
||||||
\ ['ac', 'ox'], ['ic', 'ox'],
|
\ ['ac', 'ox'], ['ic', 'ox'],
|
||||||
\ ['<CR>', 'i'], ['<Tab>', 'i'], ['<S-Tab>', 'i'],
|
\ ['<CR>', 'i'], ['<S-CR>', 'i'], ['<Tab>', 'i'], ['<S-Tab>', 'i'],
|
||||||
\ ['<C-D>', 'i'], ['<C-T>', 'i'],
|
\ ['<C-D>', 'i'], ['<C-T>', 'i'],
|
||||||
\ ['<C-L><C-J>', 'i'], ['<C-L><C-K>', 'i'], ['<C-L><C-M>', 'i'],
|
\ ['<C-L><C-J>', 'i'], ['<C-L><C-K>', 'i'], ['<C-L><C-M>', 'i'],
|
||||||
\ ]
|
\ ]
|
||||||
@@ -221,6 +225,18 @@ for s:entry in s:mapping_surface
|
|||||||
endfor
|
endfor
|
||||||
endfor
|
endfor
|
||||||
|
|
||||||
|
" ===== Diary <Leader>w<Leader>{t,m} target (collision fix) =====
|
||||||
|
" Upstream: <Leader>w<Leader>t = today in a new tab, <Leader>w<Leader>m =
|
||||||
|
" tomorrow. nuwiki used to bind both to tomorrow.
|
||||||
|
call s:record(
|
||||||
|
\ maparg('<Leader>w<Leader>t', 'n') =~# 'diary_today_tab' ? 1 : 0,
|
||||||
|
\ 'diary.leader_t_opens_today_in_tab',
|
||||||
|
\ 'rhs=' . maparg('<Leader>w<Leader>t', 'n'))
|
||||||
|
call s:record(
|
||||||
|
\ maparg('<Leader>w<Leader>m', 'n') =~# 'diary_tomorrow' ? 1 : 0,
|
||||||
|
\ 'diary.leader_m_opens_tomorrow',
|
||||||
|
\ 'rhs=' . maparg('<Leader>w<Leader>m', 'n'))
|
||||||
|
|
||||||
" ===== Header nav (pure VimL) =====
|
" ===== Header nav (pure VimL) =====
|
||||||
|
|
||||||
call s:run('headers.]]_jumps_to_next', {
|
call s:run('headers.]]_jumps_to_next', {
|
||||||
@@ -320,6 +336,20 @@ 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 | |'],
|
||||||
\ })
|
\ })
|
||||||
|
" 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', {
|
||||||
|
\ 'lines': ['- item'],
|
||||||
|
\ 'cursor': [1, 6],
|
||||||
|
\ 'keys': "A\<S-CR>x\<Esc>",
|
||||||
|
\ 'expect_lines': ['- item', ' x'],
|
||||||
|
\ })
|
||||||
|
call s:run('cr.shift_cr_multiline_checkbox', {
|
||||||
|
\ 'lines': ['- [ ] item'],
|
||||||
|
\ 'cursor': [1, 10],
|
||||||
|
\ 'keys': "A\<S-CR>x\<Esc>",
|
||||||
|
\ 'expect_lines': ['- [ ] item', ' x'],
|
||||||
|
\ })
|
||||||
|
|
||||||
" ===== Insert-mode smart_tab / smart_shift_tab (Cluster 6) =====
|
" ===== Insert-mode smart_tab / smart_shift_tab (Cluster 6) =====
|
||||||
|
|
||||||
@@ -477,6 +507,112 @@ call s:record(
|
|||||||
\ 'ext=' . get(get(s:wl, 1, {}), 'ext', ''))
|
\ 'ext=' . get(get(s:wl, 1, {}), 'ext', ''))
|
||||||
unlet g:nuwiki_wikis
|
unlet g:nuwiki_wikis
|
||||||
|
|
||||||
|
" ===== Colorize (cword, visual selection, ranged command) =====
|
||||||
|
" The command is -nargs=1 -range: normal invocation wraps the cword; a visual
|
||||||
|
" range (`:'<,'>VimwikiColorize` / the x-mode <Leader>wc map) wraps the
|
||||||
|
" selection. Run these while still on the vimwiki buffer (the buffer-local
|
||||||
|
" command must be defined).
|
||||||
|
call s:set_buf(['word here'])
|
||||||
|
call cursor(1, 1)
|
||||||
|
silent VimwikiColorize red
|
||||||
|
call s:record(
|
||||||
|
\ getline(1) ==# '<span style="color:red">word</span> here' ? 1 : 0,
|
||||||
|
\ 'colorize.command_wraps_cword',
|
||||||
|
\ 'got ' . string(getline(1)))
|
||||||
|
|
||||||
|
" Visual selection → wraps the selection (visual flag passed explicitly).
|
||||||
|
call s:set_buf(['one two three'])
|
||||||
|
call cursor(1, 5)
|
||||||
|
call feedkeys("viw\<Esc>", 'tx')
|
||||||
|
call nuwiki#commands#colorize('blue', 2)
|
||||||
|
call s:record(
|
||||||
|
\ getline(1) ==# 'one <span style="color:blue">two</span> three' ? 1 : 0,
|
||||||
|
\ 'colorize.visual_wraps_selection',
|
||||||
|
\ 'got ' . string(getline(1)))
|
||||||
|
|
||||||
|
" Ranged command must not raise E481 and wraps the selection.
|
||||||
|
call s:set_buf(['alpha beta gamma'])
|
||||||
|
call cursor(1, 8)
|
||||||
|
call feedkeys("viw\<Esc>", 'tx')
|
||||||
|
let s:cz_err = ''
|
||||||
|
try
|
||||||
|
silent '<,'>VimwikiColorize green
|
||||||
|
catch
|
||||||
|
let s:cz_err = v:exception
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:cz_err ==# '' && getline(1) ==# 'alpha <span style="color:green">beta</span> gamma' ? 1 : 0,
|
||||||
|
\ 'colorize.ranged_command_no_error',
|
||||||
|
\ 'err=' . s:cz_err . ' got ' . string(getline(1)))
|
||||||
|
|
||||||
|
" Colour spans are concealed down to their text. Put the span on a non-cursor
|
||||||
|
" line so concealcursor (=c) doesn't reveal it, then refresh + inspect.
|
||||||
|
call s:set_buf(['plain line', '<span style="color:red">word</span> tail'])
|
||||||
|
call cursor(1, 1)
|
||||||
|
call nuwiki#colors#refresh()
|
||||||
|
let s:cz_l = getline(2)
|
||||||
|
let s:cz_open = synconcealed(2, 1)[0]
|
||||||
|
let s:cz_word = synconcealed(2, stridx(s:cz_l, 'word') + 1)
|
||||||
|
let s:cz_close = synconcealed(2, stridx(s:cz_l, '</span>') + 1)[0]
|
||||||
|
call s:record(
|
||||||
|
\ s:cz_open == 1 && s:cz_close == 1 && s:cz_word[0] == 0
|
||||||
|
\ && synIDattr(synID(2, stridx(s:cz_l, 'word') + 1, 1), 'name') ==# 'nuwikiColorSpan_red' ? 1 : 0,
|
||||||
|
\ 'colorize.conceal_hides_tags_shows_text',
|
||||||
|
\ 'open=' . s:cz_open . ' close=' . s:cz_close . ' word=' . s:cz_word[0]
|
||||||
|
\ . ' grp=' . synIDattr(synID(2, stridx(s:cz_l, 'word') + 1, 1), 'name'))
|
||||||
|
|
||||||
|
" Running the command conceals the new span immediately (colorize refreshes;
|
||||||
|
" no manual step). Move the cursor off the line so concealcursor reveals nothing.
|
||||||
|
call s:set_buf(['target word', 'other line'])
|
||||||
|
call cursor(1, 1)
|
||||||
|
silent VimwikiColorize teal
|
||||||
|
call cursor(2, 1)
|
||||||
|
let s:cz_l2 = getline(1)
|
||||||
|
call s:record(
|
||||||
|
\ synconcealed(1, 1)[0] == 1
|
||||||
|
\ && synIDattr(synID(1, stridx(s:cz_l2, 'target') + 1, 1), 'name') ==# 'nuwikiColorSpan_teal' ? 1 : 0,
|
||||||
|
\ 'colorize.command_conceals_immediately',
|
||||||
|
\ 'concealed=' . synconcealed(1, 1)[0]
|
||||||
|
\ . ' grp=' . synIDattr(synID(1, stridx(s:cz_l2, 'target') + 1, 1), 'name'))
|
||||||
|
|
||||||
|
" A span whose colour Vim can't allocate (rgb()/prose) must be skipped — not
|
||||||
|
" abort refresh, not conceal — while a real span afterwards still conceals.
|
||||||
|
call s:set_buf(['plain', 'x <span style="color:rgb(1,2,3)">y</span> z', '<span style="color:red">word</span> z'])
|
||||||
|
call cursor(1, 1)
|
||||||
|
let s:cz_e = ''
|
||||||
|
try
|
||||||
|
call nuwiki#colors#refresh()
|
||||||
|
catch
|
||||||
|
let s:cz_e = v:exception
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:cz_e ==# ''
|
||||||
|
\ && synconcealed(3, 1)[0] == 1
|
||||||
|
\ && synconcealed(2, stridx(getline(2), '<span') + 1)[0] == 0 ? 1 : 0,
|
||||||
|
\ 'colorize.skips_unallocatable_colour',
|
||||||
|
\ 'err=' . s:cz_e . ' real=' . synconcealed(3, 1)[0]
|
||||||
|
\ . ' rgb=' . synconcealed(2, stridx(getline(2), '<span') + 1)[0])
|
||||||
|
|
||||||
|
" ===== NormalizeLink visual wraps the selection (pure-VimL) =====
|
||||||
|
" `:VimwikiNormalizeLink 1` / the x-mode `+` wrap the `'<`/`'>` selection.
|
||||||
|
call s:set_buf(['one two three'])
|
||||||
|
call cursor(1, 5)
|
||||||
|
call feedkeys("viw\<Esc>", 'tx')
|
||||||
|
call nuwiki#commands#normalize_link(1)
|
||||||
|
call s:record(
|
||||||
|
\ getline(1) ==# 'one [[two]] three' ? 1 : 0,
|
||||||
|
\ 'normalize.visual_wraps_selection',
|
||||||
|
\ 'got ' . string(getline(1)))
|
||||||
|
|
||||||
|
" ===== Visual <CR> mapping wraps the selection (== visual `+`) =====
|
||||||
|
call s:set_buf(['alpha beta gamma'])
|
||||||
|
call cursor(1, 7)
|
||||||
|
call feedkeys("viw\<CR>", 'tx')
|
||||||
|
call s:record(
|
||||||
|
\ getline(1) ==# 'alpha [[beta]] gamma' ? 1 : 0,
|
||||||
|
\ 'map.visual_cr_wraps_selection',
|
||||||
|
\ 'got ' . string(getline(1)))
|
||||||
|
|
||||||
" ===== Follow-link resolves + opens in the current window (regression) =====
|
" ===== Follow-link resolves + opens in the current window (regression) =====
|
||||||
" follow_link_or_create() must resolve the definition itself and `:edit` the
|
" follow_link_or_create() must resolve the definition itself and `:edit` the
|
||||||
" target in the CURRENT window — including a page that does not exist yet
|
" target in the CURRENT window — including a page that does not exist yet
|
||||||
@@ -521,6 +657,263 @@ unlet g:_nuwiki_target
|
|||||||
" follow_link_drop() request mechanism; its end-to-end coverage is the manual
|
" follow_link_drop() request mechanism; its end-to-end coverage is the manual
|
||||||
" development/start-vim.sh check.
|
" development/start-vim.sh check.
|
||||||
|
|
||||||
|
" ===== :VimwikiTable -nargs=* (cols+rows) =====
|
||||||
|
" Was -nargs=1, which raised E488 on `:VimwikiTable 4 3`. The table insert
|
||||||
|
" itself is an LSP roundtrip (no server in this headless harness), so we only
|
||||||
|
" assert the arg parse succeeds — i.e. no E488 trailing-characters error.
|
||||||
|
call s:set_buf([''])
|
||||||
|
call cursor(1, 1)
|
||||||
|
let s:tbl_err = ''
|
||||||
|
try
|
||||||
|
silent VimwikiTable 4 3
|
||||||
|
catch /E488/
|
||||||
|
let s:tbl_err = v:exception
|
||||||
|
catch
|
||||||
|
" A non-E488 error (e.g. no LSP attached) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:tbl_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiTable_accepts_cols_rows',
|
||||||
|
\ 'err=' . s:tbl_err)
|
||||||
|
|
||||||
|
" ===== :VimwikiListChangeLvl -range -nargs=+ =====
|
||||||
|
" Was -nargs=? (range-less): `:1,3VimwikiListChangeLvl increase 0` raised E481
|
||||||
|
" (no range allowed) and/or E488 (trailing chars). The level change is an LSP
|
||||||
|
" roundtrip (no server here), so assert only the attribute parse: no E481/E488.
|
||||||
|
call s:set_buf(['- a', '- b', '- c'])
|
||||||
|
let s:lcl_err = ''
|
||||||
|
try
|
||||||
|
silent 1,3VimwikiListChangeLvl increase 0
|
||||||
|
catch /E481\|E488/
|
||||||
|
let s:lcl_err = v:exception
|
||||||
|
catch
|
||||||
|
" non-attribute error (e.g. no LSP) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:lcl_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiListChangeLvl_accepts_range_and_args',
|
||||||
|
\ 'err=' . s:lcl_err)
|
||||||
|
|
||||||
|
" ===== :VimwikiSearch / VWS -nargs=* + :VimwikiTableAlignQ/W -nargs=? =====
|
||||||
|
" Upstream attributes: Search/VWS take -nargs=* (was -nargs=1, so a multi-word
|
||||||
|
" `:VimwikiSearch foo bar` raised E488); TableAlignQ/W take -nargs=? (were bare,
|
||||||
|
" so `:VimwikiTableAlignQ 2` raised E488). lvimgrep/align run for real here, so
|
||||||
|
" swallow any non-attribute error and assert only that E488 is gone.
|
||||||
|
let s:attr_err = ''
|
||||||
|
try
|
||||||
|
silent! VimwikiSearch foo bar
|
||||||
|
silent! VWS foo bar
|
||||||
|
silent! VimwikiTableAlignQ 2
|
||||||
|
silent! VimwikiTableAlignW 2
|
||||||
|
silent! NuwikiTableAlign 2
|
||||||
|
catch /E488/
|
||||||
|
let s:attr_err = v:exception
|
||||||
|
catch
|
||||||
|
" runtime errors (no match, empty table, …) are expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:attr_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.search_and_tablealign_nargs',
|
||||||
|
\ 'err=' . s:attr_err)
|
||||||
|
|
||||||
|
" A no-match search must be caught (friendly message), not raise a raw E480.
|
||||||
|
let s:nm_err = ''
|
||||||
|
try
|
||||||
|
VimwikiSearch zzznomatchxyzqqq
|
||||||
|
catch /E480/
|
||||||
|
let s:nm_err = v:exception
|
||||||
|
catch
|
||||||
|
" other errors (no wiki files, etc.) are fine — only E480 is the regression.
|
||||||
|
endtry
|
||||||
|
call s:record(s:nm_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiSearch_no_match_graceful', 'err=' . s:nm_err)
|
||||||
|
|
||||||
|
" ===== auto_header inserts `= Stem =` on a new wiki page; skips the index =====
|
||||||
|
let s:ah_root = fnamemodify(expand(nuwiki#commands#wiki_list()[0].root), ':p')
|
||||||
|
if s:ah_root[len(s:ah_root)-1:] !=# '/' | let s:ah_root .= '/' | endif
|
||||||
|
new
|
||||||
|
execute 'file ' . fnameescape('/tmp/nuwiki-ah-plain/MyNewPage.wiki')
|
||||||
|
call nuwiki#commands#auto_header()
|
||||||
|
call s:record(getline(1) ==# '= MyNewPage =' ? 1 : 0,
|
||||||
|
\ 'cmd.auto_header_inserts', 'line1=' . getline(1))
|
||||||
|
bwipeout!
|
||||||
|
new
|
||||||
|
execute 'file ' . fnameescape(s:ah_root . 'index.wiki')
|
||||||
|
call nuwiki#commands#auto_header()
|
||||||
|
call s:record(getline(1) ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.auto_header_skips_index', 'line1=' . getline(1))
|
||||||
|
bwipeout!
|
||||||
|
unlet s:ah_root
|
||||||
|
|
||||||
|
" ===== :VimwikiGoto -nargs=* =====
|
||||||
|
" Was -nargs=1, so a page name with a space raised E488 (and the empty-arg
|
||||||
|
" prompt fallback was unreachable). Now -nargs=*: a multi-word arg must parse.
|
||||||
|
" The goto is an LSP roundtrip (no server here); swallow non-attribute errors.
|
||||||
|
let s:goto_err = ''
|
||||||
|
try
|
||||||
|
silent! VimwikiGoto My Page
|
||||||
|
catch /E471\|E488/
|
||||||
|
let s:goto_err = v:exception
|
||||||
|
catch
|
||||||
|
" no-LSP / not-found errors are expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:goto_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiGoto_accepts_multiword',
|
||||||
|
\ 'err=' . s:goto_err)
|
||||||
|
|
||||||
|
" ===== :VimwikiVar get/set =====
|
||||||
|
" `:VimwikiVar key val` sets g:nuwiki_<key>; bare/get forms just echo.
|
||||||
|
silent VimwikiVar testkey hello world
|
||||||
|
call s:record(get(g:, 'nuwiki_testkey', '') ==# 'hello world' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiVar_sets_global',
|
||||||
|
\ 'g:nuwiki_testkey=' . get(g:, 'nuwiki_testkey', '(unset)'))
|
||||||
|
|
||||||
|
" ===== global entry points work outside a wiki buffer =====
|
||||||
|
" Index / TabIndex / Var / ShowVersion are defined globally (upstream defines
|
||||||
|
" the Index family in plugin/). Open a throwaway non-wiki buffer and confirm.
|
||||||
|
new
|
||||||
|
setlocal buftype=nofile
|
||||||
|
let s:glob_ok = exists(':VimwikiIndex') == 2 && exists(':VimwikiTabIndex') == 2
|
||||||
|
\ && exists(':VimwikiVar') == 2 && exists(':VimwikiShowVersion') == 2
|
||||||
|
call s:record(s:glob_ok ? 1 : 0, 'cmd.global_entry_points',
|
||||||
|
\ 'Index/TabIndex/Var/ShowVersion visible outside a wiki buffer')
|
||||||
|
bwipeout!
|
||||||
|
|
||||||
|
" ===== autowriteall / table_auto_fmt (Vim path) =====
|
||||||
|
" autowriteall (default on) sets Vim's global &autowriteall on the wiki buffer.
|
||||||
|
call s:record(&autowriteall ? 1 : 0, 'config.autowriteall_applied',
|
||||||
|
\ '&autowriteall=' . &autowriteall)
|
||||||
|
" table_auto_fmt (default on) registers the NuwikiTableAutoFmt InsertLeave group.
|
||||||
|
call s:record(exists('#NuwikiTableAutoFmt#InsertLeave') ? 1 : 0,
|
||||||
|
\ 'config.table_auto_fmt_autocmd',
|
||||||
|
\ exists('#NuwikiTableAutoFmt#InsertLeave') ? '' : 'no InsertLeave autocmd')
|
||||||
|
|
||||||
|
" ===== :VimwikiRemoveDone -bang -range =====
|
||||||
|
" Upstream is `-range`; nuwiki had `-bang` only, so `:'<,'>VimwikiRemoveDone`
|
||||||
|
" raised E481. Now `-bang -range`: a ranged invocation must parse (no E481),
|
||||||
|
" and the `!` whole-buffer form must still work. The removal is an LSP
|
||||||
|
" roundtrip (no server here), so assert only the attribute parse.
|
||||||
|
call s:set_buf(['- [X] a', '- [ ] b', '- [X] c'])
|
||||||
|
let s:rd_err = ''
|
||||||
|
try
|
||||||
|
silent 1,2VimwikiRemoveDone
|
||||||
|
silent VimwikiRemoveDone!
|
||||||
|
catch /E481/
|
||||||
|
let s:rd_err = v:exception
|
||||||
|
catch
|
||||||
|
" non-E481 (e.g. no LSP attached) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:rd_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiRemoveDone_accepts_range_and_bang',
|
||||||
|
\ 'err=' . s:rd_err)
|
||||||
|
|
||||||
|
" ===== :VimwikiToggleRejectedListItem -range =====
|
||||||
|
" Was bare (range-less): `:1,3VimwikiToggleRejectedListItem` raised E481. The
|
||||||
|
" reject is an LSP roundtrip (no server here), so assert only the attribute
|
||||||
|
" parse: no E481.
|
||||||
|
call s:set_buf(['- [ ] a', '- [ ] b', '- [ ] c'])
|
||||||
|
let s:trl_err = ''
|
||||||
|
try
|
||||||
|
silent 1,3VimwikiToggleRejectedListItem
|
||||||
|
catch /E481/
|
||||||
|
let s:trl_err = v:exception
|
||||||
|
catch
|
||||||
|
" non-E481 (e.g. no LSP attached) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:trl_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiToggleRejectedListItem_accepts_range',
|
||||||
|
\ 'err=' . s:trl_err)
|
||||||
|
|
||||||
|
" ===== :VimwikiSplitLink -nargs=* =====
|
||||||
|
" Upstream takes optional reuse/move-cursor flags; nuwiki's defs took none, so
|
||||||
|
" `:VimwikiSplitLink 0 1` raised E488. Now -nargs=*: assert the arg parse
|
||||||
|
" succeeds. The split opens a window (the follow itself is an LSP no-op here);
|
||||||
|
" collapse back to one window afterward.
|
||||||
|
call s:set_buf(['[[Target]]'])
|
||||||
|
let s:sl_err = ''
|
||||||
|
try
|
||||||
|
silent VimwikiSplitLink 0 1
|
||||||
|
catch /E488/
|
||||||
|
let s:sl_err = v:exception
|
||||||
|
catch
|
||||||
|
" non-E488 (e.g. no LSP attached) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
silent! only
|
||||||
|
call s:record(
|
||||||
|
\ s:sl_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiSplitLink_accepts_args',
|
||||||
|
\ 'err=' . s:sl_err)
|
||||||
|
|
||||||
|
" ===== Command-line completion (nuwiki#complete#*) =====
|
||||||
|
" Pure client-side completers backing the -complete= specs on VimwikiGoto
|
||||||
|
" (pages), VimwikiColorize (colours), and the tag commands. Driven directly
|
||||||
|
" (no LSP) against a throwaway wiki dir.
|
||||||
|
let s:cw = tempname()
|
||||||
|
call mkdir(s:cw, 'p')
|
||||||
|
call writefile(['= alpha =', 'tagged :foo:bar:'], s:cw . '/alpha.wiki')
|
||||||
|
call writefile(['= beta ='], s:cw . '/beta.wiki')
|
||||||
|
let s:save_root = get(g:, 'nuwiki_wiki_root', v:null)
|
||||||
|
let g:nuwiki_wiki_root = s:cw
|
||||||
|
|
||||||
|
let s:pg = nuwiki#complete#pages('a', '', 0)
|
||||||
|
call s:record(
|
||||||
|
\ (index(s:pg, 'alpha') >= 0 && index(s:pg, 'beta') < 0) ? 1 : 0,
|
||||||
|
\ 'complete.pages_globs_and_filters',
|
||||||
|
\ 'got ' . string(s:pg))
|
||||||
|
|
||||||
|
let s:tg = nuwiki#complete#tags('', '', 0)
|
||||||
|
call s:record(
|
||||||
|
\ (index(s:tg, 'foo') >= 0 && index(s:tg, 'bar') >= 0) ? 1 : 0,
|
||||||
|
\ 'complete.tags_scans_wiki_files',
|
||||||
|
\ 'got ' . string(s:tg))
|
||||||
|
|
||||||
|
if s:save_root is v:null
|
||||||
|
unlet g:nuwiki_wiki_root
|
||||||
|
else
|
||||||
|
let g:nuwiki_wiki_root = s:save_root
|
||||||
|
endif
|
||||||
|
call delete(s:cw, 'rf')
|
||||||
|
|
||||||
|
" Colours come from `color:NAME` spans already in the buffer.
|
||||||
|
call s:set_buf(['<span style="color:tomato">x</span> plain'])
|
||||||
|
let s:cl = nuwiki#complete#colors('t', '', 0)
|
||||||
|
call s:record(
|
||||||
|
\ index(s:cl, 'tomato') >= 0 ? 1 : 0,
|
||||||
|
\ 'complete.colors_from_buffer_spans',
|
||||||
|
\ 'got ' . string(s:cl))
|
||||||
|
|
||||||
|
" ===== Diary-note family -count=0 (wiki selector) =====
|
||||||
|
" Was bare: `:2VimwikiMakeDiaryNote` raised E481. The diary open is an LSP
|
||||||
|
" roundtrip (no server here), so assert only the attribute parse: no E481.
|
||||||
|
call s:set_buf(['= page ='])
|
||||||
|
let s:dc_err = ''
|
||||||
|
try
|
||||||
|
silent 2VimwikiMakeDiaryNote
|
||||||
|
catch /E481/
|
||||||
|
let s:dc_err = v:exception
|
||||||
|
catch
|
||||||
|
" non-E481 (e.g. no LSP attached) is expected and fine here.
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:dc_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiMakeDiaryNote_accepts_count',
|
||||||
|
\ 'err=' . s:dc_err)
|
||||||
|
|
||||||
|
let s:di_err = ''
|
||||||
|
try
|
||||||
|
silent 2VimwikiDiaryIndex
|
||||||
|
catch /E481/
|
||||||
|
let s:di_err = v:exception
|
||||||
|
catch
|
||||||
|
endtry
|
||||||
|
call s:record(
|
||||||
|
\ s:di_err ==# '' ? 1 : 0,
|
||||||
|
\ 'cmd.VimwikiDiaryIndex_accepts_count',
|
||||||
|
\ 'err=' . s:di_err)
|
||||||
|
|
||||||
" ===== Wrap up =====
|
" ===== Wrap up =====
|
||||||
|
|
||||||
call add(s:results, '')
|
call add(s:results, '')
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ vim.defer_fn(function()
|
|||||||
'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
||||||
'NormalizeLink', 'RenumberList', 'RenumberAllLists',
|
'NormalizeLink', 'RenumberList', 'RenumberAllLists',
|
||||||
'ListToggle', 'IncrementListItem', 'DecrementListItem',
|
'ListToggle', 'IncrementListItem', 'DecrementListItem',
|
||||||
'CatUrl',
|
'CatUrl', 'ShowVersion', 'Return', 'Var', 'Search',
|
||||||
}
|
}
|
||||||
for _, suffix in ipairs(both_forms) do
|
for _, suffix in ipairs(both_forms) do
|
||||||
for _, prefix in ipairs({ 'Nuwiki', 'Vimwiki' }) do
|
for _, prefix in ipairs({ 'Nuwiki', 'Vimwiki' }) do
|
||||||
@@ -206,11 +206,15 @@ vim.defer_fn(function()
|
|||||||
local mapping_surface = {
|
local mapping_surface = {
|
||||||
-- Links
|
-- Links
|
||||||
{ '<CR>', 'n' }, { '<S-CR>', 'n' }, { '<C-CR>', 'n' }, { '<C-S-CR>', 'n' },
|
{ '<CR>', 'n' }, { '<S-CR>', 'n' }, { '<C-CR>', 'n' }, { '<C-S-CR>', 'n' },
|
||||||
|
{ '<M-CR>', 'n' }, { '<D-CR>', 'n' },
|
||||||
{ '<BS>', 'n' }, { '<Tab>', 'n' }, { '<S-Tab>', 'n' }, { '+', 'nx' },
|
{ '<BS>', 'n' }, { '<Tab>', 'n' }, { '<S-Tab>', 'n' }, { '+', 'nx' },
|
||||||
|
{ '<CR>', 'x' }, -- visual normalize-link (== visual `+`)
|
||||||
-- Lists
|
-- Lists
|
||||||
{ '<C-Space>', 'nx' }, { '<C-@>', 'nx' }, { '<Nul>', 'nx' },
|
{ '<C-Space>', 'nx' }, { '<C-@>', 'nx' }, { '<Nul>', 'nx' },
|
||||||
{ 'gnt', 'n' }, { 'gln', 'nx' }, { 'glp', 'nx' }, { 'glx', 'nx' },
|
{ 'gnt', 'n' }, { 'gln', 'nx' }, { 'glp', 'nx' }, { 'glx', 'nx' },
|
||||||
{ 'glh', 'n' }, { 'gll', 'n' }, { 'gLh', 'n' }, { 'gLl', 'n' },
|
{ 'glh', 'n' }, { 'gll', 'n' }, { 'gLh', 'n' }, { 'gLl', 'n' },
|
||||||
|
-- Upstream case-variant aliases of gLh/gLl/gLr.
|
||||||
|
{ 'gLH', 'n' }, { 'gLL', 'n' }, { 'gLR', 'n' },
|
||||||
{ 'glr', 'n' }, { 'gLr', 'n' }, { 'gl', 'n' }, { 'gL', 'n' },
|
{ 'glr', 'n' }, { 'gLr', 'n' }, { 'gl', 'n' }, { 'gL', 'n' },
|
||||||
{ 'gl-', 'n' }, { 'gL-', 'n' }, { 'gl*', 'n' }, { 'gL*', 'n' },
|
{ 'gl-', 'n' }, { 'gL-', 'n' }, { 'gl*', 'n' }, { 'gL*', 'n' },
|
||||||
{ 'gl#', 'n' }, { 'gL#', 'n' }, { 'gl1', 'n' }, { 'gL1', 'n' },
|
{ 'gl#', 'n' }, { 'gL#', 'n' }, { 'gl1', 'n' }, { 'gL1', 'n' },
|
||||||
@@ -226,7 +230,8 @@ vim.defer_fn(function()
|
|||||||
-- Wiki / diary / export
|
-- Wiki / diary / export
|
||||||
{ '<Leader>ww', 'n' }, { '<Leader>ws', 'n' }, { '<Leader>wi', 'n' },
|
{ '<Leader>ww', 'n' }, { '<Leader>ws', 'n' }, { '<Leader>wi', 'n' },
|
||||||
{ '<Leader>w<Leader>w', 'n' }, { '<Leader>w<Leader>y', 'n' },
|
{ '<Leader>w<Leader>w', 'n' }, { '<Leader>w<Leader>y', 'n' },
|
||||||
{ '<Leader>w<Leader>t', 'n' }, { '<Leader>w<Leader>i', 'n' },
|
{ '<Leader>w<Leader>t', 'n' }, { '<Leader>w<Leader>m', 'n' },
|
||||||
|
{ '<Leader>w<Leader>i', 'n' },
|
||||||
{ '<Leader>wn', 'n' }, { '<Leader>wd', 'n' }, { '<Leader>wr', 'n' },
|
{ '<Leader>wn', 'n' }, { '<Leader>wd', 'n' }, { '<Leader>wr', 'n' },
|
||||||
{ '<Leader>wh', 'n' }, { '<Leader>whh', 'n' }, { '<Leader>wha', 'n' },
|
{ '<Leader>wh', 'n' }, { '<Leader>whh', 'n' }, { '<Leader>wha', 'n' },
|
||||||
{ '<Leader>wc', 'nx' }, { '<C-Down>', 'n' }, { '<C-Up>', 'n' },
|
{ '<Leader>wc', 'nx' }, { '<C-Down>', 'n' }, { '<C-Up>', 'n' },
|
||||||
@@ -238,7 +243,7 @@ vim.defer_fn(function()
|
|||||||
{ 'al', 'ox' }, { 'il', 'ox' }, { 'a\\', 'ox' }, { 'i\\', 'ox' },
|
{ 'al', 'ox' }, { 'il', 'ox' }, { 'a\\', 'ox' }, { 'i\\', 'ox' },
|
||||||
{ 'ac', 'ox' }, { 'ic', 'ox' },
|
{ 'ac', 'ox' }, { 'ic', 'ox' },
|
||||||
-- Insert-mode
|
-- Insert-mode
|
||||||
{ '<CR>', 'i' }, { '<Tab>', 'i' }, { '<S-Tab>', 'i' },
|
{ '<CR>', 'i' }, { '<S-CR>', 'i' }, { '<Tab>', 'i' }, { '<S-Tab>', 'i' },
|
||||||
{ '<C-D>', 'i' }, { '<C-T>', 'i' },
|
{ '<C-D>', 'i' }, { '<C-T>', 'i' },
|
||||||
{ '<C-L><C-J>', 'i' }, { '<C-L><C-K>', 'i' }, { '<C-L><C-M>', 'i' },
|
{ '<C-L><C-J>', 'i' }, { '<C-L><C-K>', 'i' }, { '<C-L><C-M>', 'i' },
|
||||||
}
|
}
|
||||||
@@ -251,6 +256,80 @@ vim.defer_fn(function()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ===== Config-driven buffer behaviors =====
|
||||||
|
-- autowriteall (default on) mirrors into Vim's global 'autowriteall' while
|
||||||
|
-- the wiki buffer is current.
|
||||||
|
record(vim.o.autowriteall == true, 'config.autowriteall_applied',
|
||||||
|
vim.o.autowriteall and '' or 'autowriteall not set on the wiki buffer')
|
||||||
|
-- table_auto_fmt wiring: with the option enabled, attaching a buffer
|
||||||
|
-- registers a buffer-local InsertLeave autocmd. (It's off in the harness
|
||||||
|
-- setup so the async re-align doesn't perturb the table-nav cases below, so
|
||||||
|
-- verify on a throwaway scratch buffer in its own window.)
|
||||||
|
do
|
||||||
|
local cfg = require('nuwiki.config')
|
||||||
|
cfg.options.table_auto_fmt = true
|
||||||
|
vim.cmd('new')
|
||||||
|
local sbuf = vim.api.nvim_get_current_buf()
|
||||||
|
require('nuwiki.ftplugin').attach(sbuf)
|
||||||
|
local found = false
|
||||||
|
for _, a in ipairs(vim.api.nvim_get_autocmds({ event = 'InsertLeave', buffer = sbuf })) do
|
||||||
|
if a.group_name and a.group_name:match('^nuwiki_taf_') then
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
record(found, 'config.table_auto_fmt_autocmd',
|
||||||
|
found and '' or 'attach did not register the InsertLeave autocmd when enabled')
|
||||||
|
cfg.options.table_auto_fmt = false
|
||||||
|
vim.cmd('bwipeout!')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- :VimwikiVar key val sets the in-memory option.
|
||||||
|
do
|
||||||
|
vim.cmd('VimwikiVar testkey hello')
|
||||||
|
local v = require('nuwiki.config').options.testkey
|
||||||
|
record(v == 'hello', 'cmd.VimwikiVar_sets_option', 'options.testkey=' .. tostring(v))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Index / TabIndex / Var / ShowVersion are global (work outside a wiki
|
||||||
|
-- buffer). Open a throwaway scratch buffer and confirm they're visible.
|
||||||
|
do
|
||||||
|
vim.cmd('enew')
|
||||||
|
vim.bo.buftype = 'nofile'
|
||||||
|
local ok = vim.fn.exists(':VimwikiIndex') == 2
|
||||||
|
and vim.fn.exists(':VimwikiTabIndex') == 2
|
||||||
|
and vim.fn.exists(':VimwikiVar') == 2
|
||||||
|
and vim.fn.exists(':VimwikiShowVersion') == 2
|
||||||
|
record(ok, 'cmd.global_entry_points',
|
||||||
|
ok and '' or 'a global command is not visible outside a wiki buffer')
|
||||||
|
vim.cmd('bwipeout!')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- auto_header: the function inserts `= Stem =` on an empty new wiki buffer,
|
||||||
|
-- and skips the wiki index page. Operate on dedicated buffer handles so the
|
||||||
|
-- harness's own buffer is untouched.
|
||||||
|
do
|
||||||
|
local wroot = require('nuwiki.config').wiki_cfg(0).root
|
||||||
|
local root = vim.fn.fnamemodify(vim.fn.expand(wroot), ':p')
|
||||||
|
if root:sub(-1) ~= '/' then root = root .. '/' end
|
||||||
|
local function ah_line(name)
|
||||||
|
local buf = vim.api.nvim_create_buf(true, false)
|
||||||
|
vim.api.nvim_buf_set_name(buf, name)
|
||||||
|
require('nuwiki.commands').auto_header(buf)
|
||||||
|
local line = vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] or ''
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
-- A normal page (outside any wiki root) → header inserted.
|
||||||
|
local got = ah_line('/tmp/nuwiki-ah-plain/MyNewPage.wiki')
|
||||||
|
record(got == '= MyNewPage =', 'cmd.auto_header_inserts', 'line1=' .. got)
|
||||||
|
-- The diary index page → skipped (no header). (The wiki index itself is the
|
||||||
|
-- harness's open buffer, so use the diary index to avoid an E95 collision.)
|
||||||
|
local c = require('nuwiki.config').wiki_cfg(0)
|
||||||
|
local idx_line = ah_line(root .. c.diary_rel .. '/' .. c.diary_idx .. '.wiki')
|
||||||
|
record(idx_line == '', 'cmd.auto_header_skips_diary_index', 'line1=' .. idx_line)
|
||||||
|
end
|
||||||
|
|
||||||
-- ===== Lists =====
|
-- ===== Lists =====
|
||||||
|
|
||||||
run('list.toggle_via_C-Space', {
|
run('list.toggle_via_C-Space', {
|
||||||
@@ -716,6 +795,338 @@ vim.defer_fn(function()
|
|||||||
-- a populated diary, but at least confirm the maps fire without
|
-- a populated diary, but at least confirm the maps fire without
|
||||||
-- error.
|
-- error.
|
||||||
|
|
||||||
|
-- ===== Colorize commands pass their {color} arg (regression) =====
|
||||||
|
-- :VimwikiColorize / :NuwikiColorize are -nargs=1; the Neovim defs used to
|
||||||
|
-- call colorize() without <q-args>, silently dropping the colour and
|
||||||
|
-- prompting instead. Run the real commands and assert the colour lands.
|
||||||
|
tobj_case('cmd.VimwikiColorize_uses_arg', function()
|
||||||
|
set_buf({ 'word here' })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.cmd('VimwikiColorize red')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= '<span style="color:red">word</span> here' then
|
||||||
|
error('VimwikiColorize dropped its arg: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
tobj_case('cmd.NuwikiColorize_uses_arg', function()
|
||||||
|
set_buf({ 'second word' })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.cmd('NuwikiColorize #00ff00')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= '<span style="color:#00ff00">second</span> word' then
|
||||||
|
error('NuwikiColorize dropped its arg: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- Establish a stale visual selection, then confirm the normal-mode command
|
||||||
|
-- still wraps the cword (not the leftover selection) — the visualmode()
|
||||||
|
-- heuristic regression.
|
||||||
|
tobj_case('cmd.Colorize_ignores_stale_visual_selection', function()
|
||||||
|
set_buf({ 'alpha beta gamma' })
|
||||||
|
vim.cmd('normal! 0vee\27') -- select 'alpha beta', leave marks behind
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 11 }) -- on 'gamma'
|
||||||
|
vim.cmd('VimwikiColorize blue')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= 'alpha beta <span style="color:blue">gamma</span>' then
|
||||||
|
error('command wrapped stale selection instead of cword: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- The visual mapping path (explicit visual=true) wraps the selection.
|
||||||
|
tobj_case('colorize.visual_wraps_selection', function()
|
||||||
|
set_buf({ 'one two three' })
|
||||||
|
vim.cmd('normal! 0wviw\27') -- visually select 'two', marks set on exit
|
||||||
|
require('nuwiki.commands').colorize('red', true)
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= 'one <span style="color:red">two</span> three' then
|
||||||
|
error('visual colorize did not wrap selection: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- Invoking the command with a range (`:'<,'>VimwikiColorize`, as Vim builds
|
||||||
|
-- it when you select then type the command) must not error (-range) and
|
||||||
|
-- wraps the selection.
|
||||||
|
tobj_case('cmd.Colorize_range_wraps_selection', function()
|
||||||
|
set_buf({ 'pick this word' })
|
||||||
|
vim.cmd('normal! 0wviw\27') -- select 'this'
|
||||||
|
vim.cmd("'<,'>VimwikiColorize orange")
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= 'pick <span style="color:orange">this</span> word' then
|
||||||
|
error('ranged command did not wrap selection: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- Colour spans are concealed down to their coloured text. Put the span on a
|
||||||
|
-- non-cursor line so concealcursor (=c) doesn't reveal it, then refresh.
|
||||||
|
tobj_case('colorize.conceal_hides_tags_shows_text', function()
|
||||||
|
set_buf({ 'plain line', '<span style="color:red">word</span> tail' })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.fn['nuwiki#colors#refresh']()
|
||||||
|
local l = vim.fn.getline(2)
|
||||||
|
local wcol = (l:find('word')) -- 1-based
|
||||||
|
local open = vim.fn.synconcealed(2, 1)[1]
|
||||||
|
local close = vim.fn.synconcealed(2, (l:find('</span>')))[1]
|
||||||
|
local word = vim.fn.synconcealed(2, wcol)[1]
|
||||||
|
local grp = vim.fn.synIDattr(vim.fn.synID(2, wcol, 1), 'name')
|
||||||
|
if not (open == 1 and close == 1 and word == 0 and grp == 'nuwikiColorSpan_red') then
|
||||||
|
error(string.format('open=%s close=%s word=%s grp=%s', open, close, word, grp))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ===== Visual-range list commands + normalize-visual (regression) =====
|
||||||
|
-- :N,MVimwikiToggleListItem toggles every checkbox in the range (server,
|
||||||
|
-- one request per line; wait for the edits to land).
|
||||||
|
tobj_case('cmd.toggle_list_item_range', function()
|
||||||
|
set_buf({ '- [ ] a', '- [ ] b', '- [ ] c' })
|
||||||
|
vim.cmd('1,3VimwikiToggleListItem')
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
for _, l in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do
|
||||||
|
if not l:match('%[[xX]%]') then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('range toggle incomplete: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:1,3VimwikiToggleRejectedListItem` — now -range (was bare → E481 on a
|
||||||
|
-- ranged invocation, and cursor-only). Each line should gain a rejected
|
||||||
|
-- checkbox `[-]`.
|
||||||
|
tobj_case('cmd.reject_list_item_range', function()
|
||||||
|
set_buf({ '- [ ] a', '- [ ] b', '- [ ] c' })
|
||||||
|
vim.cmd('1,3VimwikiToggleRejectedListItem')
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
for _, l in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do
|
||||||
|
if not l:match('%[%-%]') then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('range reject incomplete: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:1,2VimwikiRemoveDone` — now -range (was -bang only → E481 on a ranged
|
||||||
|
-- invocation). Only done items within the range are removed; line-2 `[X]`
|
||||||
|
-- (0-indexed) outside [0,1] survives.
|
||||||
|
tobj_case('cmd.remove_done_range', function()
|
||||||
|
set_buf({ '- [X] a', '- [ ] b', '- [X] c' })
|
||||||
|
vim.cmd('1,2VimwikiRemoveDone')
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
local ls = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||||
|
-- line-0 done item gone (2 lines left), line-2 done item still present.
|
||||||
|
return #ls == 2 and ls[#ls]:match('%[X%] c')
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('ranged remove-done wrong: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:VimwikiTable {cols} [rows]` — now -nargs=* (was -nargs=1, which dropped
|
||||||
|
-- the args entirely). A 4-col table has 5 pipes per row; 3 data rows give
|
||||||
|
-- header + separator + 3 = 5 table lines.
|
||||||
|
tobj_case('cmd.VimwikiTable_passes_cols_and_rows', function()
|
||||||
|
set_buf({ '' })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.cmd('VimwikiTable 4 3')
|
||||||
|
local tbl = {}
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
tbl = {}
|
||||||
|
for _, l in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do
|
||||||
|
if l:find('|', 1, true) then tbl[#tbl + 1] = l end
|
||||||
|
end
|
||||||
|
return #tbl >= 5
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('table not inserted: ' .. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
local pipes = select(2, tbl[1]:gsub('|', ''))
|
||||||
|
if pipes ~= 5 then
|
||||||
|
error('want 4 cols (5 pipes), got ' .. pipes .. ': ' .. tbl[1])
|
||||||
|
end
|
||||||
|
if #tbl ~= 5 then
|
||||||
|
error('want 5 table lines (hdr+sep+3 rows), got ' .. #tbl)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- The Neovim :Vimwiki*Link Ex-commands used to dispatch to raw
|
||||||
|
-- vim.lsp.buf.definition(), bypassing the bare-word → [[link]] wrap that the
|
||||||
|
-- Vim branch and the <CR> mapping do. They now route through
|
||||||
|
-- follow_link_or_create; the wrap is a pure client-side buffer edit (no LSP),
|
||||||
|
-- so it's the headlessly-reliable observable.
|
||||||
|
tobj_case('cmd.VimwikiFollowLink_wraps_bare_word', function()
|
||||||
|
set_buf({ 'word here' })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.cmd('VimwikiFollowLink')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= '[[word]] here' then
|
||||||
|
error('VimwikiFollowLink did not wrap bare word: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
tobj_case('cmd.VimwikiSplitLink_wraps_bare_word', function()
|
||||||
|
set_buf({ 'word here' })
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.cmd('VimwikiSplitLink')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
vim.cmd('only') -- drop the split this command opened
|
||||||
|
if got ~= '[[word]] here' then
|
||||||
|
error('VimwikiSplitLink did not wrap bare word: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:[range]VimwikiListChangeLvl {dir} [plus_children]` — now -range -nargs=+
|
||||||
|
-- (was -nargs=?, range-less). A 1,3 range with `increase` must indent the
|
||||||
|
-- items that have somewhere to nest (the 2nd item under the 1st).
|
||||||
|
tobj_case('cmd.ListChangeLvl_range_indents', function()
|
||||||
|
set_buf({ '- a', '- b', '- c' })
|
||||||
|
vim.cmd('1,3VimwikiListChangeLvl increase 0')
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
local l = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||||
|
return l[2] ~= nil and l[2]:match('^%s%s*%-') ~= nil
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('range list-change-lvl did not indent: '
|
||||||
|
.. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:[range]VimwikiChangeSymbolTo {sym}` — now -range -nargs=1. A 1,3 range
|
||||||
|
-- must set every item's marker to the given symbol.
|
||||||
|
tobj_case('cmd.ChangeSymbolTo_range_sets_marker', function()
|
||||||
|
set_buf({ '- a', '- b', '- c' })
|
||||||
|
vim.cmd('1,3VimwikiChangeSymbolTo *')
|
||||||
|
local done = vim.wait(2000, function()
|
||||||
|
local l = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||||
|
for _, ln in ipairs(l) do
|
||||||
|
if not ln:match('^%*%s') then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end, 30)
|
||||||
|
if not done then
|
||||||
|
error('range change-symbol did not set all markers: '
|
||||||
|
.. vim.inspect(vim.api.nvim_buf_get_lines(0, 0, -1, false)))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:VimwikiGenerateLinks [rel_path]` — now -nargs=? (was bare; an arg raised
|
||||||
|
-- E488). Both forms must dispatch without a Vim-level command error.
|
||||||
|
tobj_case('cmd.GenerateLinks_accepts_optional_path', function()
|
||||||
|
set_buf({ '= page =' })
|
||||||
|
local ok1 = pcall(vim.cmd, 'VimwikiGenerateLinks')
|
||||||
|
local ok2 = pcall(vim.cmd, 'VimwikiGenerateLinks subdir')
|
||||||
|
if not (ok1 and ok2) then
|
||||||
|
error('GenerateLinks rejected an arg: ok1=' .. tostring(ok1) .. ' ok2=' .. tostring(ok2))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- :NuwikiGenerateTags alias must exist (parity with :VimwikiGenerateTags).
|
||||||
|
tobj_case('cmd.NuwikiGenerateTags_exists', function()
|
||||||
|
if vim.fn.exists(':NuwikiGenerateTags') ~= 2 then
|
||||||
|
error(':NuwikiGenerateTags not defined')
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- Diary-note family carries -count=0 (vimwiki's wiki selector), so
|
||||||
|
-- `:2VimwikiMakeDiaryNote` selects wiki #2 instead of raising E481.
|
||||||
|
tobj_case('cmd.VimwikiMakeDiaryNote_has_count', function()
|
||||||
|
local c = vim.api.nvim_buf_get_commands(0, {}).VimwikiMakeDiaryNote
|
||||||
|
if not c then error(':VimwikiMakeDiaryNote not defined') end
|
||||||
|
if c.count == nil or c.count == false then
|
||||||
|
error('VimwikiMakeDiaryNote missing -count: ' .. vim.inspect(c))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- `:VimwikiNormalizeLink 1` (the arg is upstream's visual flag; the command
|
||||||
|
-- is -nargs=? not -range, so no `'<,'>`) wraps the last visual selection.
|
||||||
|
tobj_case('cmd.normalize_link_visual_wraps_selection', function()
|
||||||
|
set_buf({ 'one two three' })
|
||||||
|
vim.cmd('normal! 0wviw\27') -- select 'two', leave visual (sets '<,'> marks)
|
||||||
|
vim.cmd('VimwikiNormalizeLink 1')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= 'one [[two]] three' then
|
||||||
|
error('visual normalize did not wrap selection: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Visual <CR> mapping wraps the selection (upstream parity with visual `+`).
|
||||||
|
-- Select via `normal!` (sets the '<,'> marks reliably), then `gv` reselects
|
||||||
|
-- and the fed <CR> fires the x-mode mapping.
|
||||||
|
tobj_case('map.visual_cr_wraps_selection', function()
|
||||||
|
set_buf({ 'alpha beta gamma' })
|
||||||
|
vim.cmd('normal! 0wviw\27') -- select 'beta', leave visual → marks set
|
||||||
|
send('gv<CR>')
|
||||||
|
local got = vim.api.nvim_get_current_line()
|
||||||
|
if got ~= 'alpha [[beta]] gamma' then
|
||||||
|
error('visual <CR> did not wrap selection: ' .. got)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Insert <S-CR> continues a list item with no marker, aligned under the text.
|
||||||
|
-- Test the `<expr>` handler directly — `<S-CR>` doesn't round-trip through
|
||||||
|
-- headless feedkeys, but the handler is what the mapping invokes.
|
||||||
|
tobj_case('map.insert_shift_cr_multiline', function()
|
||||||
|
local cr = vim.api.nvim_replace_termcodes('<CR>', true, false, true)
|
||||||
|
set_buf({ '- item' }) -- "- " is 2 cols → continuation aligns at column 2
|
||||||
|
local got = require('nuwiki.commands').smart_shift_return()
|
||||||
|
if got ~= cr .. ' ' then
|
||||||
|
error('S-CR (bullet) wrong: ' .. vim.inspect(got))
|
||||||
|
end
|
||||||
|
set_buf({ '- [ ] item' }) -- "- [ ] " is 6 cols
|
||||||
|
got = require('nuwiki.commands').smart_shift_return()
|
||||||
|
if got ~= cr .. ' ' then
|
||||||
|
error('S-CR (checkbox) wrong: ' .. vim.inspect(got))
|
||||||
|
end
|
||||||
|
set_buf({ 'plain text' }) -- not a list → plain <CR>
|
||||||
|
got = require('nuwiki.commands').smart_shift_return()
|
||||||
|
if got ~= cr then
|
||||||
|
error('S-CR (non-list) should be plain CR: ' .. vim.inspect(got))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ===== Configurable map_prefix (vimwiki g:vimwiki_map_prefix) =====
|
||||||
|
-- Overriding map_prefix must relocate the whole <prefix>* family. We attach
|
||||||
|
-- to a throwaway scratch buffer with a custom prefix and assert the new
|
||||||
|
-- bindings exist there while the default <Leader>w* ones do not.
|
||||||
|
tobj_case('map_prefix.relocates_wiki_family', function()
|
||||||
|
local config = require('nuwiki.config')
|
||||||
|
local keymaps = require('nuwiki.keymaps')
|
||||||
|
local saved = config.options.map_prefix
|
||||||
|
config.options.map_prefix = '<Leader>n'
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
keymaps.attach(buf, { enabled = true, wiki_prefix = true, links = true, html_export = true })
|
||||||
|
local function bmap(lhs)
|
||||||
|
local d = vim.fn.maparg(lhs, 'n', false, true)
|
||||||
|
return not vim.tbl_isempty(d) and d.buffer == 1
|
||||||
|
end
|
||||||
|
for _, lhs in ipairs({
|
||||||
|
'<Leader>nw', '<Leader>nt', '<Leader>n<Leader>w', '<Leader>nr', '<Leader>nh', '<Leader>nha',
|
||||||
|
}) do
|
||||||
|
if not bmap(lhs) then error('relocated map missing: ' .. lhs) end
|
||||||
|
end
|
||||||
|
if bmap('<Leader>ww') then error('<Leader>ww still bound under custom prefix') end
|
||||||
|
end)
|
||||||
|
-- Always restore global state so later cases see the default prefix.
|
||||||
|
config.options.map_prefix = saved
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
if not ok then error(err) end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ===== Global (non-buffer) entry-point diary maps =====
|
||||||
|
-- init.lua _setup_global_mappings ran during setup(). Regression guard for
|
||||||
|
-- the audit-found bug: <prefix><Leader>t must be today-in-a-tab and
|
||||||
|
-- <prefix><Leader>m must be tomorrow (matching the Vim global side +
|
||||||
|
-- upstream), not the old wiring that dropped `m` and bound `t` to tomorrow.
|
||||||
|
-- Query in a scratch (non-vimwiki) buffer so the buffer-local maps don't
|
||||||
|
-- shadow the globals.
|
||||||
|
tobj_case('global_maps.diary_tab_and_tomorrow_targets', function()
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
local function gdesc(lhs)
|
||||||
|
local d = vim.fn.maparg(lhs, 'n', false, true)
|
||||||
|
if vim.tbl_isempty(d) then return nil end
|
||||||
|
return d.desc or ''
|
||||||
|
end
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
local t = gdesc('<Leader>w<Leader>t')
|
||||||
|
local m = gdesc('<Leader>w<Leader>m')
|
||||||
|
if not t then error('<Leader>w<Leader>t global map missing') end
|
||||||
|
if not m then error('<Leader>w<Leader>m global map missing (tomorrow)') end
|
||||||
|
if not t:find('tab') then error('<Leader>w<Leader>t should be today-in-tab, desc=' .. t) end
|
||||||
|
if not m:find('tomorrow') then error('<Leader>w<Leader>m should be tomorrow, desc=' .. m) end
|
||||||
|
end)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
if not ok then error(err) end
|
||||||
|
end)
|
||||||
|
|
||||||
-- ===== Wiki picker (config-driven, no LSP) =====
|
-- ===== Wiki picker (config-driven, no LSP) =====
|
||||||
-- The picker must build its list from config so it works from any buffer
|
-- The picker must build its list from config so it works from any buffer
|
||||||
-- before the server attaches.
|
-- before the server attaches.
|
||||||
|
|||||||
@@ -39,8 +39,14 @@ cat > "$INIT" <<EOF
|
|||||||
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
||||||
vim.g.nuwiki_binary_path = '$BIN'
|
vim.g.nuwiki_binary_path = '$BIN'
|
||||||
-- Enable the opt-in mouse mappings so the harness can assert the full
|
-- Enable the opt-in mouse mappings so the harness can assert the full
|
||||||
-- documented mapping surface (doc §6 mouse group).
|
-- documented mapping surface (doc §6 mouse group). table_auto_fmt is off here
|
||||||
require('nuwiki').setup({ wiki_root = '$TMP/wiki', mappings = { mouse = true } })
|
-- so its async on-InsertLeave re-align doesn't perturb the deterministic
|
||||||
|
-- table-nav keystroke cases (its wiring is verified on a scratch buffer).
|
||||||
|
require('nuwiki').setup({
|
||||||
|
wiki_root = '$TMP/wiki',
|
||||||
|
mappings = { mouse = true },
|
||||||
|
table_auto_fmt = false,
|
||||||
|
})
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
RESULTS="$TMP/results.txt"
|
RESULTS="$TMP/results.txt"
|
||||||
|
|||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# development/tests/test-vars-vim.sh — exercises the vimwiki compatibility
|
||||||
|
# shim (autoload/vimwiki/vars.vim) that third-party plugins (vimwiki-sync,
|
||||||
|
# vim-zettel) call via vimwiki#vars#get_wikilocal.
|
||||||
|
#
|
||||||
|
# Runs test-vars-vim.vim under headless Vim; that script writes PASS/FAIL
|
||||||
|
# lines to $NUWIKI_VARS_OUT. Covers per-wiki-index resolution, global
|
||||||
|
# fallback, out-of-range clamp, the no-index "owning wiki" path (the
|
||||||
|
# BufRead path that previously raised E121), and the single-wiki shorthand.
|
||||||
|
#
|
||||||
|
# Exit code: 0 when every check passes, 1 on any FAIL or missing output.
|
||||||
|
# Skips (exit 0) when vim is not installed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m[vars-vim]\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
|
if ! command -v vim >/dev/null 2>&1; then
|
||||||
|
log 'vim not installed — skipping'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="$(mktemp -d -t nuwiki-vars-vim-test-XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
DIR_A="$TMP/wiki_a"
|
||||||
|
DIR_B="$TMP/wiki_b"
|
||||||
|
mkdir -p "$DIR_A" "$DIR_B"
|
||||||
|
printf '= Home =\n' > "$DIR_A/index.wiki"
|
||||||
|
printf '= B =\n' > "$DIR_B/index.wiki"
|
||||||
|
|
||||||
|
OUT="$TMP/vars.out"
|
||||||
|
|
||||||
|
VIMRC="$TMP/vimrc"
|
||||||
|
cat > "$VIMRC" <<EOF
|
||||||
|
set nocompatible
|
||||||
|
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log 'running Vim vars-shim harness…'
|
||||||
|
NUWIKI_VARS_OUT="$OUT" \
|
||||||
|
NUWIKI_VARS_DIR_A="$DIR_A" \
|
||||||
|
NUWIKI_VARS_DIR_B="$DIR_B" \
|
||||||
|
timeout 30 vim -e -s -u "$VIMRC" \
|
||||||
|
-c "source $REPO_ROOT/development/tests/test-vars-vim.vim" \
|
||||||
|
</dev/null >"$TMP/vim.log" 2>&1 || true
|
||||||
|
|
||||||
|
if [[ ! -f "$OUT" ]]; then
|
||||||
|
echo 'vars harness produced no output' >&2
|
||||||
|
echo '--- vim log ---' >&2
|
||||||
|
cat "$TMP/vim.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$OUT"
|
||||||
|
PASSED="$(grep -c '^PASS ' "$OUT" || true)"
|
||||||
|
FAILED="$(grep -c '^FAIL ' "$OUT" || true)"
|
||||||
|
echo "SUMMARY: ${PASSED} passed, ${FAILED} failed"
|
||||||
|
|
||||||
|
if [[ "$FAILED" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log 'vars shim OK ✓'
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
" development/tests/test-vars-vim.vim — exercises the vimwiki compat shim
|
||||||
|
" (autoload/vimwiki/vars.vim) that third-party plugins call.
|
||||||
|
"
|
||||||
|
" Sourced by test-vars-vim.sh under headless Vim. Writes PASS/FAIL lines to
|
||||||
|
" $NUWIKI_VARS_OUT; the shell wrapper fails on any FAIL (or missing output).
|
||||||
|
|
||||||
|
let s:out = []
|
||||||
|
let s:dir_a = $NUWIKI_VARS_DIR_A
|
||||||
|
let s:dir_b = $NUWIKI_VARS_DIR_B
|
||||||
|
|
||||||
|
function! s:check(desc, got, want) abort
|
||||||
|
if a:got ==# a:want
|
||||||
|
call add(s:out, 'PASS ' . a:desc)
|
||||||
|
else
|
||||||
|
call add(s:out, 'FAIL ' . a:desc . ' got=[' . a:got . '] want=[' . a:want . ']')
|
||||||
|
endif
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" ----- multi-wiki: explicit index -----
|
||||||
|
let g:nuwiki_wikis = [
|
||||||
|
\ {'root': s:dir_a, 'file_extension': 'wiki', 'syntax': 'markdown'},
|
||||||
|
\ {'root': s:dir_b, 'file_extension': '.wiki'},
|
||||||
|
\ ]
|
||||||
|
let g:nuwiki_syntax = 'vimwiki'
|
||||||
|
|
||||||
|
call s:check('idx0 path', vimwiki#vars#get_wikilocal('path', 0), s:dir_a . '/')
|
||||||
|
call s:check('idx0 ext', vimwiki#vars#get_wikilocal('ext', 0), '.wiki')
|
||||||
|
call s:check('idx0 syntax', vimwiki#vars#get_wikilocal('syntax', 0), 'markdown')
|
||||||
|
" wiki[1] doesn't set syntax → global default
|
||||||
|
call s:check('idx1 path', vimwiki#vars#get_wikilocal('path', 1), s:dir_b . '/')
|
||||||
|
call s:check('idx1 syntax', vimwiki#vars#get_wikilocal('syntax', 1), 'vimwiki')
|
||||||
|
" out-of-range clamps to first wiki
|
||||||
|
call s:check('idx9 clamp', vimwiki#vars#get_wikilocal('path', 9), s:dir_a . '/')
|
||||||
|
" misc keys
|
||||||
|
call s:check('is_temporary', vimwiki#vars#get_wikilocal('is_temporary_wiki', 0) . '', '0')
|
||||||
|
call s:check('unknown key', vimwiki#vars#get_wikilocal('bogus', 0), '')
|
||||||
|
|
||||||
|
" ----- no index: resolve the wiki owning the current buffer -----
|
||||||
|
" (this is the BufRead path; regression guard for the s:resolve_index E121.)
|
||||||
|
execute 'edit ' . fnameescape(s:dir_b . '/index.wiki')
|
||||||
|
call s:check('noarg owning wiki_b', vimwiki#vars#get_wikilocal('path'), s:dir_b . '/')
|
||||||
|
execute 'edit ' . fnameescape(s:dir_a . '/index.wiki')
|
||||||
|
call s:check('noarg owning wiki_a', vimwiki#vars#get_wikilocal('path'), s:dir_a . '/')
|
||||||
|
" buffer outside any wiki → first wiki
|
||||||
|
enew
|
||||||
|
call s:check('noarg outside→first', vimwiki#vars#get_wikilocal('path'), s:dir_a . '/')
|
||||||
|
|
||||||
|
" ----- single-wiki shorthand (no g:nuwiki_wikis) -----
|
||||||
|
unlet g:nuwiki_wikis
|
||||||
|
let g:nuwiki_wiki_root = s:dir_a
|
||||||
|
let g:nuwiki_file_extension = 'txt'
|
||||||
|
enew
|
||||||
|
call s:check('shorthand path', vimwiki#vars#get_wikilocal('path'), s:dir_a . '/')
|
||||||
|
call s:check('shorthand ext', vimwiki#vars#get_wikilocal('ext'), '.txt')
|
||||||
|
call s:check('shorthand syntax', vimwiki#vars#get_wikilocal('syntax'), 'vimwiki')
|
||||||
|
|
||||||
|
call writefile(s:out, $NUWIKI_VARS_OUT)
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# development/tests/test-vimwiki-compat-vim.sh — upstream-vimwiki config
|
||||||
|
# drop-in compatibility for the Vim client.
|
||||||
|
#
|
||||||
|
# Runs test-vimwiki-compat-vim.vim under headless Vim; that script sets an
|
||||||
|
# upstream `g:vimwiki_list` + `g:vimwiki_*` config and asserts
|
||||||
|
# nuwiki#lsp#settings() translates it into the server's schema (per-wiki key
|
||||||
|
# remap + global scalar settings folded into each wiki). Also checks that
|
||||||
|
# nuwiki-native config takes precedence.
|
||||||
|
#
|
||||||
|
# Exit code: 0 when every check passes, 1 on any FAIL or missing output.
|
||||||
|
# Skips (exit 0) when vim is not installed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m[vimwiki-compat]\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
|
if ! command -v vim >/dev/null 2>&1; then
|
||||||
|
log 'vim not installed — skipping'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="$(mktemp -d -t nuwiki-vwc-test-XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
OUT="$TMP/vwc.out"
|
||||||
|
|
||||||
|
VIMRC="$TMP/vimrc"
|
||||||
|
cat > "$VIMRC" <<EOF
|
||||||
|
set nocompatible
|
||||||
|
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log 'running Vim vimwiki-compat harness…'
|
||||||
|
NUWIKI_VWC_OUT="$OUT" \
|
||||||
|
timeout 30 vim -e -s -u "$VIMRC" \
|
||||||
|
-c "source $REPO_ROOT/development/tests/test-vimwiki-compat-vim.vim" \
|
||||||
|
</dev/null >"$TMP/vim.log" 2>&1 || true
|
||||||
|
|
||||||
|
if [[ ! -f "$OUT" ]]; then
|
||||||
|
echo 'vimwiki-compat harness produced no output' >&2
|
||||||
|
echo '--- vim log ---' >&2
|
||||||
|
cat "$TMP/vim.log" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$OUT"
|
||||||
|
PASSED="$(grep -c '^PASS ' "$OUT" || true)"
|
||||||
|
FAILED="$(grep -c '^FAIL ' "$OUT" || true)"
|
||||||
|
echo "SUMMARY: ${PASSED} passed, ${FAILED} failed"
|
||||||
|
|
||||||
|
if [[ "$FAILED" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log 'vimwiki config compat OK ✓'
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
" development/tests/test-vimwiki-compat-vim.vim — upstream-vimwiki config
|
||||||
|
" drop-in compatibility for the Vim client.
|
||||||
|
"
|
||||||
|
" Sourced by test-vimwiki-compat-vim.sh under headless Vim. Sets an upstream
|
||||||
|
" `g:vimwiki_list` + `g:vimwiki_*` config and asserts nuwiki#lsp#settings()
|
||||||
|
" translates it into the server's schema. Writes PASS/FAIL to
|
||||||
|
" $NUWIKI_VWC_OUT; the wrapper fails on any FAIL.
|
||||||
|
|
||||||
|
let s:out = []
|
||||||
|
function! s:check(desc, cond) abort
|
||||||
|
call add(s:out, (a:cond ? 'PASS ' : 'FAIL ') . a:desc)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" ----- upstream vimwiki config (no nuwiki-native vars set) -----
|
||||||
|
let g:vimwiki_toc_header_level = 2
|
||||||
|
let g:vimwiki_html_header_numbering = 2
|
||||||
|
let g:vimwiki_html_header_numbering_sym = ' -'
|
||||||
|
let s:personal = {}
|
||||||
|
let s:personal.path = '~/.vimwiki/personal_wiki'
|
||||||
|
let s:personal.path_html = '~/public_html/personal_wiki'
|
||||||
|
let s:personal.template_default = 'default'
|
||||||
|
let s:personal.template_ext = '.tpl'
|
||||||
|
let s:personal.auto_export = 1
|
||||||
|
let s:ifood = {}
|
||||||
|
let s:ifood.path = '~/.vimwiki/ifood_wiki'
|
||||||
|
let g:vimwiki_list = [s:personal, s:ifood]
|
||||||
|
|
||||||
|
let s:cfg = nuwiki#lsp#settings()
|
||||||
|
|
||||||
|
call s:check('has wikis list', has_key(s:cfg, 'wikis'))
|
||||||
|
call s:check('two wikis', has_key(s:cfg, 'wikis') && len(s:cfg.wikis) == 2)
|
||||||
|
let s:w0 = get(s:cfg, 'wikis', [{}])[0]
|
||||||
|
let s:w1 = get(s:cfg, 'wikis', [{},{}])[1]
|
||||||
|
call s:check('w0 path->root', get(s:w0, 'root', '') ==# '~/.vimwiki/personal_wiki')
|
||||||
|
call s:check('w0 path_html->html_path', get(s:w0, 'html_path', '') ==# '~/public_html/personal_wiki')
|
||||||
|
call s:check('w0 template_default', get(s:w0, 'template_default', '') ==# 'default')
|
||||||
|
call s:check('w0 auto_export', get(s:w0, 'auto_export', 0) == 1)
|
||||||
|
call s:check('w0 toc_header_level=2', get(s:w0, 'toc_header_level', 0) == 2)
|
||||||
|
call s:check('w0 html_numbering=2', get(s:w0, 'html_header_numbering', 0) == 2)
|
||||||
|
call s:check('w0 numbering_sym', get(s:w0, 'html_header_numbering_sym', '') ==# ' -')
|
||||||
|
call s:check('w1 root', get(s:w1, 'root', '') ==# '~/.vimwiki/ifood_wiki')
|
||||||
|
call s:check('w1 globals applied', get(s:w1, 'toc_header_level', 0) == 2)
|
||||||
|
|
||||||
|
" The buffer-side commands (e.g. <Leader>ww) must resolve the SAME wikis from
|
||||||
|
" g:vimwiki_list — not just the LSP payload. Regression guard for "<Leader>ww
|
||||||
|
" opens an empty ~/vimwiki".
|
||||||
|
let s:wl = nuwiki#commands#wiki_list()
|
||||||
|
call s:check('wiki_list sees vimwiki config', len(s:wl) == 2)
|
||||||
|
call s:check('wiki_list w0 root', len(s:wl) >= 1 && s:wl[0].root ==# expand('~/.vimwiki/personal_wiki'))
|
||||||
|
call s:check('wiki_list w1 root', len(s:wl) >= 2 && s:wl[1].root ==# expand('~/.vimwiki/ifood_wiki'))
|
||||||
|
|
||||||
|
" ----- nuwiki-native config wins over vimwiki -----
|
||||||
|
let g:nuwiki_wikis = [{'root': '~/native', 'name': 'native'}]
|
||||||
|
let s:cfg2 = nuwiki#lsp#settings()
|
||||||
|
call s:check('native g:nuwiki_wikis wins',
|
||||||
|
\ len(get(s:cfg2, 'wikis', [])) == 1
|
||||||
|
\ && get(s:cfg2.wikis[0], 'root', '') ==# '~/native')
|
||||||
|
unlet g:nuwiki_wikis
|
||||||
|
|
||||||
|
" ----- nuwiki-native scalar overrides the vimwiki global -----
|
||||||
|
let g:vimwiki_list = []
|
||||||
|
let g:nuwiki_toc_header_level = 4
|
||||||
|
let s:cfg3 = nuwiki#lsp#settings()
|
||||||
|
" No wikis list now (vimwiki_list empty) → scalar folded into top-level.
|
||||||
|
call s:check('native scalar override', get(s:cfg3, 'toc_header_level', 0) == 4)
|
||||||
|
unlet g:nuwiki_toc_header_level
|
||||||
|
|
||||||
|
" ----- global shorthand on native g:nuwiki_wikis (vimwiki-style) -----
|
||||||
|
" A global default applies to every wiki; a per-wiki value overrides it.
|
||||||
|
let g:nuwiki_toc_header_level = 2
|
||||||
|
let g:nuwiki_html_header_numbering = 2
|
||||||
|
let g:nuwiki_wikis = [
|
||||||
|
\ {'root': '~/w1', 'name': 'One'},
|
||||||
|
\ {'root': '~/w2', 'name': 'Two', 'toc_header_level': 3},
|
||||||
|
\ ]
|
||||||
|
let s:cfg4 = nuwiki#lsp#settings()
|
||||||
|
let s:g0 = s:cfg4.wikis[0]
|
||||||
|
let s:g1 = s:cfg4.wikis[1]
|
||||||
|
call s:check('global folds into wiki 0', get(s:g0, 'toc_header_level', 0) == 2)
|
||||||
|
call s:check('global numbering folds', get(s:g0, 'html_header_numbering', 0) == 2)
|
||||||
|
call s:check('per-wiki override wins', get(s:g1, 'toc_header_level', 0) == 3)
|
||||||
|
call s:check('global still folds elsewhere', get(s:g1, 'html_header_numbering', 0) == 2)
|
||||||
|
" The user's original dict must not be mutated by the fold.
|
||||||
|
call s:check('no mutation of g:nuwiki_wikis', !has_key(g:nuwiki_wikis[0], 'toc_header_level'))
|
||||||
|
|
||||||
|
call writefile(s:out, $NUWIKI_VWC_OUT)
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
# vimwiki → nuwiki Parity Gaps
|
|
||||||
|
|
||||||
Tracking document for closing the gap between nuwiki and the original
|
|
||||||
[vimwiki](https://github.com/vimwiki/vimwiki) plugin across **configuration**,
|
|
||||||
**commands**, and **key mappings**.
|
|
||||||
|
|
||||||
Source of truth for upstream: vimwiki `master` —
|
|
||||||
`autoload/vimwiki/vars.vim` (option defaults), `plugin/vimwiki.vim` +
|
|
||||||
`ftplugin/vimwiki.vim` (commands/mappings), `doc/vimwiki.txt` (prose).
|
|
||||||
|
|
||||||
Audited 2026-05-31. Check items off as they land. Each row cites the nuwiki
|
|
||||||
fix site.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P1 — Core workflow gaps (fix first)
|
|
||||||
|
|
||||||
- [x] **Vim client split/tab link-follow commands** — `VimwikiSplitLink`,
|
|
||||||
`VimwikiVSplitLink`, `VimwikiGoBackLink`, `VimwikiTabnewLink`,
|
|
||||||
`VimwikiTabDropLink` exist only in the Neovim branch. Vim users lose
|
|
||||||
split-window link following and back-navigation.
|
|
||||||
_Fix:_ `ftplugin/vimwiki.vim` (Vim branch) — commands added + `:Nuwiki*`
|
|
||||||
aliases; `follow_link_drop()` helper in `autoload/nuwiki/commands.vim`;
|
|
||||||
true `:tab drop` tab-reuse wired in both clients (`lua/nuwiki/commands.lua`
|
|
||||||
`open_uri` `'tabdrop'` case); `<C-S-CR>` repointed to tab-drop.
|
|
||||||
- [x] **`gl<symbol>` change-symbol mappings** — upstream's one-key bullet/number
|
|
||||||
symbol change (`gl*` `gl#` `gl-` `gl1` `gla` `gli` … plus `gL…`) has no
|
|
||||||
nuwiki keymap; only `:…ChangeSymbol*` commands exist.
|
|
||||||
_Fix:_ added normal-mode `gl{-,*,#,1,i,I,a,A}` (item) + `gL…` (whole list) in
|
|
||||||
`lua/nuwiki/keymaps.lua` lists group and `ftplugin/vimwiki.vim` Vim-branch
|
|
||||||
lists block, wired to the existing `list_change_symbol`. `1)` stays
|
|
||||||
command-only (upstream shadows it with `1.`); normal-mode only (server acts on
|
|
||||||
cursor item / whole list, not a visual range).
|
|
||||||
- [x] **`gl<Space>` / `gL<Space>` semantics diverge** — upstream = *remove
|
|
||||||
checkbox* from item / list siblings; nuwiki rebound to *remove done items*.
|
|
||||||
Same keys, different effect (parity trap).
|
|
||||||
_Fix:_ bound **bare `gl`/`gL`** to remove-checkbox-item / -in-list for exact
|
|
||||||
upstream parity (`lua/nuwiki/keymaps.lua` lists block, `ftplugin/vimwiki.vim`
|
|
||||||
Vim-branch lists block); they share the `gl…` prefix so they fire after
|
|
||||||
`timeoutlen`, just like upstream. Remove-*done* is now command-only:
|
|
||||||
`:NuwikiRemoveDone` (current list) gained a `-bang` so `:NuwikiRemoveDone!`
|
|
||||||
reaches the whole-buffer sweep that `gL<Space>` previously held (the only
|
|
||||||
prior entry point). Normal-mode only.
|
|
||||||
|
|
||||||
## Regressions (reported by users — verify, then fix)
|
|
||||||
|
|
||||||
- [x] **`<CR>` link-follow: wrong window placement + no create-on-follow on the
|
|
||||||
Vim clients** — two related Vim-branch bugs (Neovim path was already correct):
|
|
||||||
(a) on coc.nvim, `<CR>` opened the target in a **new tab** instead of the
|
|
||||||
current window; (b) on **both** vim-lsp and coc, `<CR>` on a link to a
|
|
||||||
not-yet-created page **failed to open/create it**.
|
|
||||||
_Root cause:_ the Vim follow path delegated to the LSP client's jump UI
|
|
||||||
(`:LspDefinition` / `CocActionAsync('jumpDefinition')`). Those fetch the
|
|
||||||
target's text before jumping — vim-lsp `readfile()`s it for the quickfix entry
|
|
||||||
(`autoload/lsp/utils/location.vim`), which throws `E484` for a missing file
|
|
||||||
and aborts the jump (→ no create-on-follow); coc, lacking an explicit open
|
|
||||||
command, fell back to the user's `coc.preferences.jumpCommand` (→ new tab).
|
|
||||||
The server itself was correct throughout — verified over JSON-RPC that
|
|
||||||
`textDocument/definition` returns a *synthesised* location for missing pages
|
|
||||||
and resolves existing links.
|
|
||||||
_Fix:_ stop delegating. `nuwiki#commands#follow_link_or_create()` /
|
|
||||||
`follow_link_drop()` now resolve `textDocument/definition` directly and open
|
|
||||||
the URI ourselves via a shared `s:open_definition(open_cmd)` helper
|
|
||||||
(`autoload/nuwiki/commands.vim`) — `:edit` for `<CR>`, `tab drop` for
|
|
||||||
`<C-S-CR>`. `:edit` opens a buffer for a missing path (save creates it,
|
|
||||||
matching vimwiki) and gives exact current-window placement regardless of the
|
|
||||||
user's coc jumpCommand. Covered by
|
|
||||||
`cr.follow_opens_missing_page_in_current_window` in
|
|
||||||
`development/tests/test-keymaps-vim.vim`; manually reproducible via
|
|
||||||
`development/start-vim.sh` (vim-lsp) and `development/start-vim-coc.sh` (coc).
|
|
||||||
- [x] **coc.nvim reported valid links as broken / couldn't resolve** — only via
|
|
||||||
the new `development/start-vim-coc.sh` launcher: its generated
|
|
||||||
`coc-settings.json` registered the server with `command`/`filetypes` but no
|
|
||||||
`initializationOptions`, so `wiki_root` never reached the server and it
|
|
||||||
resolved links against its default (`~/vimwiki`). _Fix:_ the launcher now
|
|
||||||
emits `initializationOptions` **and** `settings.nuwiki` mirroring the vim-lsp
|
|
||||||
client (`autoload/nuwiki/lsp.vim` `s:settings()`). Verified with a headless
|
|
||||||
coc.nvim run: `[[Notes]]` resolves, only genuinely-missing links warn. (Shipped
|
|
||||||
plugin only — no production code change; coc users configure their own
|
|
||||||
`coc-settings.json` per the README snippet.)
|
|
||||||
|
|
||||||
## P2 — Moderate (real users hit these)
|
|
||||||
|
|
||||||
- [ ] **`<Leader>w<Leader>t` collision** — upstream = today-in-new-tab; nuwiki =
|
|
||||||
tomorrow's diary. _Fix:_ `lua/nuwiki/keymaps.lua`, `ftplugin/vimwiki.vim`.
|
|
||||||
- [ ] **Generated-section captions hardcoded** — `toc_header` /
|
|
||||||
`toc_header_level`, `links_header`, `tags_header` (+levels).
|
|
||||||
_Fix:_ `crates/nuwiki-lsp/src/config.rs` + server TOC/generate renderers.
|
|
||||||
- [ ] **On-save autoregen family** — `auto_diary_index`, `auto_generate_links`,
|
|
||||||
`auto_generate_tags`, `auto_tags` (only `auto_toc` / `auto_export` exist).
|
|
||||||
_Fix:_ `config.rs` `RawWiki`/`WikiConfig` + the `didSave` path.
|
|
||||||
- [ ] **`listsym_rejected`** — rejected glyph `-` is hardcoded.
|
|
||||||
_Fix:_ `config.rs` `WikiConfig`.
|
|
||||||
- [ ] **`custom_wiki2html` (+`_args`)** and **`base_url`** — external HTML
|
|
||||||
converter hook + export URL prefix. _Fix:_ `config.rs` `HtmlConfig`.
|
|
||||||
- [ ] **`map_prefix`** — `<Leader>w` hardcoded across the map layer.
|
|
||||||
_Fix:_ `plugin/nuwiki.vim`, `lua/nuwiki/keymaps.lua`, `ftplugin/vimwiki.vim`.
|
|
||||||
- [ ] **`<M-CR>` badd-link** — missing (badd is mouse-only via `<MiddleMouse>` /
|
|
||||||
`:VimwikiBaddLink`). _Fix:_ `lua/nuwiki/keymaps.lua`, `ftplugin/vimwiki.vim`.
|
|
||||||
- [ ] **`diary_caption_level` default divergence** — nuwiki `1` vs vimwiki `0`.
|
|
||||||
_Fix:_ `config.rs:291` (and defaults in `lua/nuwiki/config.lua`).
|
|
||||||
- [ ] **`diary_start_week_day`** — **wrongly removed** in `c63ec67`. The removal
|
|
||||||
treated it as a server concern and hardwired the weekly diary to ISO-Monday
|
|
||||||
(`crates/nuwiki-core/src/date.rs` `monday_of_iso_week`), but choosing which
|
|
||||||
weekday the diary week begins on is a **client-side concern** and it **matters
|
|
||||||
for parity** — non-Monday users lose the upstream option entirely. _Fix:_
|
|
||||||
restore the config key client-side (`lua/nuwiki/config.lua`) and thread the
|
|
||||||
week-start through to the diary date logic instead of hardcoding Monday.
|
|
||||||
|
|
||||||
## P3 — Niche / low impact
|
|
||||||
|
|
||||||
### Commands
|
|
||||||
- [ ] `VimwikiVar` — config-var get/set introspection. _Fix:_ `plugin/nuwiki.vim`.
|
|
||||||
- [ ] `VimwikiShowVersion` — print version (trivial). _Fix:_ `plugin/nuwiki.vim`.
|
|
||||||
- [ ] `VimwikiReturn` — no Ex-command (mapping-driven only). _Fix:_ `ftplugin/vimwiki.vim`.
|
|
||||||
- [ ] `VimwikiTableAlignQ` vs `AlignW` collapsed to one `table_align` — `gqq`
|
|
||||||
(align) vs `gww` (align w/o resize) distinction lost.
|
|
||||||
- [ ] `VimwikiTable` is `-nargs=1` vs upstream `-nargs=*` (can't pass cols+rows).
|
|
||||||
- [ ] `VimwikiListChangeLvl` is `-nargs=?` vs upstream `-range -nargs=+`.
|
|
||||||
- [ ] `VimwikiRebuildTags` lacks `-bang` ("rebuild all" variant).
|
|
||||||
- [ ] `:VimwikiSearch` / `VWS` uses `lvimgrep`, not vimwiki's search engine.
|
|
||||||
- [ ] `VimwikiIndex` family is buffer-local in nuwiki (upstream defines globally
|
|
||||||
in `plugin/`); only `:…UISelect` is a global entry point.
|
|
||||||
- [ ] **`VimwikiColorize` / `NuwikiColorize` drop their argument in the Neovim
|
|
||||||
branch** — both are `-nargs=1`, but the Neovim defs call `colorize()` with no
|
|
||||||
`<q-args>` (`ftplugin/vimwiki.vim:445,517`) while the Vim branch passes it
|
|
||||||
(`:106,177`). The color name is silently ignored under Neovim (real bug, not
|
|
||||||
just parity). _Fix:_ `ftplugin/vimwiki.vim` Neovim branch.
|
|
||||||
- [ ] **`VimwikiTableMoveColumn{Left,Right}` dispatch to different backing names
|
|
||||||
per client** — Vim branch → `table_move_left/right`; Neovim branch →
|
|
||||||
`table_move_column_left/right`. Harmless today (each name exists in its own
|
|
||||||
impl) but a divergence to converge.
|
|
||||||
- [ ] **`NuwikiTabIndex` lacks `-count` in the Neovim branch** (`:452`) while its
|
|
||||||
siblings (`VimwikiTabIndex`, `NuwikiIndex`, and the Vim-branch `NuwikiTabIndex`)
|
|
||||||
all carry it — `vim.v.count` is still read, so a count is honored inconsistently.
|
|
||||||
- [ ] **`VimwikiToggleListItem` / `Increment` / `DecrementListItem` lack
|
|
||||||
`-range`** (both branches) vs upstream's `-range` (`:350`) — no visual-range
|
|
||||||
checkbox toggle / symbol cycle.
|
|
||||||
|
|
||||||
### Config
|
|
||||||
- [ ] `auto_header` — auto H1-from-filename on new page (server-side).
|
|
||||||
- [ ] `create_link` — toggle to suppress link-target creation.
|
|
||||||
- [ ] `dir_link` — index file to open when following a directory link.
|
|
||||||
- [ ] `auto_chdir` — `:cd` into wiki root on open (client-side).
|
|
||||||
- [ ] `bullet_types` / `cycle_bullets` — custom bullet glyphs + wrap-around.
|
|
||||||
- [ ] `commentstring` (`'%%%s'`) — comment toggling/text-objects.
|
|
||||||
- [ ] `color_tag_template` — `color_dic` present; template regex missing.
|
|
||||||
- [ ] `rss_max_items` / `rss_name` — `:Rss` output hardcoded. _Fix:_ `HtmlConfig`.
|
|
||||||
- [ ] `emoji_enable` — emoji substitution.
|
|
||||||
- [ ] `html_header_numbering` (+`_sym`) — numbered HTML headers.
|
|
||||||
- [ ] `valid_html_tags` — allowed inline HTML tags in export.
|
|
||||||
- [ ] `generated_links_caption`, `toc_link_format`, `markdown_link_ext`.
|
|
||||||
- [ ] `list_ignore_newline` / `text_ignore_newline` — export newline handling.
|
|
||||||
|
|
||||||
### Mappings
|
|
||||||
- [ ] Visual `<CR>` normalize-link (visual `+` covers the same intent).
|
|
||||||
- [ ] Insert `<S-CR>` multiline list item.
|
|
||||||
- [ ] `gLH` / `gLL` / `gLR` — upstream binds these as case-variant **aliases**
|
|
||||||
of `gLh` / `gLl` / `gLr` (same actions: dedent / indent whole item, renumber
|
|
||||||
all lists; upstream `ftplugin/vimwiki.vim:553,555,561`). nuwiki has the
|
|
||||||
lowercase forms but not the uppercase-suffix aliases. Cosmetic — same effect,
|
|
||||||
redundant keys.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Intentional divergences (no action — architectural)
|
|
||||||
|
|
||||||
These upstream options have no nuwiki equivalent by design, because nuwiki is
|
|
||||||
an LSP-backed reimplementation rather than a pure-VimL plugin. Listed so future
|
|
||||||
audits don't re-flag them.
|
|
||||||
|
|
||||||
- `nested_syntaxes` / `automatic_nested_syntaxes` — code-fence languages are
|
|
||||||
auto-detected from the fence tag (`syntax/vimwiki.vim`); always on, no toggle.
|
|
||||||
- `maxhi` — existence-based link highlighting; superseded by LSP diagnostics.
|
|
||||||
- `conceal*` (`conceallevel`, `conceal_onechar_markers`, …) — semantic tokens
|
|
||||||
replace conceal.
|
|
||||||
- `ext2syntax` — syntax chosen by extension / `syntax` key automatically.
|
|
||||||
- `folding` (upstream string) — nuwiki uses `folding = 'lsp' | 'expr' | 'off'`
|
|
||||||
backed by LSP foldingRange.
|
|
||||||
- `key_mappings` (dict) — replaced by Lua `mappings.<group>` + the
|
|
||||||
`g:nuwiki_no_<group>_mappings` globals.
|
|
||||||
- `CJK_length`, `listing_hl*`, `schemes_*`, `w32_dir_enc`, `menu`,
|
|
||||||
`rx_todo` / `tag_format` — syntax internals, menu, or Vim/Win shims.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Audit confidence: upstream defaults pulled from vimwiki `master`; a couple of
|
|
||||||
values (`CJK_length`, `links_space_char`) were summarized loosely but don't
|
|
||||||
affect the gap list.
|
|
||||||
- nuwiki adds some commands with no upstream equivalent (e.g.
|
|
||||||
`:NuwikiFindOrphans`) — additive, not divergences.
|
|
||||||
+295
-23
@@ -103,6 +103,7 @@ default): >
|
|||||||
file_extension = '.wiki',
|
file_extension = '.wiki',
|
||||||
syntax = 'vimwiki',
|
syntax = 'vimwiki',
|
||||||
log_level = 'warn',
|
log_level = 'warn',
|
||||||
|
map_prefix = '<Leader>w',
|
||||||
})
|
})
|
||||||
|
|
||||||
<For multiple wikis or per-wiki tuning, use the `wikis` list: >
|
<For multiple wikis or per-wiki tuning, use the `wikis` list: >
|
||||||
@@ -132,6 +133,7 @@ plugin: >
|
|||||||
let g:nuwiki_wiki_root = '~/vimwiki'
|
let g:nuwiki_wiki_root = '~/vimwiki'
|
||||||
let g:nuwiki_file_extension = '.wiki'
|
let g:nuwiki_file_extension = '.wiki'
|
||||||
let g:nuwiki_log_level = 'warn'
|
let g:nuwiki_log_level = 'warn'
|
||||||
|
let g:nuwiki_map_prefix = '<Leader>w'
|
||||||
|
|
||||||
<Top-level options ~
|
<Top-level options ~
|
||||||
|
|
||||||
@@ -152,6 +154,12 @@ plugin: >
|
|||||||
`log_level` warn
|
`log_level` warn
|
||||||
`error` | `warn` | `info` | `debug`. Forwarded to the language server.
|
`error` | `warn` | `info` | `debug`. Forwarded to the language server.
|
||||||
|
|
||||||
|
*nuwiki-config-map-prefix*
|
||||||
|
`map_prefix` '<Leader>w'
|
||||||
|
Prefix for the wiki command family (`<prefix>w`, `<prefix>t`,
|
||||||
|
`<prefix><Leader>w`, …). Mirrors vimwiki's `g:vimwiki_map_prefix`. Vim
|
||||||
|
users set |g:nuwiki_map_prefix| instead.
|
||||||
|
|
||||||
*nuwiki-config-folding*
|
*nuwiki-config-folding*
|
||||||
`folding` 'lsp'
|
`folding` 'lsp'
|
||||||
`lsp` uses the server's `foldingRange` provider (Neovim 0.11+ wires
|
`lsp` uses the server's `foldingRange` provider (Neovim 0.11+ wires
|
||||||
@@ -170,6 +178,23 @@ plugin: >
|
|||||||
`diagnostic.link_severity` `'warn'`
|
`diagnostic.link_severity` `'warn'`
|
||||||
Severity of broken-link diagnostics: `off`, `hint`, `warn`, `error`.
|
Severity of broken-link diagnostics: `off`, `hint`, `warn`, `error`.
|
||||||
|
|
||||||
|
*g:nuwiki_auto_chdir*
|
||||||
|
`auto_chdir` false
|
||||||
|
`:lcd` into the owning wiki's root when a wiki buffer becomes current.
|
||||||
|
Mirrors vimwiki's `auto_chdir`. Vim users set |g:nuwiki_auto_chdir|.
|
||||||
|
|
||||||
|
*g:nuwiki_auto_header*
|
||||||
|
`auto_header` false
|
||||||
|
Insert a level-1 header derived from the filename on new (empty) wiki
|
||||||
|
pages. Honours `links_space_char` (so `My_Page.wiki` → `= My Page =`).
|
||||||
|
Mirrors vimwiki's `auto_header`. Vim users set |g:nuwiki_auto_header|.
|
||||||
|
|
||||||
|
*g:nuwiki_no_calendar*
|
||||||
|
`use_calendar` true
|
||||||
|
When true (default), wire up calendar-vim integration if it is on the
|
||||||
|
runtimepath (`g:calendar_action` / `g:calendar_sign`). Set to false, or
|
||||||
|
define |g:nuwiki_no_calendar| before setup(), to opt out.
|
||||||
|
|
||||||
Per-wiki options ~
|
Per-wiki options ~
|
||||||
|
|
||||||
Every option in this list lives on a `wikis[i] = {...}` entry. Defaults
|
Every option in this list lives on a `wikis[i] = {...}` entry. Defaults
|
||||||
@@ -182,19 +207,85 @@ match upstream vimwiki.
|
|||||||
`diary_rel_path` `'diary'`
|
`diary_rel_path` `'diary'`
|
||||||
`diary_index` `'diary'` — stem of the diary index page.
|
`diary_index` `'diary'` — stem of the diary index page.
|
||||||
`diary_frequency` `'daily'` | `'weekly'` | `'monthly'` | `'yearly'`
|
`diary_frequency` `'daily'` | `'weekly'` | `'monthly'` | `'yearly'`
|
||||||
`diary_caption_level` `1`
|
`diary_weekly_style` `'iso'` (YYYY-Www) | `'date'`/`'vimwiki'` (week-start
|
||||||
|
YYYY-MM-DD). Weekly diaries only.
|
||||||
|
`diary_start_week_day` `'monday'`..`'sunday'`; week start used when
|
||||||
|
`diary_weekly_style = 'date'`.
|
||||||
|
`diary_caption_level` `0`
|
||||||
`diary_sort` `'desc'` | `'asc'`
|
`diary_sort` `'desc'` | `'asc'`
|
||||||
`diary_header` `'Diary'`
|
`diary_header` `'Diary'`
|
||||||
|
`diary_months` English month names — month-number → display name
|
||||||
|
used in the diary index headings.
|
||||||
|
`create_link` `true` — create the target page when following a link
|
||||||
|
to a missing page; `false` makes the follow a no-op.
|
||||||
|
`dir_link` `''` — index stem opened when following a link to a
|
||||||
|
directory (e.g. `'index'`); empty opens the directory.
|
||||||
|
`bullet_types` `['-', '*', '#']` — unordered-bullet glyphs for this
|
||||||
|
wiki; drives `cycle_bullets`.
|
||||||
|
`cycle_bullets` `false` — rotate an unordered item's glyph through
|
||||||
|
`bullet_types` as it is indented/dedented.
|
||||||
|
`generated_links_caption` `false` — emit `[[page|Heading]]` (first heading as
|
||||||
|
caption) from `:VimwikiGenerateLinks`.
|
||||||
|
`toc_link_format` `0` — `0` = `[[#anchor|title]]` (with description);
|
||||||
|
`1` = `[[#anchor]]` (anchor only).
|
||||||
|
`table_reduce_last_col` `false` — don't pad the last table column to fill;
|
||||||
|
keep it at its minimum width.
|
||||||
`html_path` Path for `:Vimwiki2HTML` output.
|
`html_path` Path for `:Vimwiki2HTML` output.
|
||||||
`template_path` Path holding `<name>.tpl` templates.
|
`template_path` Path holding `<name>.tpl` templates.
|
||||||
`template_default` `'default'`
|
`template_default` `'default'`
|
||||||
`template_ext` `'.tpl'`
|
`template_ext` `'.tpl'`
|
||||||
|
`template_date_format` `'%Y-%m-%d'` — strftime format for `%date` in templates.
|
||||||
`css_name` `'style.css'`
|
`css_name` `'style.css'`
|
||||||
|
`html_filename_parameterization` `false` — encode subdir paths into the HTML
|
||||||
|
filename instead of nesting directories.
|
||||||
|
`color_dic` `{}` — map of `{ name = 'color' }` for `:NuwikiColorize`.
|
||||||
|
`color_tag_template` `'<span style="__STYLE__">__CONTENT__</span>'` — HTML
|
||||||
|
emitted for a `color_dic` colour span. `__STYLE__` →
|
||||||
|
`color:<css>`, `__CONTENT__` → the rendered inner HTML.
|
||||||
|
`valid_html_tags` `'b,i,s,u,sub,sup,kbd,br,hr,div,center,strong,em'` —
|
||||||
|
comma-separated inline HTML tags passed through export
|
||||||
|
unescaped (`span` is always allowed for colour spans).
|
||||||
|
`emoji_enable` `true` — substitute `:alias:` emoji shortcodes with
|
||||||
|
their glyph during HTML export.
|
||||||
|
`text_ignore_newline` `true` — a single newline inside a paragraph renders
|
||||||
|
as a space in HTML; `false` emits `<br>`.
|
||||||
|
`list_ignore_newline` `true` — a single newline inside a list item renders
|
||||||
|
as a space in HTML; `false` emits `<br>`.
|
||||||
|
`rss_name` `'rss.xml'` — filename of the generated diary RSS feed,
|
||||||
|
relative to `html_path`.
|
||||||
|
`rss_max_items` `10` — cap on the number of diary items in the RSS feed.
|
||||||
|
`user_htmls` `{}` — basenames of HTML files with no wiki source that
|
||||||
|
`:VimwikiAll2HTML` must not prune.
|
||||||
|
`custom_wiki2html` `''` — external converter command. When set, export
|
||||||
|
shells out to it instead of the built-in renderer,
|
||||||
|
called as: <cmd> <force> <syntax> <ext> <out_dir>
|
||||||
|
<in_file> <css> <tpl_path> <tpl_default> <tpl_ext>
|
||||||
|
<root_path> <args> (`-` for empty optionals), matching
|
||||||
|
vimwiki's `g:vimwiki_custom_wiki2html` contract.
|
||||||
|
`custom_wiki2html_args` `''` — extra args appended to the converter call.
|
||||||
|
`base_url` `''` — public URL prefix. When set, diary RSS item
|
||||||
|
links become <base_url><diary_rel_path>/<date>.html
|
||||||
|
instead of local `file://` paths.
|
||||||
`auto_export` `false` — export to HTML on save.
|
`auto_export` `false` — export to HTML on save.
|
||||||
`auto_toc` `false` — refresh TOC on save.
|
`auto_toc` `false` — refresh TOC on save.
|
||||||
|
`auto_generate_links` `false` — regen an existing Generated Links section
|
||||||
|
on save.
|
||||||
|
`auto_generate_tags` `false` — regen an existing Generated Tags index on
|
||||||
|
save.
|
||||||
|
`auto_diary_index` `false` — regen the diary index when a diary entry is
|
||||||
|
saved.
|
||||||
|
`toc_header` `'Contents'` (+ `toc_header_level` `1`) — :VimwikiTOC.
|
||||||
|
`links_header` `'Generated Links'` (+ `links_header_level` `1`).
|
||||||
|
`tags_header` `'Generated Tags'` (+ `tags_header_level` `1`).
|
||||||
`exclude_files` List of glob patterns excluded from export.
|
`exclude_files` List of glob patterns excluded from export.
|
||||||
`listsyms` `' .oOX'` — checkbox progression characters.
|
`listsyms` `' .oOX'` — checkbox progression characters.
|
||||||
|
`listsym_rejected` `'-'` — glyph for a cancelled checkbox `[-]`.
|
||||||
`listsyms_propagate` `true`
|
`listsyms_propagate` `true`
|
||||||
|
`list_margin` `-1` — spaces before a generated list bullet (TOC /
|
||||||
|
generated links). `-1` means "no extra margin": the
|
||||||
|
server can't read the editor's `shiftwidth`, so it
|
||||||
|
clamps negative values to `0`. Set a non-negative
|
||||||
|
number to force a fixed indent.
|
||||||
`links_space_char` `' '`
|
`links_space_char` `' '`
|
||||||
|
|
||||||
Code fences tagged with a language (`{{{python …}}}`) are highlighted with
|
Code fences tagged with a language (`{{{python …}}}`) are highlighted with
|
||||||
@@ -202,6 +293,16 @@ that language's syntax automatically — there is no `nested_syntaxes` key.
|
|||||||
|
|
||||||
Vim-specific globals ~
|
Vim-specific globals ~
|
||||||
|
|
||||||
|
*g:nuwiki_diary_rel_path*
|
||||||
|
`g:nuwiki_diary_rel_path` `'diary'`
|
||||||
|
Diary subdirectory, relative to the wiki root. The scalar form for the
|
||||||
|
single-wiki shape; per-wiki `diary_rel_path` (or `g:nuwiki_wikis`) wins.
|
||||||
|
|
||||||
|
*g:nuwiki_link_severity*
|
||||||
|
`g:nuwiki_link_severity` `'warn'`
|
||||||
|
Broken-link diagnostic severity: `off` | `hint` | `warn` | `error`. The
|
||||||
|
scalar/global form of the `diagnostic.link_severity` setup() option.
|
||||||
|
|
||||||
*g:nuwiki_no_default_mappings*
|
*g:nuwiki_no_default_mappings*
|
||||||
`g:nuwiki_no_default_mappings` 0
|
`g:nuwiki_no_default_mappings` 0
|
||||||
When 1, the Vim path skips every buffer-local keymap. Equivalent to
|
When 1, the Vim path skips every buffer-local keymap. Equivalent to
|
||||||
@@ -219,20 +320,60 @@ Vim-specific globals ~
|
|||||||
Per-subgroup opt-outs, each defaulting to 0. Setting one to 1 drops just
|
Per-subgroup opt-outs, each defaulting to 0. Setting one to 1 drops just
|
||||||
that group's buffer-local keymaps while leaving the rest in place; the
|
that group's buffer-local keymaps while leaving the rest in place; the
|
||||||
whole-layer |g:nuwiki_no_default_mappings| still wins over all of them.
|
whole-layer |g:nuwiki_no_default_mappings| still wins over all of them.
|
||||||
These mirror the Lua-side `mappings.<group>` booleans. Valid groups:
|
These mirror the Lua-side `mappings.<group>` booleans. Every binding in
|
||||||
`wiki_prefix` `<Leader>w*`
|
each group (the `<Leader>w` prefix follows |g:nuwiki_map_prefix|):
|
||||||
`links` `<CR>`, `<S-CR>`, `<Tab>`, `<BS>`, `+`, …
|
`wiki_prefix` `<Leader>ww`, `<Leader>wt`, `<Leader>ws`,
|
||||||
`lists` `<C-Space>`, `gl*`, `o`/`O`, insert-mode list edits
|
`<Leader>wi`, `<Leader>w<Leader>w`,
|
||||||
|
`<Leader>w<Leader>y`, `<Leader>w<Leader>t`,
|
||||||
|
`<Leader>w<Leader>m`, `<Leader>w<Leader>i`
|
||||||
|
`links` `<CR>`, `<S-CR>`, `<C-CR>`, `<C-S-CR>`, `<D-CR>`,
|
||||||
|
`<M-CR>`, `<BS>`, `<Tab>`, `<S-Tab>`, `+` (n/x),
|
||||||
|
`<CR>` (x: normalize selection),
|
||||||
|
`<Leader>wn`, `<Leader>wd`, `<Leader>wr`,
|
||||||
|
`<Leader>wc` (n/x)
|
||||||
|
`lists` `<C-Space>` (also `<C-@>`/`<Nul>`), `gnt`, `gln`,
|
||||||
|
`glp`, `glx`, `glh`, `gll`, `gLh`, `gLl`, `glr`,
|
||||||
|
`gLr`, `gl-`, `gl*`, `gl#`, `gl1`, `gli`, `glI`,
|
||||||
|
`gla`, `glA`, `gL-`, `gL*`, `gL#`, `gL1`, `gLi`,
|
||||||
|
`gLI`, `gLa`, `gLA`, `gl`, `gL`, `o`, `O`; insert
|
||||||
|
`<C-D>`, `<C-T>`, `<C-L><C-J>`, `<C-L><C-K>`,
|
||||||
|
`<C-L><C-M>`, `<CR>`, `<S-CR>`
|
||||||
`headers` `=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u`
|
`headers` `=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u`
|
||||||
`table_editing` `gqq`, `<A-Left>`, `<A-Right>`, insert `<Tab>`
|
`table_editing` `gqq`, `gq1`, `gww`, `gw1`, `<A-Left>`,
|
||||||
|
`<A-Right>`; insert `<Tab>`, `<S-Tab>`
|
||||||
`diary` `<C-Down>`, `<C-Up>`
|
`diary` `<C-Down>`, `<C-Up>`
|
||||||
`html_export` `<Leader>wh`, `<Leader>whh`, `<Leader>wha`
|
`html_export` `<Leader>wh`, `<Leader>whh`, `<Leader>wha`
|
||||||
`text_objects` `ah`, `ih`, `al`, `il`, `a\`, `i\`, `ac`, `ic`
|
`text_objects` `ah`, `ih`, `aH`, `iH`, `al`, `il`, `a\`, `i\`,
|
||||||
|
`ac`, `ic`
|
||||||
|
`mouse` `<2-LeftMouse>`, `<S-2-LeftMouse>`,
|
||||||
|
`<C-2-LeftMouse>`, `<MiddleMouse>`, `<RightMouse>`
|
||||||
|
|
||||||
|
*g:nuwiki_map_prefix*
|
||||||
|
`g:nuwiki_map_prefix` `'<Leader>w'`
|
||||||
|
Prefix for the wiki command family — both the global entry-point maps
|
||||||
|
(`<prefix>w`, `<prefix>t`, `<prefix>s`, `<prefix>i`, `<prefix><Leader>{w,y,
|
||||||
|
t,m,i}`) and the buffer-local ones (`<prefix>{n,d,r,c,h,hh,ha}`). Mirrors
|
||||||
|
vimwiki's `g:vimwiki_map_prefix`. Set it before the plugin loads. Neovim
|
||||||
|
users set the `map_prefix` option in `setup()` instead.
|
||||||
|
|
||||||
*g:nuwiki_no_folding*
|
*g:nuwiki_no_folding*
|
||||||
`g:nuwiki_no_folding` 0
|
`g:nuwiki_no_folding` 0
|
||||||
When 1, the Vim path skips `foldexpr` / `foldmethod` setup.
|
When 1, the Vim path skips `foldexpr` / `foldmethod` setup.
|
||||||
|
|
||||||
|
*g:nuwiki_autowriteall*
|
||||||
|
`g:nuwiki_autowriteall` 1
|
||||||
|
When 1 (default), Vim's global `'autowriteall'` is set while a wiki
|
||||||
|
buffer is current and restored on leave, so a modified buffer is
|
||||||
|
auto-saved on page switch / `:make`. Mirrors vimwiki's `autowriteall`
|
||||||
|
and the Neovim `autowriteall` setup option. Set to 0 to leave
|
||||||
|
`'autowriteall'` untouched.
|
||||||
|
|
||||||
|
*g:nuwiki_table_auto_fmt*
|
||||||
|
`g:nuwiki_table_auto_fmt` 1
|
||||||
|
When 1 (default), the table under the cursor is re-aligned on
|
||||||
|
|InsertLeave|. Mirrors vimwiki's `table_auto_fmt` and the Neovim
|
||||||
|
`table_auto_fmt` setup option. Set to 0 to disable.
|
||||||
|
|
||||||
*g:nuwiki_mouse_mappings*
|
*g:nuwiki_mouse_mappings*
|
||||||
`g:nuwiki_mouse_mappings` 0
|
`g:nuwiki_mouse_mappings` 0
|
||||||
When 1, the Vim path registers mouse keymaps (double-click follows,
|
When 1, the Vim path registers mouse keymaps (double-click follows,
|
||||||
@@ -265,17 +406,36 @@ Wiki / navigation ~
|
|||||||
|
|
||||||
*:NuwikiIndex*
|
*:NuwikiIndex*
|
||||||
:NuwikiIndex [{count}]
|
:NuwikiIndex [{count}]
|
||||||
Open wiki N's index page (default: 1).
|
Open wiki N's index page (default: 1). Defined globally, so it works
|
||||||
|
from any buffer (not just inside a wiki).
|
||||||
|
|
||||||
*:NuwikiTabIndex*
|
*:NuwikiTabIndex*
|
||||||
:NuwikiTabIndex [{count}]
|
:NuwikiTabIndex [{count}]
|
||||||
Same, in a new tab.
|
Same, in a new tab. Also global.
|
||||||
|
|
||||||
*:NuwikiUISelect*
|
*:NuwikiUISelect*
|
||||||
:NuwikiUISelect
|
:NuwikiUISelect
|
||||||
Pick a wiki from a list (multi-wiki only). Uses |vim.ui.select| on
|
Pick a wiki from a list (multi-wiki only). Uses |vim.ui.select| on
|
||||||
Neovim, |inputlist()| on Vim.
|
Neovim, |inputlist()| on Vim.
|
||||||
|
|
||||||
|
*:NuwikiShowVersion*
|
||||||
|
:NuwikiShowVersion
|
||||||
|
Echo the nuwiki version (`g:nuwiki_version`) and the host editor.
|
||||||
|
Also available as `:VimwikiShowVersion`.
|
||||||
|
|
||||||
|
*:NuwikiVar*
|
||||||
|
:NuwikiVar [{key} [{value}]]
|
||||||
|
Get/set nuwiki client config. No argument prints the config (Vim:
|
||||||
|
every `g:nuwiki_*`; Neovim: the resolved |nuwiki-setup| options); one
|
||||||
|
argument prints that key; two or more sets it (server-backed settings
|
||||||
|
need a restart to take effect). Also `:VimwikiVar`.
|
||||||
|
|
||||||
|
*:NuwikiReturn*
|
||||||
|
:NuwikiReturn
|
||||||
|
Command form of the smart `<CR>` continuation: continue the current
|
||||||
|
list item / table row. Mostly used internally; the insert-mode `<CR>`
|
||||||
|
mapping does the same. Also `:VimwikiReturn`.
|
||||||
|
|
||||||
*:NuwikiGoto*
|
*:NuwikiGoto*
|
||||||
:NuwikiGoto {page}
|
:NuwikiGoto {page}
|
||||||
Open `{page}.wiki` by name.
|
Open `{page}.wiki` by name.
|
||||||
@@ -365,15 +525,30 @@ Page generation ~
|
|||||||
|
|
||||||
*:NuwikiTOC*
|
*:NuwikiTOC*
|
||||||
:NuwikiTOC
|
:NuwikiTOC
|
||||||
Generate or refresh the table of contents on the current page.
|
Generate or refresh the table of contents on the current page. A new TOC
|
||||||
|
is inserted at the cursor line; an existing one is refreshed in place.
|
||||||
|
|
||||||
*:NuwikiGenerateLinks*
|
*:NuwikiGenerateLinks*
|
||||||
:NuwikiGenerateLinks
|
:NuwikiGenerateLinks [path]
|
||||||
Insert a flat list of every page in the wiki under the cursor.
|
Insert a flat list of every page in the wiki. With a `path` argument, the
|
||||||
|
list is scoped to that subtree (a wiki-root-relative directory, or a glob
|
||||||
|
pattern containing `*`/`?`). A new section is inserted at the cursor line;
|
||||||
|
an existing one is refreshed in place.
|
||||||
|
|
||||||
*:NuwikiCheckLinks*
|
*:NuwikiCheckLinks*
|
||||||
:NuwikiCheckLinks
|
:[range]NuwikiCheckLinks
|
||||||
Send every broken link in the workspace to the |quickfix| list.
|
Send every broken link in the workspace to the |quickfix| list. With a
|
||||||
|
range (e.g. `:'<,'>NuwikiCheckLinks`) the report is limited to the current
|
||||||
|
buffer's selected lines.
|
||||||
|
|
||||||
|
*:NuwikiFindOrphans*
|
||||||
|
:NuwikiFindOrphans
|
||||||
|
List pages with no incoming links in the |quickfix| list.
|
||||||
|
|
||||||
|
*:NuwikiNormalizeLink*
|
||||||
|
:NuwikiNormalizeLink [0|1]
|
||||||
|
Turn the word (or, with arg `1`, the visual selection) under the cursor
|
||||||
|
into a wikilink. Backs the `+` mapping.
|
||||||
|
|
||||||
Tags ~
|
Tags ~
|
||||||
|
|
||||||
@@ -384,11 +559,13 @@ Tags ~
|
|||||||
*:NuwikiGenerateTagLinks*
|
*:NuwikiGenerateTagLinks*
|
||||||
:NuwikiGenerateTagLinks [tag]
|
:NuwikiGenerateTagLinks [tag]
|
||||||
Insert a section linking every page that has `<tag>`. With no
|
Insert a section linking every page that has `<tag>`. With no
|
||||||
argument, generates a section per tag found in the workspace.
|
argument, generates a section per tag found in the workspace. A new
|
||||||
|
section is inserted at the cursor line; an existing one is refreshed in
|
||||||
|
place.
|
||||||
|
|
||||||
*:NuwikiRebuildTags*
|
*:NuwikiRebuildTags*
|
||||||
:NuwikiRebuildTags
|
:NuwikiRebuildTags[!]
|
||||||
Force a full workspace re-index.
|
Re-index the current wiki. With `!`, re-index every configured wiki.
|
||||||
|
|
||||||
HTML export ~
|
HTML export ~
|
||||||
|
|
||||||
@@ -409,8 +586,86 @@ HTML export ~
|
|||||||
:NuwikiRss
|
:NuwikiRss
|
||||||
Write `rss.xml` summarising recent diary entries.
|
Write `rss.xml` summarising recent diary entries.
|
||||||
|
|
||||||
|
Lists, tasks & tables ~
|
||||||
|
|
||||||
|
*:NuwikiNextTask*
|
||||||
|
:NuwikiNextTask
|
||||||
|
Jump to the next unfinished task in the buffer.
|
||||||
|
|
||||||
|
*:NuwikiListToggle*
|
||||||
|
:NuwikiListToggle
|
||||||
|
Toggle a checkbox on the current item, adding `[ ]` if it has none.
|
||||||
|
|
||||||
|
*:NuwikiToggleListItem*
|
||||||
|
:[range]NuwikiToggleListItem
|
||||||
|
Toggle the checkbox under the cursor. With a range, every item in the
|
||||||
|
selection.
|
||||||
|
|
||||||
|
*:NuwikiToggleRejected*
|
||||||
|
:NuwikiToggleRejected
|
||||||
|
Toggle the rejected marker `[-]` on the current item.
|
||||||
|
|
||||||
|
*:NuwikiIncrementListItem* *:NuwikiDecrementListItem*
|
||||||
|
:[range]NuwikiIncrementListItem
|
||||||
|
:[range]NuwikiDecrementListItem
|
||||||
|
Cycle the current item's list marker to the next / previous symbol.
|
||||||
|
With a range, every item in the selection.
|
||||||
|
|
||||||
|
*:NuwikiChangeSymbol* *:NuwikiChangeSymbolInList*
|
||||||
|
:NuwikiChangeSymbol {sym}
|
||||||
|
:NuwikiChangeSymbolInList {sym}
|
||||||
|
Set the current item's marker to `{sym}` — or every item in the list,
|
||||||
|
for the `InList` form.
|
||||||
|
|
||||||
|
*:NuwikiListChangeLvl*
|
||||||
|
:NuwikiListChangeLvl [increase|decrease]
|
||||||
|
Indent (`increase`) or dedent (`decrease`) the current list item.
|
||||||
|
|
||||||
|
*:NuwikiRemoveDone*
|
||||||
|
:NuwikiRemoveDone[!]
|
||||||
|
Remove every completed item from the current list; with `!`, from the
|
||||||
|
whole buffer.
|
||||||
|
|
||||||
|
*:NuwikiRemoveCheckbox* *:NuwikiRemoveCheckboxInList*
|
||||||
|
:[range]NuwikiRemoveCheckbox
|
||||||
|
:NuwikiRemoveCheckboxInList
|
||||||
|
Strip the checkbox from the current item (or the range), or from every
|
||||||
|
item in the current list for the `InList` form.
|
||||||
|
|
||||||
|
*:NuwikiRenumberList* *:NuwikiRenumberAllLists*
|
||||||
|
:NuwikiRenumberList
|
||||||
|
:NuwikiRenumberAllLists
|
||||||
|
Renumber the current ordered list, or every ordered list in the buffer.
|
||||||
|
|
||||||
|
*:NuwikiTable*
|
||||||
|
:NuwikiTable {cols} [rows]
|
||||||
|
Insert a table with `{cols}` columns (and optional `[rows]`) at the cursor.
|
||||||
|
|
||||||
|
*:NuwikiTableAlign*
|
||||||
|
:NuwikiTableAlign
|
||||||
|
Re-align the current table's columns.
|
||||||
|
|
||||||
|
*:NuwikiTableMoveColumnLeft* *:NuwikiTableMoveColumnRight*
|
||||||
|
:NuwikiTableMoveColumnLeft
|
||||||
|
:NuwikiTableMoveColumnRight
|
||||||
|
Move the table column under the cursor left / right.
|
||||||
|
|
||||||
Other ~
|
Other ~
|
||||||
|
|
||||||
|
*:NuwikiColorize*
|
||||||
|
:[range]NuwikiColorize {color}
|
||||||
|
Wrap the word (or the visual selection) under the cursor in a coloured
|
||||||
|
`<span>`. `{color}` may be a `color_dic` name or a literal CSS colour.
|
||||||
|
|
||||||
|
*:NuwikiPasteLink* *:NuwikiPasteUrl*
|
||||||
|
:NuwikiPasteLink
|
||||||
|
:NuwikiPasteUrl
|
||||||
|
Paste the clipboard contents as a wikilink, or as a raw URL.
|
||||||
|
|
||||||
|
*:NuwikiCatUrl*
|
||||||
|
:NuwikiCatUrl
|
||||||
|
Echo the `file://` URL of the current page's HTML export.
|
||||||
|
|
||||||
*:NuwikiInstall*
|
*:NuwikiInstall*
|
||||||
:NuwikiInstall
|
:NuwikiInstall
|
||||||
Install (or re-install) the `nuwiki-ls` binary into the plugin's
|
Install (or re-install) the `nuwiki-ls` binary into the plugin's
|
||||||
@@ -432,7 +687,8 @@ Links (normal mode) ~
|
|||||||
<S-CR> Follow in a horizontal split.
|
<S-CR> Follow in a horizontal split.
|
||||||
<C-CR> Follow in a vertical split.
|
<C-CR> Follow in a vertical split.
|
||||||
<C-S-CR> Follow in a tab, reusing an existing tab if the file is
|
<C-S-CR> Follow in a tab, reusing an existing tab if the file is
|
||||||
already open.
|
already open. `<D-CR>` (macOS Cmd) is an alias.
|
||||||
|
<M-CR> Add the link target to the buffer list (`:badd`, no jump).
|
||||||
<BS> Jump back (`<C-o>`).
|
<BS> Jump back (`<C-o>`).
|
||||||
<Tab> Next wikilink on or after the cursor.
|
<Tab> Next wikilink on or after the cursor.
|
||||||
<S-Tab> Previous wikilink.
|
<S-Tab> Previous wikilink.
|
||||||
@@ -448,10 +704,10 @@ Lists (normal mode) ~
|
|||||||
gnt Jump to the next unfinished task.
|
gnt Jump to the next unfinished task.
|
||||||
glh Dedent the current list item.
|
glh Dedent the current list item.
|
||||||
gll Indent the current list item.
|
gll Indent the current list item.
|
||||||
gLh Dedent the item including its sublist.
|
gLh Dedent the item including its sublist. (alias: gLH)
|
||||||
gLl Indent the item including its sublist.
|
gLl Indent the item including its sublist. (alias: gLL)
|
||||||
glr Renumber the current ordered list.
|
glr Renumber the current ordered list.
|
||||||
gLr Renumber every ordered list in the buffer.
|
gLr Renumber every ordered list in the buffer. (alias: gLR)
|
||||||
gl<sym> Set the current item's marker to <sym>, where <sym> is one
|
gl<sym> Set the current item's marker to <sym>, where <sym> is one
|
||||||
of `-` `*` `#` `1` (1.) `i` (i)) `I` (I)) `a` (a)) `A` (A)).
|
of `-` `*` `#` `1` (1.) `i` (i)) `I` (I)) `a` (a)) `A` (A)).
|
||||||
For `1)` use `:NuwikiChangeSymbol 1)` (no single-key form).
|
For `1)` use `:NuwikiChangeSymbol 1)` (no single-key form).
|
||||||
@@ -486,12 +742,17 @@ Tables (normal mode) ~
|
|||||||
|
|
||||||
Wiki / diary / export ~
|
Wiki / diary / export ~
|
||||||
|
|
||||||
<Leader>ww Open today's diary.
|
The `<Leader>w` prefix below is the default |map_prefix| / |g:nuwiki_map_prefix|;
|
||||||
|
set that option to relocate the whole family.
|
||||||
|
|
||||||
|
<Leader>ww Open the wiki index.
|
||||||
|
<Leader>wt Open the wiki index in a new tab.
|
||||||
<Leader>ws Pick a wiki.
|
<Leader>ws Pick a wiki.
|
||||||
<Leader>wi Open the diary index.
|
<Leader>wi Open the diary index.
|
||||||
<Leader>w<Leader>w Today.
|
<Leader>w<Leader>w Today.
|
||||||
<Leader>w<Leader>y Yesterday.
|
<Leader>w<Leader>y Yesterday.
|
||||||
<Leader>w<Leader>t Tomorrow.
|
<Leader>w<Leader>t Today, in a new tab.
|
||||||
|
<Leader>w<Leader>m Tomorrow.
|
||||||
<Leader>w<Leader>i Rebuild the diary index page.
|
<Leader>w<Leader>i Rebuild the diary index page.
|
||||||
<Leader>wn Goto page (prompts for name).
|
<Leader>wn Goto page (prompts for name).
|
||||||
<Leader>wd Delete the current page.
|
<Leader>wd Delete the current page.
|
||||||
@@ -546,6 +807,10 @@ on context:
|
|||||||
same column count; cursor lands inside the first cell.
|
same column count; cursor lands inside the first cell.
|
||||||
* Otherwise — plain `<CR>`.
|
* Otherwise — plain `<CR>`.
|
||||||
|
|
||||||
|
Insert `<S-CR>` continues a list item as a *multiline* item: a new line
|
||||||
|
with no marker, indented to align under the item's text (or a plain
|
||||||
|
`<CR>` when not on a list item). Mirrors vimwiki's `<S-CR>`.
|
||||||
|
|
||||||
Table cell navigation ~
|
Table cell navigation ~
|
||||||
|
|
||||||
<Tab> Jump to the next cell. Past the last cell, insert a fresh
|
<Tab> Jump to the next cell. Past the last cell, insert a fresh
|
||||||
@@ -580,6 +845,13 @@ The `diary_frequency` per-wiki option controls the cadence:
|
|||||||
monthly YYYY-MM e.g. `2026-05.wiki`
|
monthly YYYY-MM e.g. `2026-05.wiki`
|
||||||
yearly YYYY e.g. `2026.wiki`
|
yearly YYYY e.g. `2026.wiki`
|
||||||
|
|
||||||
|
For weekly diaries, `diary_weekly_style` chooses the file name: the default
|
||||||
|
`'iso'` uses the ISO-week label above (always Monday-based); `'date'` (a.k.a.
|
||||||
|
`'vimwiki'`) names the file after the week-start day's date — e.g. with
|
||||||
|
`diary_start_week_day = 'monday'`, the week of 2026-05-27 is `2026-05-25.wiki`
|
||||||
|
— matching upstream vimwiki. `diary_start_week_day` (`'monday'`..`'sunday'`)
|
||||||
|
only applies in `'date'` mode.
|
||||||
|
|
||||||
`:NuwikiMakeDiaryNote` opens the entry for the current period at the
|
`:NuwikiMakeDiaryNote` opens the entry for the current period at the
|
||||||
configured cadence; the yesterday / tomorrow commands step back and
|
configured cadence; the yesterday / tomorrow commands step back and
|
||||||
forward by one period. `<C-Down>` / `<C-Up>` walk between indexed
|
forward by one period. `<C-Down>` / `<C-Up>` walk between indexed
|
||||||
|
|||||||
+227
-156
@@ -1,4 +1,4 @@
|
|||||||
" ftplugin/nuwiki.vim — per-buffer settings + command/keymap glue.
|
" ftplugin/vimwiki.vim — per-buffer settings + command/keymap glue.
|
||||||
"
|
"
|
||||||
" Wires up:
|
" Wires up:
|
||||||
" - basic edit-time options (commentstring, suffixesadd, …)
|
" - basic edit-time options (commentstring, suffixesadd, …)
|
||||||
@@ -12,7 +12,8 @@ if exists('b:did_ftplugin')
|
|||||||
endif
|
endif
|
||||||
let b:did_ftplugin = 1
|
let b:did_ftplugin = 1
|
||||||
|
|
||||||
setlocal commentstring=%%\ %s
|
" vimwiki's commentstring is `%%%s` (no space); match it exactly.
|
||||||
|
setlocal commentstring=%%%s
|
||||||
setlocal comments=:%%
|
setlocal comments=:%%
|
||||||
setlocal formatoptions+=ron
|
setlocal formatoptions+=ron
|
||||||
setlocal suffixesadd=.wiki
|
setlocal suffixesadd=.wiki
|
||||||
@@ -27,6 +28,52 @@ setlocal concealcursor=c
|
|||||||
|
|
||||||
let b:undo_ftplugin = 'setlocal commentstring< comments< formatoptions< suffixesadd< iskeyword< conceallevel< concealcursor<'
|
let b:undo_ftplugin = 'setlocal commentstring< comments< formatoptions< suffixesadd< iskeyword< conceallevel< concealcursor<'
|
||||||
|
|
||||||
|
" Conceal `:VimwikiColorize` spans down to their coloured text. New colours
|
||||||
|
" appear as the user colorizes, so refresh the syntax groups on edits (and now,
|
||||||
|
" for any spans already in the buffer). Both clients — it's pure syntax.
|
||||||
|
augroup NuwikiColorConceal
|
||||||
|
autocmd! * <buffer>
|
||||||
|
autocmd TextChanged,TextChangedI,InsertLeave <buffer> call nuwiki#colors#refresh()
|
||||||
|
augroup END
|
||||||
|
call nuwiki#colors#refresh()
|
||||||
|
let b:undo_ftplugin .= ' | execute "autocmd! NuwikiColorConceal * <buffer>"'
|
||||||
|
|
||||||
|
" vimwiki `autowriteall` / `table_auto_fmt` (Vim path; Neovim wires both via
|
||||||
|
" lua/nuwiki/ftplugin.lua). autowriteall mirrors the option into Vim's global
|
||||||
|
" &autowriteall while a wiki buffer is current (auto-save on switch/make),
|
||||||
|
" restoring the prior value on leave. table_auto_fmt re-aligns the table under
|
||||||
|
" the cursor on InsertLeave. Disable with g:nuwiki_autowriteall /
|
||||||
|
" g:nuwiki_table_auto_fmt = 0.
|
||||||
|
if !has('nvim')
|
||||||
|
if get(g:, 'nuwiki_autowriteall', 1)
|
||||||
|
augroup NuwikiAutoWriteAll
|
||||||
|
autocmd! * <buffer>
|
||||||
|
autocmd BufEnter <buffer> let s:nuwiki_awa_saved = &autowriteall | let &autowriteall = 1
|
||||||
|
autocmd BufLeave <buffer> let &autowriteall = get(s:, 'nuwiki_awa_saved', &autowriteall)
|
||||||
|
augroup END
|
||||||
|
" FileType fires after the initial BufEnter, so apply once now too.
|
||||||
|
let s:nuwiki_awa_saved = &autowriteall
|
||||||
|
let &autowriteall = 1
|
||||||
|
let b:undo_ftplugin .= ' | execute "autocmd! NuwikiAutoWriteAll * <buffer>"'
|
||||||
|
endif
|
||||||
|
if get(g:, 'nuwiki_table_auto_fmt', 1)
|
||||||
|
augroup NuwikiTableAutoFmt
|
||||||
|
autocmd! * <buffer>
|
||||||
|
autocmd InsertLeave <buffer> if getline('.') =~# '|' | call nuwiki#commands#table_align() | endif
|
||||||
|
augroup END
|
||||||
|
let b:undo_ftplugin .= ' | execute "autocmd! NuwikiTableAutoFmt * <buffer>"'
|
||||||
|
endif
|
||||||
|
" vimwiki `auto_chdir` (default off): lcd into the owning wiki's root.
|
||||||
|
if get(g:, 'nuwiki_auto_chdir', 0)
|
||||||
|
augroup NuwikiAutoChdir
|
||||||
|
autocmd! * <buffer>
|
||||||
|
autocmd BufEnter,BufWinEnter <buffer> call nuwiki#commands#auto_chdir()
|
||||||
|
augroup END
|
||||||
|
call nuwiki#commands#auto_chdir()
|
||||||
|
let b:undo_ftplugin .= ' | execute "autocmd! NuwikiAutoChdir * <buffer>"'
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
if !has('nvim')
|
if !has('nvim')
|
||||||
" ===== Plain Vim path =====
|
" ===== Plain Vim path =====
|
||||||
"
|
"
|
||||||
@@ -35,20 +82,20 @@ if !has('nvim')
|
|||||||
" coc.nvim users can run `:CocCommand nuwiki.x` directly and skip
|
" coc.nvim users can run `:CocCommand nuwiki.x` directly and skip
|
||||||
" these aliases.
|
" these aliases.
|
||||||
|
|
||||||
command! -buffer -count VimwikiIndex call nuwiki#commands#wiki_index(<count>)
|
command! -buffer -count=0 VimwikiIndex call nuwiki#commands#wiki_index(<count>)
|
||||||
command! -buffer -count VimwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
|
command! -buffer -count=0 VimwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
|
||||||
command! -buffer VimwikiUISelect call nuwiki#commands#wiki_ui_select()
|
command! -buffer VimwikiUISelect call nuwiki#commands#wiki_ui_select()
|
||||||
command! -buffer VimwikiDiaryIndex call nuwiki#commands#diary_index()
|
command! -buffer -count=0 VimwikiDiaryIndex call nuwiki#commands#diary_index(<count>)
|
||||||
command! -buffer VimwikiMakeDiaryNote call nuwiki#commands#diary_today()
|
command! -buffer -count=0 VimwikiMakeDiaryNote call nuwiki#commands#diary_today(<count>)
|
||||||
command! -buffer VimwikiTabMakeDiaryNote call nuwiki#commands#diary_today_tab()
|
command! -buffer -count=0 VimwikiTabMakeDiaryNote call nuwiki#commands#diary_today_tab(<count>)
|
||||||
command! -buffer VimwikiMakeYesterdayDiaryNote call nuwiki#commands#diary_yesterday()
|
command! -buffer -count=0 VimwikiMakeYesterdayDiaryNote call nuwiki#commands#diary_yesterday(<count>)
|
||||||
command! -buffer VimwikiMakeTomorrowDiaryNote call nuwiki#commands#diary_tomorrow()
|
command! -buffer -count=0 VimwikiMakeTomorrowDiaryNote call nuwiki#commands#diary_tomorrow(<count>)
|
||||||
command! -buffer VimwikiDiaryNextDay call nuwiki#commands#diary_next()
|
command! -buffer VimwikiDiaryNextDay call nuwiki#commands#diary_next()
|
||||||
command! -buffer VimwikiDiaryPrevDay call nuwiki#commands#diary_prev()
|
command! -buffer VimwikiDiaryPrevDay call nuwiki#commands#diary_prev()
|
||||||
command! -buffer VimwikiDiaryGenerateLinks call nuwiki#commands#diary_generate_index()
|
command! -buffer VimwikiDiaryGenerateLinks call nuwiki#commands#diary_generate_index()
|
||||||
command! -buffer VimwikiFollowLink call nuwiki#commands#follow_link_or_create()
|
command! -buffer VimwikiFollowLink call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer VimwikiSplitLink split | call nuwiki#commands#follow_link_or_create()
|
command! -buffer -nargs=* VimwikiSplitLink split | call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer VimwikiVSplitLink vsplit | call nuwiki#commands#follow_link_or_create()
|
command! -buffer -nargs=* VimwikiVSplitLink vsplit | call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer VimwikiTabnewLink tabnew | call nuwiki#commands#follow_link_or_create()
|
command! -buffer VimwikiTabnewLink tabnew | call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer VimwikiTabDropLink call nuwiki#commands#follow_link_drop()
|
command! -buffer VimwikiTabDropLink call nuwiki#commands#follow_link_drop()
|
||||||
command! -buffer VimwikiGoBackLink execute "normal! \<C-o>"
|
command! -buffer VimwikiGoBackLink execute "normal! \<C-o>"
|
||||||
@@ -57,32 +104,35 @@ if !has('nvim')
|
|||||||
command! -buffer VimwikiNextLink call nuwiki#commands#link_next()
|
command! -buffer VimwikiNextLink call nuwiki#commands#link_next()
|
||||||
command! -buffer VimwikiPrevLink call nuwiki#commands#link_prev()
|
command! -buffer VimwikiPrevLink call nuwiki#commands#link_prev()
|
||||||
command! -buffer VimwikiBaddLink call nuwiki#commands#badd_link()
|
command! -buffer VimwikiBaddLink call nuwiki#commands#badd_link()
|
||||||
command! -buffer -nargs=1 VimwikiSearch lvimgrep /<args>/ **
|
command! -buffer -nargs=* VimwikiSearch call nuwiki#commands#search(<q-args>)
|
||||||
command! -buffer -nargs=1 VWS lvimgrep /<args>/ **
|
command! -buffer -nargs=* NuwikiSearch call nuwiki#commands#search(<q-args>)
|
||||||
command! -buffer -nargs=1 VimwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
|
command! -buffer -nargs=* VWS call nuwiki#commands#search(<q-args>)
|
||||||
|
command! -buffer -nargs=* -complete=customlist,nuwiki#complete#pages VimwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
|
||||||
|
|
||||||
command! -buffer VimwikiDeleteFile call nuwiki#commands#delete_file()
|
command! -buffer VimwikiDeleteFile call nuwiki#commands#delete_file()
|
||||||
command! -buffer VimwikiDeleteLink call nuwiki#commands#delete_file()
|
command! -buffer VimwikiDeleteLink call nuwiki#commands#delete_file()
|
||||||
command! -buffer VimwikiRenameFile call nuwiki#commands#rename_file()
|
command! -buffer -nargs=? VimwikiRenameFile call nuwiki#commands#rename_file()
|
||||||
command! -buffer VimwikiRenameLink call nuwiki#commands#rename_file()
|
command! -buffer VimwikiRenameLink call nuwiki#commands#rename_file()
|
||||||
command! -buffer VimwikiNextTask call nuwiki#commands#next_task()
|
command! -buffer VimwikiNextTask call nuwiki#commands#next_task()
|
||||||
command! -buffer VimwikiToggleListItem call nuwiki#commands#toggle_list_item()
|
command! -buffer -range VimwikiToggleListItem call nuwiki#commands#toggle_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer VimwikiToggleRejectedListItem call nuwiki#commands#reject_list_item()
|
command! -buffer -range VimwikiToggleRejectedListItem call nuwiki#commands#reject_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer VimwikiListToggle call nuwiki#commands#list_toggle_or_add_checkbox()
|
command! -buffer VimwikiListToggle call nuwiki#commands#list_toggle_or_add_checkbox()
|
||||||
command! -buffer VimwikiIncrementListItem call nuwiki#commands#list_cycle_symbol(1)
|
command! -buffer -range VimwikiIncrementListItem call nuwiki#commands#list_cycle_symbol_range(1, <line1>, <line2>)
|
||||||
command! -buffer VimwikiDecrementListItem call nuwiki#commands#list_cycle_symbol(-1)
|
command! -buffer -range VimwikiDecrementListItem call nuwiki#commands#list_cycle_symbol_range(-1, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiChangeSymbolTo call nuwiki#commands#list_change_symbol(<q-args>, 0)
|
command! -buffer -range -nargs=1 VimwikiChangeSymbolTo call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiListChangeSymbolI call nuwiki#commands#list_change_symbol(<q-args>, 0)
|
command! -buffer -range -nargs=1 VimwikiListChangeSymbolI call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo call nuwiki#commands#list_change_symbol(<q-args>, 1)
|
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo call nuwiki#commands#list_change_symbol(<q-args>, 1)
|
||||||
command! -buffer -bang VimwikiRemoveDone if <bang>0 | call nuwiki#commands#list_remove_done_all() | else | call nuwiki#commands#list_remove_done() | endif
|
command! -buffer -bang -range VimwikiRemoveDone call nuwiki#commands#remove_done(<bang>0, <range>, <line1>, <line2>)
|
||||||
command! -buffer -range VimwikiRemoveSingleCB call nuwiki#commands#list_remove_checkbox()
|
command! -buffer -range VimwikiRemoveSingleCB call nuwiki#commands#list_remove_checkbox()
|
||||||
command! -buffer VimwikiRemoveCBInList call nuwiki#commands#list_remove_checkbox_in_list()
|
command! -buffer VimwikiRemoveCBInList call nuwiki#commands#list_remove_checkbox_in_list()
|
||||||
command! -buffer -nargs=? VimwikiListChangeLvl call nuwiki#commands#list_change_lvl()
|
command! -buffer -range -nargs=+ VimwikiListChangeLvl call nuwiki#commands#list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
command! -buffer VimwikiNormalizeLink call nuwiki#commands#normalize_link()
|
command! -buffer -nargs=? VimwikiNormalizeLink call nuwiki#commands#normalize_link(<q-args>)
|
||||||
command! -buffer VimwikiRenumberList call nuwiki#commands#list_renumber()
|
command! -buffer VimwikiRenumberList call nuwiki#commands#list_renumber()
|
||||||
command! -buffer VimwikiRenumberAllLists call nuwiki#commands#list_renumber_all()
|
command! -buffer VimwikiRenumberAllLists call nuwiki#commands#list_renumber_all()
|
||||||
command! -buffer VimwikiTableAlignQ call nuwiki#commands#table_align()
|
command! -buffer -nargs=? VimwikiTableAlignQ call nuwiki#commands#table_align_or_cmd('gqq')
|
||||||
command! -buffer VimwikiTableAlignW call nuwiki#commands#table_align()
|
command! -buffer -nargs=? VimwikiTableAlignW call nuwiki#commands#table_align_or_cmd('gww')
|
||||||
|
command! -buffer -nargs=* VimwikiReturn call nuwiki#commands#return_cmd(<f-args>)
|
||||||
|
command! -buffer -nargs=* NuwikiReturn call nuwiki#commands#return_cmd(<f-args>)
|
||||||
|
|
||||||
command! -buffer Vimwiki2HTML call nuwiki#commands#export_current()
|
command! -buffer Vimwiki2HTML call nuwiki#commands#export_current()
|
||||||
command! -buffer Vimwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
command! -buffer Vimwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
||||||
@@ -90,67 +140,69 @@ if !has('nvim')
|
|||||||
command! -buffer VimwikiRss call nuwiki#commands#export_rss()
|
command! -buffer VimwikiRss call nuwiki#commands#export_rss()
|
||||||
|
|
||||||
command! -buffer VimwikiTOC call nuwiki#commands#toc_generate()
|
command! -buffer VimwikiTOC call nuwiki#commands#toc_generate()
|
||||||
command! -buffer VimwikiGenerateLinks call nuwiki#commands#links_generate()
|
command! -buffer -nargs=? VimwikiGenerateLinks call nuwiki#commands#links_generate(<q-args>)
|
||||||
command! -buffer VimwikiCheckLinks call nuwiki#commands#check_links()
|
command! -buffer -range VimwikiCheckLinks call nuwiki#commands#check_links(<range>, <line1>, <line2>)
|
||||||
command! -buffer VimwikiFindOrphans call nuwiki#commands#find_orphans()
|
command! -buffer VimwikiFindOrphans call nuwiki#commands#find_orphans()
|
||||||
|
|
||||||
command! -buffer VimwikiRebuildTags call nuwiki#commands#tags_rebuild()
|
command! -buffer -bang VimwikiRebuildTags call nuwiki#commands#tags_rebuild(<bang>0)
|
||||||
command! -buffer -nargs=? VimwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags VimwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
|
||||||
command! -buffer -nargs=? VimwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags VimwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
|
||||||
command! -buffer -nargs=? VimwikiGenerateTags call nuwiki#commands#tags_generate_links(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags VimwikiGenerateTags call nuwiki#commands#tags_generate_links(<q-args>)
|
||||||
|
|
||||||
" Tables, colorize, and clipboard paste.
|
" Tables, colorize, and clipboard paste.
|
||||||
command! -buffer -nargs=1 VimwikiTable call nuwiki#commands#table_insert()
|
command! -buffer -nargs=* VimwikiTable call nuwiki#commands#table_insert(<f-args>)
|
||||||
command! -buffer VimwikiTableMoveColumnLeft call nuwiki#commands#table_move_left()
|
command! -buffer VimwikiTableMoveColumnLeft call nuwiki#commands#table_move_column_left()
|
||||||
command! -buffer VimwikiTableMoveColumnRight call nuwiki#commands#table_move_right()
|
command! -buffer VimwikiTableMoveColumnRight call nuwiki#commands#table_move_column_right()
|
||||||
command! -buffer -nargs=1 VimwikiColorize call nuwiki#commands#colorize(<q-args>)
|
command! -buffer -nargs=* -range -complete=customlist,nuwiki#complete#colors VimwikiColorize call nuwiki#commands#colorize(<q-args>, <range>)
|
||||||
command! -buffer VimwikiPasteLink call nuwiki#commands#paste_link()
|
command! -buffer VimwikiPasteLink call nuwiki#commands#paste_link()
|
||||||
command! -buffer VimwikiPasteUrl call nuwiki#commands#paste_url()
|
command! -buffer VimwikiPasteUrl call nuwiki#commands#paste_url()
|
||||||
command! -buffer VimwikiCatUrl call nuwiki#commands#cat_url()
|
command! -buffer VimwikiCatUrl call nuwiki#commands#cat_url()
|
||||||
|
|
||||||
" :Nuwiki* canonical aliases — Vim side.
|
" :Nuwiki* canonical aliases — Vim side.
|
||||||
command! -buffer -count NuwikiIndex call nuwiki#commands#wiki_index(<count>)
|
command! -buffer -count=0 NuwikiIndex call nuwiki#commands#wiki_index(<count>)
|
||||||
command! -buffer -count NuwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
|
command! -buffer -count=0 NuwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
|
||||||
command! -buffer NuwikiDiaryIndex call nuwiki#commands#diary_index()
|
command! -buffer NuwikiUISelect call nuwiki#commands#wiki_ui_select()
|
||||||
command! -buffer NuwikiDiaryToday call nuwiki#commands#diary_today()
|
command! -buffer -count=0 NuwikiDiaryIndex call nuwiki#commands#diary_index(<count>)
|
||||||
command! -buffer NuwikiDiaryYesterday call nuwiki#commands#diary_yesterday()
|
command! -buffer -count=0 NuwikiDiaryToday call nuwiki#commands#diary_today(<count>)
|
||||||
command! -buffer NuwikiDiaryTomorrow call nuwiki#commands#diary_tomorrow()
|
command! -buffer -count=0 NuwikiDiaryYesterday call nuwiki#commands#diary_yesterday(<count>)
|
||||||
|
command! -buffer -count=0 NuwikiDiaryTomorrow call nuwiki#commands#diary_tomorrow(<count>)
|
||||||
command! -buffer NuwikiDiaryNext call nuwiki#commands#diary_next()
|
command! -buffer NuwikiDiaryNext call nuwiki#commands#diary_next()
|
||||||
command! -buffer NuwikiDiaryPrev call nuwiki#commands#diary_prev()
|
command! -buffer NuwikiDiaryPrev call nuwiki#commands#diary_prev()
|
||||||
command! -buffer NuwikiFollowLink call nuwiki#commands#follow_link_or_create()
|
command! -buffer NuwikiFollowLink call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer NuwikiSplitLink split | call nuwiki#commands#follow_link_or_create()
|
command! -buffer -nargs=* NuwikiSplitLink split | call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer NuwikiVSplitLink vsplit | call nuwiki#commands#follow_link_or_create()
|
command! -buffer -nargs=* NuwikiVSplitLink vsplit | call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer NuwikiTabnewLink tabnew | call nuwiki#commands#follow_link_or_create()
|
command! -buffer NuwikiTabnewLink tabnew | call nuwiki#commands#follow_link_or_create()
|
||||||
command! -buffer NuwikiTabDropLink call nuwiki#commands#follow_link_drop()
|
command! -buffer NuwikiTabDropLink call nuwiki#commands#follow_link_drop()
|
||||||
command! -buffer NuwikiGoBackLink execute "normal! \<C-o>"
|
command! -buffer NuwikiGoBackLink execute "normal! \<C-o>"
|
||||||
command! -buffer NuwikiBacklinks call nuwiki#commands#backlinks()
|
command! -buffer NuwikiBacklinks call nuwiki#commands#backlinks()
|
||||||
command! -buffer NuwikiDeleteFile call nuwiki#commands#delete_file()
|
command! -buffer NuwikiDeleteFile call nuwiki#commands#delete_file()
|
||||||
command! -buffer NuwikiRenameFile call nuwiki#commands#rename_file()
|
command! -buffer -nargs=? NuwikiRenameFile call nuwiki#commands#rename_file()
|
||||||
command! -buffer NuwikiNextTask call nuwiki#commands#next_task()
|
command! -buffer NuwikiNextTask call nuwiki#commands#next_task()
|
||||||
command! -buffer NuwikiToggleListItem call nuwiki#commands#toggle_list_item()
|
command! -buffer -range NuwikiToggleListItem call nuwiki#commands#toggle_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer NuwikiToggleRejected call nuwiki#commands#reject_list_item()
|
command! -buffer -range NuwikiToggleRejected call nuwiki#commands#reject_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer NuwikiExport call nuwiki#commands#export_current()
|
command! -buffer NuwikiExport call nuwiki#commands#export_current()
|
||||||
command! -buffer NuwikiExportBrowse call nuwiki#commands#export_browse()
|
command! -buffer NuwikiExportBrowse call nuwiki#commands#export_browse()
|
||||||
command! -buffer -bang NuwikiExportAll if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
command! -buffer -bang NuwikiExportAll if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
||||||
command! -buffer NuwikiRss call nuwiki#commands#export_rss()
|
command! -buffer NuwikiRss call nuwiki#commands#export_rss()
|
||||||
command! -buffer NuwikiTOC call nuwiki#commands#toc_generate()
|
command! -buffer NuwikiTOC call nuwiki#commands#toc_generate()
|
||||||
command! -buffer NuwikiGenerateLinks call nuwiki#commands#links_generate()
|
command! -buffer -nargs=? NuwikiGenerateLinks call nuwiki#commands#links_generate(<q-args>)
|
||||||
command! -buffer NuwikiCheckLinks call nuwiki#commands#check_links()
|
command! -buffer -range NuwikiCheckLinks call nuwiki#commands#check_links(<range>, <line1>, <line2>)
|
||||||
command! -buffer NuwikiFindOrphans call nuwiki#commands#find_orphans()
|
command! -buffer NuwikiFindOrphans call nuwiki#commands#find_orphans()
|
||||||
command! -buffer NuwikiRebuildTags call nuwiki#commands#tags_rebuild()
|
command! -buffer -bang NuwikiRebuildTags call nuwiki#commands#tags_rebuild(<bang>0)
|
||||||
command! -buffer -nargs=? NuwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags NuwikiSearchTags call nuwiki#commands#tags_search(<q-args>)
|
||||||
command! -buffer -nargs=? NuwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags NuwikiGenerateTagLinks call nuwiki#commands#tags_generate_links(<q-args>)
|
||||||
command! -buffer -nargs=1 NuwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags NuwikiGenerateTags call nuwiki#commands#tags_generate_links(<q-args>)
|
||||||
|
command! -buffer -nargs=* -complete=customlist,nuwiki#complete#pages NuwikiGoto call nuwiki#commands#wiki_goto_page(<q-args>)
|
||||||
|
|
||||||
" Canonical :Nuwiki* names that mirror the documented surface.
|
" Canonical :Nuwiki* names that mirror the documented surface.
|
||||||
" The older :Nuwiki* spellings above stay defined for back-compat.
|
" The older :Nuwiki* spellings above stay defined for back-compat.
|
||||||
command! -buffer NuwikiNextLink call nuwiki#commands#link_next()
|
command! -buffer NuwikiNextLink call nuwiki#commands#link_next()
|
||||||
command! -buffer NuwikiPrevLink call nuwiki#commands#link_prev()
|
command! -buffer NuwikiPrevLink call nuwiki#commands#link_prev()
|
||||||
command! -buffer NuwikiBaddLink call nuwiki#commands#badd_link()
|
command! -buffer NuwikiBaddLink call nuwiki#commands#badd_link()
|
||||||
command! -buffer NuwikiMakeDiaryNote call nuwiki#commands#diary_today()
|
command! -buffer -count=0 NuwikiMakeDiaryNote call nuwiki#commands#diary_today(<count>)
|
||||||
command! -buffer NuwikiTabMakeDiaryNote call nuwiki#commands#diary_today_tab()
|
command! -buffer -count=0 NuwikiTabMakeDiaryNote call nuwiki#commands#diary_today_tab(<count>)
|
||||||
command! -buffer NuwikiMakeYesterdayDiaryNote call nuwiki#commands#diary_yesterday()
|
command! -buffer -count=0 NuwikiMakeYesterdayDiaryNote call nuwiki#commands#diary_yesterday(<count>)
|
||||||
command! -buffer NuwikiMakeTomorrowDiaryNote call nuwiki#commands#diary_tomorrow()
|
command! -buffer -count=0 NuwikiMakeTomorrowDiaryNote call nuwiki#commands#diary_tomorrow(<count>)
|
||||||
command! -buffer NuwikiDiaryGenerateLinks call nuwiki#commands#diary_generate_index()
|
command! -buffer NuwikiDiaryGenerateLinks call nuwiki#commands#diary_generate_index()
|
||||||
command! -buffer NuwikiDiaryNextDay call nuwiki#commands#diary_next()
|
command! -buffer NuwikiDiaryNextDay call nuwiki#commands#diary_next()
|
||||||
command! -buffer NuwikiDiaryPrevDay call nuwiki#commands#diary_prev()
|
command! -buffer NuwikiDiaryPrevDay call nuwiki#commands#diary_prev()
|
||||||
@@ -158,23 +210,23 @@ if !has('nvim')
|
|||||||
command! -buffer Nuwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
command! -buffer Nuwiki2HTMLBrowse call nuwiki#commands#export_browse()
|
||||||
command! -buffer -bang NuwikiAll2HTML if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
command! -buffer -bang NuwikiAll2HTML if <bang>0 | call nuwiki#commands#export_all_force() | else | call nuwiki#commands#export_all() | endif
|
||||||
|
|
||||||
command! -buffer -bang NuwikiRemoveDone if <bang>0 | call nuwiki#commands#list_remove_done_all() | else | call nuwiki#commands#list_remove_done() | endif
|
command! -buffer -bang -range NuwikiRemoveDone call nuwiki#commands#remove_done(<bang>0, <range>, <line1>, <line2>)
|
||||||
command! -buffer -range NuwikiRemoveCheckbox call nuwiki#commands#list_remove_checkbox()
|
command! -buffer -range NuwikiRemoveCheckbox call nuwiki#commands#list_remove_checkbox()
|
||||||
command! -buffer NuwikiRemoveCheckboxInList call nuwiki#commands#list_remove_checkbox_in_list()
|
command! -buffer NuwikiRemoveCheckboxInList call nuwiki#commands#list_remove_checkbox_in_list()
|
||||||
command! -buffer -nargs=? NuwikiListChangeLvl call nuwiki#commands#list_change_lvl()
|
command! -buffer -range -nargs=+ NuwikiListChangeLvl call nuwiki#commands#list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
command! -buffer NuwikiListToggle call nuwiki#commands#list_toggle_or_add_checkbox()
|
command! -buffer NuwikiListToggle call nuwiki#commands#list_toggle_or_add_checkbox()
|
||||||
command! -buffer NuwikiIncrementListItem call nuwiki#commands#list_cycle_symbol(1)
|
command! -buffer -range NuwikiIncrementListItem call nuwiki#commands#list_cycle_symbol_range(1, <line1>, <line2>)
|
||||||
command! -buffer NuwikiDecrementListItem call nuwiki#commands#list_cycle_symbol(-1)
|
command! -buffer -range NuwikiDecrementListItem call nuwiki#commands#list_cycle_symbol_range(-1, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 NuwikiChangeSymbol call nuwiki#commands#list_change_symbol(<q-args>, 0)
|
command! -buffer -range -nargs=1 NuwikiChangeSymbol call nuwiki#commands#list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 NuwikiChangeSymbolInList call nuwiki#commands#list_change_symbol(<q-args>, 1)
|
command! -buffer -nargs=1 NuwikiChangeSymbolInList call nuwiki#commands#list_change_symbol(<q-args>, 1)
|
||||||
command! -buffer NuwikiNormalizeLink call nuwiki#commands#normalize_link()
|
command! -buffer -nargs=? NuwikiNormalizeLink call nuwiki#commands#normalize_link(<q-args>)
|
||||||
command! -buffer NuwikiRenumberList call nuwiki#commands#list_renumber()
|
command! -buffer NuwikiRenumberList call nuwiki#commands#list_renumber()
|
||||||
command! -buffer NuwikiRenumberAllLists call nuwiki#commands#list_renumber_all()
|
command! -buffer NuwikiRenumberAllLists call nuwiki#commands#list_renumber_all()
|
||||||
command! -buffer NuwikiTableAlign call nuwiki#commands#table_align()
|
command! -buffer -nargs=? NuwikiTableAlign call nuwiki#commands#table_align_or_cmd('gqq')
|
||||||
command! -buffer -nargs=1 NuwikiTable call nuwiki#commands#table_insert()
|
command! -buffer -nargs=* NuwikiTable call nuwiki#commands#table_insert(<f-args>)
|
||||||
command! -buffer NuwikiTableMoveColumnLeft call nuwiki#commands#table_move_left()
|
command! -buffer NuwikiTableMoveColumnLeft call nuwiki#commands#table_move_column_left()
|
||||||
command! -buffer NuwikiTableMoveColumnRight call nuwiki#commands#table_move_right()
|
command! -buffer NuwikiTableMoveColumnRight call nuwiki#commands#table_move_column_right()
|
||||||
command! -buffer -nargs=1 NuwikiColorize call nuwiki#commands#colorize(<q-args>)
|
command! -buffer -nargs=* -range -complete=customlist,nuwiki#complete#colors NuwikiColorize call nuwiki#commands#colorize(<q-args>, <range>)
|
||||||
command! -buffer NuwikiPasteLink call nuwiki#commands#paste_link()
|
command! -buffer NuwikiPasteLink call nuwiki#commands#paste_link()
|
||||||
command! -buffer NuwikiPasteUrl call nuwiki#commands#paste_url()
|
command! -buffer NuwikiPasteUrl call nuwiki#commands#paste_url()
|
||||||
command! -buffer NuwikiCatUrl call nuwiki#commands#cat_url()
|
command! -buffer NuwikiCatUrl call nuwiki#commands#cat_url()
|
||||||
@@ -188,17 +240,21 @@ if !has('nvim')
|
|||||||
" toggles in lua/nuwiki/keymaps.lua.
|
" toggles in lua/nuwiki/keymaps.lua.
|
||||||
|
|
||||||
if !get(g:, 'nuwiki_no_default_mappings', 0)
|
if !get(g:, 'nuwiki_no_default_mappings', 0)
|
||||||
|
" Configurable wiki command prefix (vimwiki's g:vimwiki_map_prefix,
|
||||||
|
" default <Leader>w). Built with :exe so a custom prefix relocates the
|
||||||
|
" whole <prefix>* family below.
|
||||||
|
let s:prefix = get(g:, 'nuwiki_map_prefix', '<Leader>w')
|
||||||
" Wiki prefix
|
" Wiki prefix
|
||||||
if !get(g:, 'nuwiki_no_wiki_prefix_mappings', 0)
|
if !get(g:, 'nuwiki_no_wiki_prefix_mappings', 0)
|
||||||
nnoremap <silent><buffer> <Leader>ww :call nuwiki#commands#wiki_index(0)<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'w :call nuwiki#commands#wiki_index(0)<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>wt :call nuwiki#commands#wiki_tab_index(0)<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 't :call nuwiki#commands#wiki_tab_index(0)<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>ws :call nuwiki#commands#wiki_ui_select()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 's :call nuwiki#commands#wiki_ui_select()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>wi :call nuwiki#commands#diary_index()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'i :call nuwiki#commands#diary_index()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>w<Leader>w :call nuwiki#commands#diary_today()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . '<Leader>w :call nuwiki#commands#diary_today()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>w<Leader>y :call nuwiki#commands#diary_yesterday()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . '<Leader>y :call nuwiki#commands#diary_yesterday()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>w<Leader>t :call nuwiki#commands#diary_tomorrow()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . '<Leader>t :call nuwiki#commands#diary_today_tab()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>w<Leader>m :call nuwiki#commands#diary_tomorrow()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . '<Leader>m :call nuwiki#commands#diary_tomorrow()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>w<Leader>i :call nuwiki#commands#diary_generate_index()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . '<Leader>i :call nuwiki#commands#diary_generate_index()<CR>'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
" Links
|
" Links
|
||||||
@@ -207,16 +263,20 @@ if !has('nvim')
|
|||||||
nnoremap <silent><buffer> <S-CR> :split<CR>:call nuwiki#commands#follow_link_or_create()<CR>
|
nnoremap <silent><buffer> <S-CR> :split<CR>:call nuwiki#commands#follow_link_or_create()<CR>
|
||||||
nnoremap <silent><buffer> <C-CR> :vsplit<CR>:call nuwiki#commands#follow_link_or_create()<CR>
|
nnoremap <silent><buffer> <C-CR> :vsplit<CR>:call nuwiki#commands#follow_link_or_create()<CR>
|
||||||
nnoremap <silent><buffer> <C-S-CR> :call nuwiki#commands#follow_link_drop()<CR>
|
nnoremap <silent><buffer> <C-S-CR> :call nuwiki#commands#follow_link_drop()<CR>
|
||||||
|
nnoremap <silent><buffer> <D-CR> :call nuwiki#commands#follow_link_drop()<CR>
|
||||||
|
nnoremap <silent><buffer> <M-CR> :call nuwiki#commands#badd_link()<CR>
|
||||||
nnoremap <silent><buffer> <BS> <C-o>
|
nnoremap <silent><buffer> <BS> <C-o>
|
||||||
nnoremap <silent><buffer> <Tab> /\[\[<CR>:nohlsearch<CR>
|
nnoremap <silent><buffer> <Tab> /\[\[<CR>:nohlsearch<CR>
|
||||||
nnoremap <silent><buffer> <S-Tab> ?\[\[<CR>:nohlsearch<CR>
|
nnoremap <silent><buffer> <S-Tab> ?\[\[<CR>:nohlsearch<CR>
|
||||||
nnoremap <silent><buffer> + :call nuwiki#commands#normalize_link()<CR>
|
nnoremap <silent><buffer> + :call nuwiki#commands#normalize_link()<CR>
|
||||||
xnoremap <silent><buffer> + :<C-u>call nuwiki#commands#normalize_link()<CR>
|
xnoremap <silent><buffer> + :<C-u>call nuwiki#commands#normalize_link(1)<CR>
|
||||||
nnoremap <silent><buffer> <Leader>wn :call nuwiki#commands#wiki_goto_page('')<CR>
|
" Upstream binds visual <CR> to the same normalize-link as visual `+`.
|
||||||
nnoremap <silent><buffer> <Leader>wd :call nuwiki#commands#delete_file()<CR>
|
xnoremap <silent><buffer> <CR> :<C-u>call nuwiki#commands#normalize_link(1)<CR>
|
||||||
nnoremap <silent><buffer> <Leader>wr :call nuwiki#commands#rename_file()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'n :call nuwiki#commands#wiki_goto_page('''')<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>wc :call nuwiki#commands#colorize('')<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'd :call nuwiki#commands#delete_file()<CR>'
|
||||||
xnoremap <silent><buffer> <Leader>wc :<C-u>call nuwiki#commands#colorize('')<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'r :call nuwiki#commands#rename_file()<CR>'
|
||||||
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'c :call nuwiki#commands#colorize('''')<CR>'
|
||||||
|
exe 'xnoremap <silent><buffer> ' . s:prefix . 'c :<C-u>call nuwiki#commands#colorize('''', 2)<CR>'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
" Diary nav
|
" Diary nav
|
||||||
@@ -244,8 +304,12 @@ if !has('nvim')
|
|||||||
nnoremap <silent><buffer> gll :call nuwiki#commands#list_change_level(1, 0)<CR>
|
nnoremap <silent><buffer> gll :call nuwiki#commands#list_change_level(1, 0)<CR>
|
||||||
nnoremap <silent><buffer> gLh :call nuwiki#commands#list_change_level(-1, 1)<CR>
|
nnoremap <silent><buffer> gLh :call nuwiki#commands#list_change_level(-1, 1)<CR>
|
||||||
nnoremap <silent><buffer> gLl :call nuwiki#commands#list_change_level(1, 1)<CR>
|
nnoremap <silent><buffer> gLl :call nuwiki#commands#list_change_level(1, 1)<CR>
|
||||||
|
" Upstream binds case-variant aliases gLH/gLL/gLR == gLh/gLl/gLr.
|
||||||
|
nnoremap <silent><buffer> gLH :call nuwiki#commands#list_change_level(-1, 1)<CR>
|
||||||
|
nnoremap <silent><buffer> gLL :call nuwiki#commands#list_change_level(1, 1)<CR>
|
||||||
nnoremap <silent><buffer> glr :call nuwiki#commands#list_renumber()<CR>
|
nnoremap <silent><buffer> glr :call nuwiki#commands#list_renumber()<CR>
|
||||||
nnoremap <silent><buffer> gLr :call nuwiki#commands#list_renumber_all()<CR>
|
nnoremap <silent><buffer> gLr :call nuwiki#commands#list_renumber_all()<CR>
|
||||||
|
nnoremap <silent><buffer> gLR :call nuwiki#commands#list_renumber_all()<CR>
|
||||||
" gl<sym>/gL<sym>: set the current item's / whole list's marker (upstream
|
" gl<sym>/gL<sym>: set the current item's / whole list's marker (upstream
|
||||||
" VimwikiChangeSymbolTo / ChangeSymbolInListTo). 1) is command-only.
|
" VimwikiChangeSymbolTo / ChangeSymbolInListTo). 1) is command-only.
|
||||||
nnoremap <silent><buffer> gl- :call nuwiki#commands#list_change_symbol('-', 0)<CR>
|
nnoremap <silent><buffer> gl- :call nuwiki#commands#list_change_symbol('-', 0)<CR>
|
||||||
@@ -280,6 +344,7 @@ if !has('nvim')
|
|||||||
inoremap <silent><buffer> <C-L><C-K> <C-o>:call nuwiki#commands#list_cycle_symbol(-1)<CR>
|
inoremap <silent><buffer> <C-L><C-K> <C-o>:call nuwiki#commands#list_cycle_symbol(-1)<CR>
|
||||||
inoremap <silent><buffer> <C-L><C-M> <C-o>:call nuwiki#commands#list_toggle_or_add_checkbox()<CR>
|
inoremap <silent><buffer> <C-L><C-M> <C-o>:call nuwiki#commands#list_toggle_or_add_checkbox()<CR>
|
||||||
inoremap <silent><buffer><expr> <CR> nuwiki#commands#smart_return()
|
inoremap <silent><buffer><expr> <CR> nuwiki#commands#smart_return()
|
||||||
|
inoremap <silent><buffer><expr> <S-CR> nuwiki#commands#smart_shift_return()
|
||||||
endif
|
endif
|
||||||
|
|
||||||
" Headers
|
" Headers
|
||||||
@@ -296,9 +361,9 @@ if !has('nvim')
|
|||||||
|
|
||||||
" HTML export
|
" HTML export
|
||||||
if !get(g:, 'nuwiki_no_html_export_mappings', 0)
|
if !get(g:, 'nuwiki_no_html_export_mappings', 0)
|
||||||
nnoremap <silent><buffer> <Leader>wh :call nuwiki#commands#export_current()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'h :call nuwiki#commands#export_current()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>whh :call nuwiki#commands#export_browse()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'hh :call nuwiki#commands#export_browse()<CR>'
|
||||||
nnoremap <silent><buffer> <Leader>wha :call nuwiki#commands#export_all()<CR>
|
exe 'nnoremap <silent><buffer> ' . s:prefix . 'ha :call nuwiki#commands#export_all()<CR>'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
" Mouse (opt-in via g:nuwiki_mouse_mappings = 1).
|
" Mouse (opt-in via g:nuwiki_mouse_mappings = 1).
|
||||||
@@ -312,10 +377,10 @@ if !has('nvim')
|
|||||||
|
|
||||||
" Tables (and insert-mode table-cell navigation, same surface).
|
" Tables (and insert-mode table-cell navigation, same surface).
|
||||||
if !get(g:, 'nuwiki_no_table_editing_mappings', 0)
|
if !get(g:, 'nuwiki_no_table_editing_mappings', 0)
|
||||||
nnoremap <silent><buffer> gqq :call nuwiki#commands#table_align()<CR>
|
nnoremap <silent><buffer> gqq :call nuwiki#commands#table_align_or_cmd('gqq')<CR>
|
||||||
nnoremap <silent><buffer> gq1 :call nuwiki#commands#table_align()<CR>
|
nnoremap <silent><buffer> gq1 :call nuwiki#commands#table_align_or_cmd('gqq')<CR>
|
||||||
nnoremap <silent><buffer> gww :call nuwiki#commands#table_align()<CR>
|
nnoremap <silent><buffer> gww :call nuwiki#commands#table_align_or_cmd('gww')<CR>
|
||||||
nnoremap <silent><buffer> gw1 :call nuwiki#commands#table_align()<CR>
|
nnoremap <silent><buffer> gw1 :call nuwiki#commands#table_align_or_cmd('gww')<CR>
|
||||||
nnoremap <silent><buffer> <A-Left> :call nuwiki#commands#table_move_left()<CR>
|
nnoremap <silent><buffer> <A-Left> :call nuwiki#commands#table_move_left()<CR>
|
||||||
nnoremap <silent><buffer> <A-Right> :call nuwiki#commands#table_move_right()<CR>
|
nnoremap <silent><buffer> <A-Right> :call nuwiki#commands#table_move_right()<CR>
|
||||||
inoremap <silent><buffer><expr> <Tab> nuwiki#commands#smart_tab()
|
inoremap <silent><buffer><expr> <Tab> nuwiki#commands#smart_tab()
|
||||||
@@ -361,6 +426,8 @@ if !has('nvim')
|
|||||||
xnoremap <silent><buffer> ic :<C-u>call nuwiki#textobjects#table_column(1)<CR>
|
xnoremap <silent><buffer> ic :<C-u>call nuwiki#textobjects#table_column(1)<CR>
|
||||||
onoremap <silent><buffer> ic :<C-u>call nuwiki#textobjects#table_column(1)<CR>
|
onoremap <silent><buffer> ic :<C-u>call nuwiki#textobjects#table_column(1)<CR>
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
unlet s:prefix
|
||||||
endif
|
endif
|
||||||
|
|
||||||
finish
|
finish
|
||||||
@@ -372,56 +439,59 @@ endif
|
|||||||
" `nuwiki.commands` that either issues a `workspace/executeCommand`
|
" `nuwiki.commands` that either issues a `workspace/executeCommand`
|
||||||
" or, for client-side operations, calls `vim.lsp.buf.*` directly.
|
" or, for client-side operations, calls `vim.lsp.buf.*` directly.
|
||||||
|
|
||||||
command! -buffer -count VimwikiIndex lua require('nuwiki.commands').wiki_index(vim.v.count)
|
command! -buffer -count=0 VimwikiIndex lua require('nuwiki.commands').wiki_index(<count>)
|
||||||
command! -buffer -count VimwikiTabIndex lua require('nuwiki.commands').wiki_tab_index(vim.v.count)
|
command! -buffer -count=0 VimwikiTabIndex lua require('nuwiki.commands').wiki_tab_index(<count>)
|
||||||
command! -buffer VimwikiUISelect lua require('nuwiki.commands').wiki_ui_select()
|
command! -buffer VimwikiUISelect lua require('nuwiki.commands').wiki_ui_select()
|
||||||
command! -buffer VimwikiDiaryIndex lua require('nuwiki.commands').diary_index()
|
command! -buffer -count=0 VimwikiDiaryIndex lua require('nuwiki.commands').diary_index(<count>)
|
||||||
command! -buffer VimwikiMakeDiaryNote lua require('nuwiki.commands').diary_today()
|
command! -buffer -count=0 VimwikiMakeDiaryNote lua require('nuwiki.commands').diary_today(<count>)
|
||||||
command! -buffer VimwikiTabMakeDiaryNote lua require('nuwiki.commands').diary_today_tab()
|
command! -buffer -count=0 VimwikiTabMakeDiaryNote lua require('nuwiki.commands').diary_today_tab(<count>)
|
||||||
command! -buffer VimwikiMakeYesterdayDiaryNote lua require('nuwiki.commands').diary_yesterday()
|
command! -buffer -count=0 VimwikiMakeYesterdayDiaryNote lua require('nuwiki.commands').diary_yesterday(<count>)
|
||||||
command! -buffer VimwikiMakeTomorrowDiaryNote lua require('nuwiki.commands').diary_tomorrow()
|
command! -buffer -count=0 VimwikiMakeTomorrowDiaryNote lua require('nuwiki.commands').diary_tomorrow(<count>)
|
||||||
command! -buffer VimwikiDiaryNextDay lua require('nuwiki.commands').diary_next()
|
command! -buffer VimwikiDiaryNextDay lua require('nuwiki.commands').diary_next()
|
||||||
command! -buffer VimwikiDiaryPrevDay lua require('nuwiki.commands').diary_prev()
|
command! -buffer VimwikiDiaryPrevDay lua require('nuwiki.commands').diary_prev()
|
||||||
command! -buffer VimwikiDiaryGenerateLinks lua require('nuwiki.commands').diary_generate_index()
|
command! -buffer VimwikiDiaryGenerateLinks lua require('nuwiki.commands').diary_generate_index()
|
||||||
|
|
||||||
command! -buffer VimwikiFollowLink lua vim.lsp.buf.definition()
|
command! -buffer VimwikiFollowLink lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer VimwikiGoBackLink execute "normal! \<C-o>"
|
command! -buffer VimwikiGoBackLink execute "normal! \<C-o>"
|
||||||
command! -buffer VimwikiSplitLink split | lua vim.lsp.buf.definition()
|
command! -buffer -nargs=* VimwikiSplitLink split | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer VimwikiVSplitLink vsplit | lua vim.lsp.buf.definition()
|
command! -buffer -nargs=* VimwikiVSplitLink vsplit | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer VimwikiTabnewLink tabnew | lua vim.lsp.buf.definition()
|
command! -buffer VimwikiTabnewLink tabnew | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer VimwikiTabDropLink lua require('nuwiki.commands').follow_link_drop()
|
command! -buffer VimwikiTabDropLink lua require('nuwiki.commands').follow_link_drop()
|
||||||
command! -buffer VimwikiNextLink lua require('nuwiki.commands').link_next()
|
command! -buffer VimwikiNextLink lua require('nuwiki.commands').link_next()
|
||||||
command! -buffer VimwikiPrevLink lua require('nuwiki.commands').link_prev()
|
command! -buffer VimwikiPrevLink lua require('nuwiki.commands').link_prev()
|
||||||
command! -buffer VimwikiBaddLink lua require('nuwiki.commands').badd_link()
|
command! -buffer VimwikiBaddLink lua require('nuwiki.commands').badd_link()
|
||||||
command! -buffer -nargs=1 VimwikiGoto lua require('nuwiki.commands').wiki_goto_page(<q-args>)
|
command! -buffer -nargs=* -complete=customlist,nuwiki#complete#pages VimwikiGoto lua require('nuwiki.commands').wiki_goto_page(<q-args>)
|
||||||
command! -buffer VimwikiBacklinks lua vim.lsp.buf.references()
|
command! -buffer VimwikiBacklinks lua vim.lsp.buf.references()
|
||||||
command! -buffer VWB lua vim.lsp.buf.references()
|
command! -buffer VWB lua vim.lsp.buf.references()
|
||||||
command! -buffer -nargs=1 VimwikiSearch lvimgrep /<args>/ **
|
command! -buffer -nargs=* VimwikiSearch lua require('nuwiki.commands').search(<q-args>)
|
||||||
command! -buffer -nargs=1 VWS lvimgrep /<args>/ **
|
command! -buffer -nargs=* NuwikiSearch lua require('nuwiki.commands').search(<q-args>)
|
||||||
|
command! -buffer -nargs=* VWS lua require('nuwiki.commands').search(<q-args>)
|
||||||
|
|
||||||
command! -buffer VimwikiDeleteFile lua require('nuwiki.commands').delete_file()
|
command! -buffer VimwikiDeleteFile lua require('nuwiki.commands').delete_file()
|
||||||
command! -buffer VimwikiDeleteLink lua require('nuwiki.commands').delete_file()
|
command! -buffer VimwikiDeleteLink lua require('nuwiki.commands').delete_file()
|
||||||
command! -buffer VimwikiRenameFile lua require('nuwiki.commands').rename_file()
|
command! -buffer -nargs=? VimwikiRenameFile lua require('nuwiki.commands').rename_file()
|
||||||
command! -buffer VimwikiRenameLink lua require('nuwiki.commands').rename_file()
|
command! -buffer VimwikiRenameLink lua require('nuwiki.commands').rename_file()
|
||||||
|
|
||||||
command! -buffer VimwikiNextTask lua require('nuwiki.commands').next_task()
|
command! -buffer VimwikiNextTask lua require('nuwiki.commands').next_task()
|
||||||
command! -buffer VimwikiToggleListItem lua require('nuwiki.commands').toggle_list_item()
|
command! -buffer -range VimwikiToggleListItem lua require('nuwiki.commands').toggle_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer VimwikiToggleRejectedListItem lua require('nuwiki.commands').reject_list_item()
|
command! -buffer -range VimwikiToggleRejectedListItem lua require('nuwiki.commands').reject_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer VimwikiListToggle lua require('nuwiki.commands').list_toggle_or_add_checkbox()
|
command! -buffer VimwikiListToggle lua require('nuwiki.commands').list_toggle_or_add_checkbox()
|
||||||
command! -buffer VimwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol(1)
|
command! -buffer -range VimwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(1, <line1>, <line2>)
|
||||||
command! -buffer VimwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol(-1)
|
command! -buffer -range VimwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(-1, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiChangeSymbolTo lua require('nuwiki.commands').list_change_symbol(<q-args>, false)
|
command! -buffer -range -nargs=1 VimwikiChangeSymbolTo lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiListChangeSymbolI lua require('nuwiki.commands').list_change_symbol(<q-args>, false)
|
command! -buffer -range -nargs=1 VimwikiListChangeSymbolI lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo lua require('nuwiki.commands').list_change_symbol(<q-args>, true)
|
command! -buffer -nargs=1 VimwikiChangeSymbolInListTo lua require('nuwiki.commands').list_change_symbol(<q-args>, true)
|
||||||
command! -buffer -bang VimwikiRemoveDone lua require('nuwiki.commands')[('<bang>' == '!') and 'list_remove_done_all' or 'list_remove_done']()
|
command! -buffer -bang -range VimwikiRemoveDone lua require('nuwiki.commands').remove_done('<bang>' == '!', <range>, <line1>, <line2>)
|
||||||
command! -buffer -range VimwikiRemoveSingleCB lua require('nuwiki.commands').list_remove_checkbox()
|
command! -buffer -range VimwikiRemoveSingleCB lua require('nuwiki.commands').list_remove_checkbox()
|
||||||
command! -buffer VimwikiRemoveCBInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
command! -buffer VimwikiRemoveCBInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
||||||
command! -buffer -nargs=? VimwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl()
|
command! -buffer -range -nargs=+ VimwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
command! -buffer VimwikiNormalizeLink lua require('nuwiki.commands').normalize_link()
|
command! -buffer -nargs=? VimwikiNormalizeLink lua require('nuwiki.commands').normalize_link(<args>)
|
||||||
command! -buffer VimwikiRenumberList lua require('nuwiki.commands').list_renumber()
|
command! -buffer VimwikiRenumberList lua require('nuwiki.commands').list_renumber()
|
||||||
command! -buffer VimwikiRenumberAllLists lua require('nuwiki.commands').list_renumber_all()
|
command! -buffer VimwikiRenumberAllLists lua require('nuwiki.commands').list_renumber_all()
|
||||||
command! -buffer VimwikiTableAlignQ lua require('nuwiki.commands').table_align()
|
command! -buffer -nargs=? VimwikiTableAlignQ lua require('nuwiki.commands').table_align_or_cmd('gqq')
|
||||||
command! -buffer VimwikiTableAlignW lua require('nuwiki.commands').table_align()
|
command! -buffer -nargs=? VimwikiTableAlignW lua require('nuwiki.commands').table_align_or_cmd('gww')
|
||||||
|
command! -buffer -nargs=* VimwikiReturn lua require('nuwiki.commands').vimwiki_return()
|
||||||
|
command! -buffer -nargs=* NuwikiReturn lua require('nuwiki.commands').vimwiki_return()
|
||||||
|
|
||||||
command! -buffer Vimwiki2HTML lua require('nuwiki.commands').export_current()
|
command! -buffer Vimwiki2HTML lua require('nuwiki.commands').export_current()
|
||||||
command! -buffer Vimwiki2HTMLBrowse lua require('nuwiki.commands').export_browse()
|
command! -buffer Vimwiki2HTMLBrowse lua require('nuwiki.commands').export_browse()
|
||||||
@@ -429,64 +499,65 @@ command! -buffer -bang VimwikiAll2HTML execute (<bang>0 ? "lua require('nuwi
|
|||||||
command! -buffer VimwikiRss lua require('nuwiki.commands').export_rss()
|
command! -buffer VimwikiRss lua require('nuwiki.commands').export_rss()
|
||||||
|
|
||||||
command! -buffer VimwikiTOC lua require('nuwiki.commands').toc_generate()
|
command! -buffer VimwikiTOC lua require('nuwiki.commands').toc_generate()
|
||||||
command! -buffer VimwikiGenerateLinks lua require('nuwiki.commands').links_generate()
|
command! -buffer -nargs=? VimwikiGenerateLinks lua require('nuwiki.commands').links_generate(<q-args>)
|
||||||
command! -buffer VimwikiCheckLinks lua require('nuwiki.commands').check_links()
|
command! -buffer -range VimwikiCheckLinks lua require('nuwiki.commands').check_links(<range>, <line1>, <line2>)
|
||||||
command! -buffer VimwikiFindOrphans lua require('nuwiki.commands').find_orphans()
|
command! -buffer VimwikiFindOrphans lua require('nuwiki.commands').find_orphans()
|
||||||
|
|
||||||
command! -buffer VimwikiRebuildTags lua require('nuwiki.commands').tags_rebuild()
|
command! -buffer -bang VimwikiRebuildTags lua require('nuwiki.commands').tags_rebuild('<bang>' == '!')
|
||||||
command! -buffer -nargs=? VimwikiSearchTags lua require('nuwiki.commands').tags_search(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags VimwikiSearchTags lua require('nuwiki.commands').tags_search(<q-args>)
|
||||||
command! -buffer -nargs=? VimwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags VimwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
||||||
command! -buffer -nargs=? VimwikiGenerateTags lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags VimwikiGenerateTags lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
||||||
|
|
||||||
" Tables, colorize, and clipboard paste.
|
" Tables, colorize, and clipboard paste.
|
||||||
command! -buffer -nargs=1 VimwikiTable lua require('nuwiki.commands').table_insert()
|
command! -buffer -nargs=* VimwikiTable lua require('nuwiki.commands').table_insert(<f-args>)
|
||||||
command! -buffer VimwikiTableMoveColumnLeft lua require('nuwiki.commands').table_move_column_left()
|
command! -buffer VimwikiTableMoveColumnLeft lua require('nuwiki.commands').table_move_column_left()
|
||||||
command! -buffer VimwikiTableMoveColumnRight lua require('nuwiki.commands').table_move_column_right()
|
command! -buffer VimwikiTableMoveColumnRight lua require('nuwiki.commands').table_move_column_right()
|
||||||
command! -buffer -nargs=1 VimwikiColorize lua require('nuwiki.commands').colorize()
|
command! -buffer -nargs=* -range -complete=customlist,nuwiki#complete#colors VimwikiColorize lua require('nuwiki.commands').colorize(<q-args>, <range> > 0)
|
||||||
command! -buffer VimwikiPasteLink lua require('nuwiki.commands').paste_link()
|
command! -buffer VimwikiPasteLink lua require('nuwiki.commands').paste_link()
|
||||||
command! -buffer VimwikiPasteUrl lua require('nuwiki.commands').paste_url()
|
command! -buffer VimwikiPasteUrl lua require('nuwiki.commands').paste_url()
|
||||||
command! -buffer VimwikiCatUrl lua require('nuwiki.commands').cat_url()
|
command! -buffer VimwikiCatUrl lua require('nuwiki.commands').cat_url()
|
||||||
|
|
||||||
" Canonical :Nuwiki* aliases — P10.
|
" Canonical :Nuwiki* aliases — P10.
|
||||||
command! -buffer -count NuwikiIndex lua require('nuwiki.commands').wiki_index(vim.v.count)
|
command! -buffer -count=0 NuwikiIndex lua require('nuwiki.commands').wiki_index(<count>)
|
||||||
command! -buffer NuwikiTabIndex lua require('nuwiki.commands').wiki_tab_index(vim.v.count)
|
command! -buffer -count=0 NuwikiTabIndex lua require('nuwiki.commands').wiki_tab_index(<count>)
|
||||||
command! -buffer NuwikiUISelect lua require('nuwiki.commands').wiki_ui_select()
|
command! -buffer NuwikiUISelect lua require('nuwiki.commands').wiki_ui_select()
|
||||||
command! -buffer NuwikiDiaryIndex lua require('nuwiki.commands').diary_index()
|
command! -buffer -count=0 NuwikiDiaryIndex lua require('nuwiki.commands').diary_index(<count>)
|
||||||
command! -buffer NuwikiDiaryToday lua require('nuwiki.commands').diary_today()
|
command! -buffer -count=0 NuwikiDiaryToday lua require('nuwiki.commands').diary_today(<count>)
|
||||||
command! -buffer NuwikiDiaryYesterday lua require('nuwiki.commands').diary_yesterday()
|
command! -buffer -count=0 NuwikiDiaryYesterday lua require('nuwiki.commands').diary_yesterday(<count>)
|
||||||
command! -buffer NuwikiDiaryTomorrow lua require('nuwiki.commands').diary_tomorrow()
|
command! -buffer -count=0 NuwikiDiaryTomorrow lua require('nuwiki.commands').diary_tomorrow(<count>)
|
||||||
command! -buffer NuwikiDiaryNext lua require('nuwiki.commands').diary_next()
|
command! -buffer NuwikiDiaryNext lua require('nuwiki.commands').diary_next()
|
||||||
command! -buffer NuwikiDiaryPrev lua require('nuwiki.commands').diary_prev()
|
command! -buffer NuwikiDiaryPrev lua require('nuwiki.commands').diary_prev()
|
||||||
command! -buffer NuwikiDiaryGenerateLinks lua require('nuwiki.commands').diary_generate_index()
|
command! -buffer NuwikiDiaryGenerateLinks lua require('nuwiki.commands').diary_generate_index()
|
||||||
command! -buffer NuwikiFollowLink lua vim.lsp.buf.definition()
|
command! -buffer NuwikiFollowLink lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer NuwikiBacklinks lua vim.lsp.buf.references()
|
command! -buffer NuwikiBacklinks lua vim.lsp.buf.references()
|
||||||
command! -buffer NuwikiDeleteFile lua require('nuwiki.commands').delete_file()
|
command! -buffer NuwikiDeleteFile lua require('nuwiki.commands').delete_file()
|
||||||
command! -buffer NuwikiRenameFile lua require('nuwiki.commands').rename_file()
|
command! -buffer -nargs=? NuwikiRenameFile lua require('nuwiki.commands').rename_file()
|
||||||
command! -buffer NuwikiNextTask lua require('nuwiki.commands').next_task()
|
command! -buffer NuwikiNextTask lua require('nuwiki.commands').next_task()
|
||||||
command! -buffer NuwikiToggleListItem lua require('nuwiki.commands').toggle_list_item()
|
command! -buffer -range NuwikiToggleListItem lua require('nuwiki.commands').toggle_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer NuwikiToggleRejected lua require('nuwiki.commands').reject_list_item()
|
command! -buffer -range NuwikiToggleRejected lua require('nuwiki.commands').reject_list_item_range(<line1>, <line2>)
|
||||||
command! -buffer NuwikiExport lua require('nuwiki.commands').export_current()
|
command! -buffer NuwikiExport lua require('nuwiki.commands').export_current()
|
||||||
command! -buffer NuwikiExportBrowse lua require('nuwiki.commands').export_browse()
|
command! -buffer NuwikiExportBrowse lua require('nuwiki.commands').export_browse()
|
||||||
command! -buffer -bang NuwikiExportAll execute (<bang>0 ? "lua require('nuwiki.commands').export_all_force()" : "lua require('nuwiki.commands').export_all()")
|
command! -buffer -bang NuwikiExportAll execute (<bang>0 ? "lua require('nuwiki.commands').export_all_force()" : "lua require('nuwiki.commands').export_all()")
|
||||||
command! -buffer NuwikiRss lua require('nuwiki.commands').export_rss()
|
command! -buffer NuwikiRss lua require('nuwiki.commands').export_rss()
|
||||||
command! -buffer NuwikiTOC lua require('nuwiki.commands').toc_generate()
|
command! -buffer NuwikiTOC lua require('nuwiki.commands').toc_generate()
|
||||||
command! -buffer NuwikiGenerateLinks lua require('nuwiki.commands').links_generate()
|
command! -buffer -nargs=? NuwikiGenerateLinks lua require('nuwiki.commands').links_generate(<q-args>)
|
||||||
command! -buffer NuwikiCheckLinks lua require('nuwiki.commands').check_links()
|
command! -buffer -range NuwikiCheckLinks lua require('nuwiki.commands').check_links(<range>, <line1>, <line2>)
|
||||||
command! -buffer NuwikiFindOrphans lua require('nuwiki.commands').find_orphans()
|
command! -buffer NuwikiFindOrphans lua require('nuwiki.commands').find_orphans()
|
||||||
command! -buffer NuwikiRebuildTags lua require('nuwiki.commands').tags_rebuild()
|
command! -buffer -bang NuwikiRebuildTags lua require('nuwiki.commands').tags_rebuild('<bang>' == '!')
|
||||||
command! -buffer -nargs=? NuwikiSearchTags lua require('nuwiki.commands').tags_search(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags NuwikiSearchTags lua require('nuwiki.commands').tags_search(<q-args>)
|
||||||
command! -buffer -nargs=? NuwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags NuwikiGenerateTagLinks lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
||||||
command! -buffer -nargs=1 NuwikiGoto lua require('nuwiki.commands').wiki_goto_page(<q-args>)
|
command! -buffer -nargs=? -complete=customlist,nuwiki#complete#tags NuwikiGenerateTags lua require('nuwiki.commands').tags_generate_links(<q-args>)
|
||||||
|
command! -buffer -nargs=* -complete=customlist,nuwiki#complete#pages NuwikiGoto lua require('nuwiki.commands').wiki_goto_page(<q-args>)
|
||||||
|
|
||||||
" Canonical :Nuwiki* names that mirror the documented surface.
|
" Canonical :Nuwiki* names that mirror the documented surface.
|
||||||
" The older :Nuwiki* spellings above stay defined for back-compat.
|
" The older :Nuwiki* spellings above stay defined for back-compat.
|
||||||
command! -buffer NuwikiNextLink lua require('nuwiki.commands').link_next()
|
command! -buffer NuwikiNextLink lua require('nuwiki.commands').link_next()
|
||||||
command! -buffer NuwikiPrevLink lua require('nuwiki.commands').link_prev()
|
command! -buffer NuwikiPrevLink lua require('nuwiki.commands').link_prev()
|
||||||
command! -buffer NuwikiBaddLink lua require('nuwiki.commands').badd_link()
|
command! -buffer NuwikiBaddLink lua require('nuwiki.commands').badd_link()
|
||||||
command! -buffer NuwikiMakeDiaryNote lua require('nuwiki.commands').diary_today()
|
command! -buffer -count=0 NuwikiMakeDiaryNote lua require('nuwiki.commands').diary_today(<count>)
|
||||||
command! -buffer NuwikiTabMakeDiaryNote lua require('nuwiki.commands').diary_today_tab()
|
command! -buffer -count=0 NuwikiTabMakeDiaryNote lua require('nuwiki.commands').diary_today_tab(<count>)
|
||||||
command! -buffer NuwikiMakeYesterdayDiaryNote lua require('nuwiki.commands').diary_yesterday()
|
command! -buffer -count=0 NuwikiMakeYesterdayDiaryNote lua require('nuwiki.commands').diary_yesterday(<count>)
|
||||||
command! -buffer NuwikiMakeTomorrowDiaryNote lua require('nuwiki.commands').diary_tomorrow()
|
command! -buffer -count=0 NuwikiMakeTomorrowDiaryNote lua require('nuwiki.commands').diary_tomorrow(<count>)
|
||||||
command! -buffer NuwikiDiaryNextDay lua require('nuwiki.commands').diary_next()
|
command! -buffer NuwikiDiaryNextDay lua require('nuwiki.commands').diary_next()
|
||||||
command! -buffer NuwikiDiaryPrevDay lua require('nuwiki.commands').diary_prev()
|
command! -buffer NuwikiDiaryPrevDay lua require('nuwiki.commands').diary_prev()
|
||||||
command! -buffer Nuwiki2HTML lua require('nuwiki.commands').export_current()
|
command! -buffer Nuwiki2HTML lua require('nuwiki.commands').export_current()
|
||||||
@@ -494,27 +565,27 @@ command! -buffer Nuwiki2HTMLBrowse lua require('nuwiki.command
|
|||||||
command! -buffer -bang NuwikiAll2HTML execute (<bang>0 ? "lua require('nuwiki.commands').export_all_force()" : "lua require('nuwiki.commands').export_all()")
|
command! -buffer -bang NuwikiAll2HTML execute (<bang>0 ? "lua require('nuwiki.commands').export_all_force()" : "lua require('nuwiki.commands').export_all()")
|
||||||
|
|
||||||
command! -buffer NuwikiGoBackLink execute "normal! \<C-o>"
|
command! -buffer NuwikiGoBackLink execute "normal! \<C-o>"
|
||||||
command! -buffer NuwikiSplitLink split | lua vim.lsp.buf.definition()
|
command! -buffer -nargs=* NuwikiSplitLink split | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer NuwikiVSplitLink vsplit | lua vim.lsp.buf.definition()
|
command! -buffer -nargs=* NuwikiVSplitLink vsplit | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer NuwikiTabnewLink tabnew | lua vim.lsp.buf.definition()
|
command! -buffer NuwikiTabnewLink tabnew | lua require('nuwiki.commands').follow_link_or_create()
|
||||||
command! -buffer NuwikiTabDropLink lua require('nuwiki.commands').follow_link_drop()
|
command! -buffer NuwikiTabDropLink lua require('nuwiki.commands').follow_link_drop()
|
||||||
command! -buffer -bang NuwikiRemoveDone lua require('nuwiki.commands')[('<bang>' == '!') and 'list_remove_done_all' or 'list_remove_done']()
|
command! -buffer -bang -range NuwikiRemoveDone lua require('nuwiki.commands').remove_done('<bang>' == '!', <range>, <line1>, <line2>)
|
||||||
command! -buffer -range NuwikiRemoveCheckbox lua require('nuwiki.commands').list_remove_checkbox()
|
command! -buffer -range NuwikiRemoveCheckbox lua require('nuwiki.commands').list_remove_checkbox()
|
||||||
command! -buffer NuwikiRemoveCheckboxInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
command! -buffer NuwikiRemoveCheckboxInList lua require('nuwiki.commands').list_remove_checkbox_in_list()
|
||||||
command! -buffer -nargs=? NuwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl()
|
command! -buffer -range -nargs=+ NuwikiListChangeLvl lua require('nuwiki.commands').list_change_lvl(<line1>, <line2>, <f-args>)
|
||||||
command! -buffer NuwikiListToggle lua require('nuwiki.commands').list_toggle_or_add_checkbox()
|
command! -buffer NuwikiListToggle lua require('nuwiki.commands').list_toggle_or_add_checkbox()
|
||||||
command! -buffer NuwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol(1)
|
command! -buffer -range NuwikiIncrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(1, <line1>, <line2>)
|
||||||
command! -buffer NuwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol(-1)
|
command! -buffer -range NuwikiDecrementListItem lua require('nuwiki.commands').list_cycle_symbol_range(-1, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 NuwikiChangeSymbol lua require('nuwiki.commands').list_change_symbol(<q-args>, false)
|
command! -buffer -range -nargs=1 NuwikiChangeSymbol lua require('nuwiki.commands').list_change_symbol_range(<q-args>, <line1>, <line2>)
|
||||||
command! -buffer -nargs=1 NuwikiChangeSymbolInList lua require('nuwiki.commands').list_change_symbol(<q-args>, true)
|
command! -buffer -nargs=1 NuwikiChangeSymbolInList lua require('nuwiki.commands').list_change_symbol(<q-args>, true)
|
||||||
command! -buffer NuwikiNormalizeLink lua require('nuwiki.commands').normalize_link()
|
command! -buffer -nargs=? NuwikiNormalizeLink lua require('nuwiki.commands').normalize_link(<args>)
|
||||||
command! -buffer NuwikiRenumberList lua require('nuwiki.commands').list_renumber()
|
command! -buffer NuwikiRenumberList lua require('nuwiki.commands').list_renumber()
|
||||||
command! -buffer NuwikiRenumberAllLists lua require('nuwiki.commands').list_renumber_all()
|
command! -buffer NuwikiRenumberAllLists lua require('nuwiki.commands').list_renumber_all()
|
||||||
command! -buffer NuwikiTableAlign lua require('nuwiki.commands').table_align()
|
command! -buffer -nargs=? NuwikiTableAlign lua require('nuwiki.commands').table_align_or_cmd('gqq')
|
||||||
command! -buffer -nargs=1 NuwikiTable lua require('nuwiki.commands').table_insert()
|
command! -buffer -nargs=* NuwikiTable lua require('nuwiki.commands').table_insert(<f-args>)
|
||||||
command! -buffer NuwikiTableMoveColumnLeft lua require('nuwiki.commands').table_move_column_left()
|
command! -buffer NuwikiTableMoveColumnLeft lua require('nuwiki.commands').table_move_column_left()
|
||||||
command! -buffer NuwikiTableMoveColumnRight lua require('nuwiki.commands').table_move_column_right()
|
command! -buffer NuwikiTableMoveColumnRight lua require('nuwiki.commands').table_move_column_right()
|
||||||
command! -buffer -nargs=1 NuwikiColorize lua require('nuwiki.commands').colorize()
|
command! -buffer -nargs=* -range -complete=customlist,nuwiki#complete#colors NuwikiColorize lua require('nuwiki.commands').colorize(<q-args>, <range> > 0)
|
||||||
command! -buffer NuwikiPasteLink lua require('nuwiki.commands').paste_link()
|
command! -buffer NuwikiPasteLink lua require('nuwiki.commands').paste_link()
|
||||||
command! -buffer NuwikiPasteUrl lua require('nuwiki.commands').paste_url()
|
command! -buffer NuwikiPasteUrl lua require('nuwiki.commands').paste_url()
|
||||||
command! -buffer NuwikiCatUrl lua require('nuwiki.commands').cat_url()
|
command! -buffer NuwikiCatUrl lua require('nuwiki.commands').cat_url()
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
# Known Issues & Divergences from vimwiki
|
||||||
|
|
||||||
|
nuwiki is an LSP-backed reimplementation of [vimwiki](https://github.com/vimwiki/vimwiki),
|
||||||
|
not a fork of the original VimL plugin. It targets behavioral parity with
|
||||||
|
vimwiki, but a handful of upstream options and behaviors are implemented
|
||||||
|
differently — or intentionally not at all — because of that architecture.
|
||||||
|
|
||||||
|
This page is the single source of truth for how nuwiki differs from vimwiki.
|
||||||
|
Everything here is either a deliberate design choice or a tracked future task;
|
||||||
|
none of it is an accidental bug.
|
||||||
|
|
||||||
|
If you hit a behavior that *isn't* listed here and doesn't match vimwiki, please
|
||||||
|
file it — that's a real bug.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deferred (planned, not yet implemented)
|
||||||
|
|
||||||
|
These are genuine feature gaps that we intend to close, grouped because they
|
||||||
|
ship together.
|
||||||
|
|
||||||
|
### Markdown generated-content
|
||||||
|
|
||||||
|
The buffer-side generators — TOC (`:VimwikiTOC`), generated links
|
||||||
|
(`:VimwikiGenerateLinks`), the tags index (`:VimwikiGenerateTagLinks`), and the
|
||||||
|
diary index — always emit **vimwiki** markup (`= Heading =`), regardless of the
|
||||||
|
wiki's `syntax`. Markdown wikis therefore get vimwiki-syntax generated sections.
|
||||||
|
|
||||||
|
Dependent on markdown-syntax generation, and deferred until it lands:
|
||||||
|
|
||||||
|
- **markdown-style heading generation** in those generators (`# Heading`).
|
||||||
|
- **`markdown_header_style`** — blank lines after a generated *markdown* header
|
||||||
|
(no markdown header exists to attach it to yet).
|
||||||
|
- **`markdown_link_ext`** — append the file extension in generated markdown links.
|
||||||
|
|
||||||
|
Hand-authored markdown content renders fine; only the *generated* sections are
|
||||||
|
affected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Behavioral divergences (by design)
|
||||||
|
|
||||||
|
These upstream options have no nuwiki equivalent, because the LSP server
|
||||||
|
provides the capability a different way. They will not be re-implemented as-is.
|
||||||
|
|
||||||
|
### Highlighting & display
|
||||||
|
|
||||||
|
| vimwiki option | nuwiki behavior |
|
||||||
|
|----------------|-----------------|
|
||||||
|
| `maxhi` | Broken/existence-based link highlighting is provided by **LSP diagnostics** instead (severity is configurable via `diagnostic.link_severity`). |
|
||||||
|
| `conceallevel`, `conceal_onechar_markers`, other `conceal*` | Replaced by **LSP semantic tokens**; there is no conceal layer to configure. |
|
||||||
|
|
||||||
|
### Syntax detection
|
||||||
|
|
||||||
|
| vimwiki option | nuwiki behavior |
|
||||||
|
|----------------|-----------------|
|
||||||
|
| `nested_syntaxes` / `automatic_nested_syntaxes` | Code-fence languages are auto-detected from the fence tag (` ```python `); always on, no toggle. |
|
||||||
|
| `ext2syntax` | Syntax is chosen by file extension / the per-wiki `syntax` key automatically. |
|
||||||
|
| `global_ext` | The configured wiki extension(s) always map to the `vimwiki` filetype regardless of location; there is no per-wiki "only inside the root" toggle (effectively always on). |
|
||||||
|
| `syntax` default name | nuwiki defaults the per-wiki `syntax` to `vimwiki` where upstream defaults to `default` — a naming difference only, not behavioral. |
|
||||||
|
|
||||||
|
### Configuration mechanism
|
||||||
|
|
||||||
|
| vimwiki option | nuwiki behavior |
|
||||||
|
|----------------|-----------------|
|
||||||
|
| `folding` (string) | Replaced by `folding = 'lsp' \| 'expr' \| 'off'`, backed by the LSP `foldingRange` provider. |
|
||||||
|
| `key_mappings` (dict) | Replaced by the Lua `mappings.<group>` table and the `g:nuwiki_no_<group>_mappings` globals. |
|
||||||
|
|
||||||
|
### Tags
|
||||||
|
|
||||||
|
| vimwiki option | nuwiki behavior |
|
||||||
|
|----------------|-----------------|
|
||||||
|
| `auto_tags` | Upstream maintains an on-disk tag *metadata* file on save. nuwiki has no such file: the server re-indexes tags on every change, so tag search / jump / completion are always fresh. The configurable on-save piece (regenerating the in-buffer links section) is `auto_generate_tags`. |
|
||||||
|
|
||||||
|
### Mappings
|
||||||
|
|
||||||
|
| vimwiki option | nuwiki behavior |
|
||||||
|
|----------------|-----------------|
|
||||||
|
| Mouse maps (`<2-LeftMouse>`, `<MiddleMouse>`, …) | Upstream binds these unconditionally; nuwiki ships them **opt-in** via `mappings.mouse` / `g:nuwiki_mouse_mappings`. |
|
||||||
|
|
||||||
|
### Value-semantics differences
|
||||||
|
|
||||||
|
| vimwiki option | nuwiki behavior |
|
||||||
|
|----------------|-----------------|
|
||||||
|
| `list_margin` (negative) | Upstream resolves a negative value to the buffer's `'shiftwidth'`. The server can't observe `'shiftwidth'`, so negative values collapse to **zero** leading indent on generated bullets. Set a non-negative number for a fixed indent. |
|
||||||
|
| `diary_caption_level = -1` | Accepted (the field is deserializable), but **clamps to `0`**. nuwiki builds the diary index tree from dates, so upstream's `-1` = "no per-page captions" semantic doesn't apply. |
|
||||||
|
|
||||||
|
### Vim/Win shims & syntax internals (not applicable)
|
||||||
|
|
||||||
|
`CJK_length`, `listing_hl*`, `schemes_*`, `w32_dir_enc`, `menu`, `rx_todo`,
|
||||||
|
`tag_format` — these are syntax internals, menu definitions, or Vim/Windows
|
||||||
|
shims with no role in an LSP-backed implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default-location differences
|
||||||
|
|
||||||
|
Where you don't set a path explicitly, nuwiki derives a different *default
|
||||||
|
location* than upstream — by design, so each wiki is root-relative and
|
||||||
|
self-contained. You can set any of these explicitly to match upstream exactly.
|
||||||
|
|
||||||
|
| Key | nuwiki default | upstream default |
|
||||||
|
|-----|----------------|------------------|
|
||||||
|
| `html_path` | `<root>/_html` | sibling `<wikidir>_html/` |
|
||||||
|
| `template_path` | `<root>/_templates` | global `~/vimwiki/templates/` |
|
||||||
|
| `diary_rel_path` | `diary` | `diary/` (trailing slash only — paths join identically, so behavior matches) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal deferrals (non-parity)
|
||||||
|
|
||||||
|
These are not vimwiki differences — they're known low-priority optimizations on
|
||||||
|
cold code paths, recorded so they aren't rediscovered as "bugs." All are
|
||||||
|
correct as-is; the deferral is a deliberate risk/reward call.
|
||||||
|
|
||||||
|
- **RSS `render_entry_body` reads from disk.** It re-reads and re-parses each
|
||||||
|
diary entry from disk rather than reusing an open buffer. This is the *correct*
|
||||||
|
source of truth for an export artifact — switching to unsaved buffer text would
|
||||||
|
change semantics, not just performance.
|
||||||
|
- **`push_level_edit_for_line` scans from offset 0.** The list-level change
|
||||||
|
(`gl`/`gL`) computes each line's start offset by scanning from the document
|
||||||
|
start. It's an interactive single-keypress path over small inputs, and the
|
||||||
|
offset-arithmetic rewrite carries more regression risk than the saved time is
|
||||||
|
worth.
|
||||||
|
- **Diary next/prev neighbor scan.** `:VimwikiDiaryNextDay` / `PrevDay` rebuild
|
||||||
|
and sort the entry list to find one neighbor. Same reasoning — cold path,
|
||||||
|
small input.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Upstream `g:vimwiki_list` config (Vim & Neovim).** When you haven't
|
||||||
|
configured nuwiki natively, both clients read your upstream `g:vimwiki_list` +
|
||||||
|
`g:vimwiki_*` globals and translate them into nuwiki's schema (per-wiki
|
||||||
|
`path`/`path_html`/`template_*`/`auto_export`, plus globals `toc_header`,
|
||||||
|
`toc_header_level`, `html_header_numbering`, `html_header_numbering_sym`,
|
||||||
|
`links_space_char`, `list_margin`). `g:nuwiki_*` / `setup()` config always
|
||||||
|
wins. Globals nuwiki doesn't model (e.g. `automatic_nested_syntaxes`) are
|
||||||
|
ignored — see the divergence list above. On Neovim this means swapping the
|
||||||
|
`vimwiki` plugin for nuwiki under lazy.nvim works with your existing
|
||||||
|
`vim.g.vimwiki_list` untouched (call `require('nuwiki').setup()` with no args).
|
||||||
|
- **Third-party plugin compatibility.** A shim at `autoload/vimwiki/vars.vim`
|
||||||
|
exposes the subset of `vimwiki#vars#get_wikilocal` that plugins like
|
||||||
|
vimwiki-sync and vim-zettel call (`path`, `ext`/`extension`, `syntax`,
|
||||||
|
`is_temporary_wiki`), resolved from nuwiki's own config — including the
|
||||||
|
correct per-wiki values in a multi-wiki setup. Add keys there if a plugin you
|
||||||
|
use needs more.
|
||||||
|
- **Additive commands.** nuwiki adds some commands with no upstream equivalent
|
||||||
|
(e.g. `:NuwikiFindOrphans`). These are additions, not divergences.
|
||||||
+343
-68
@@ -237,17 +237,13 @@ function M.follow_link_drop()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.follow_link_or_create()
|
function M.follow_link_or_create()
|
||||||
if cursor_inside_wikilink() then
|
-- Already on a wikilink → follow it. Otherwise try to wrap the bare word
|
||||||
vim.lsp.buf.definition()
|
-- under the cursor as `[[word]]` and stop (a second press follows it,
|
||||||
|
-- matching vimwiki's review-then-follow flow). If there's nothing to wrap,
|
||||||
|
-- fall through to a plain definition request so a chord still works anywhere.
|
||||||
|
if not cursor_inside_wikilink() and wrap_cword_as_wikilink() then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if wrap_cword_as_wikilink() then
|
|
||||||
-- Wrapped — leave the user inside the new `[[…]]` and stop. A
|
|
||||||
-- second `<CR>` follows it, matching vimwiki's review-then-follow
|
|
||||||
-- flow.
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- No word under cursor either — fall through.
|
|
||||||
vim.lsp.buf.definition()
|
vim.lsp.buf.definition()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -280,6 +276,114 @@ end
|
|||||||
-- Pick a wiki and open its index. The list is read straight from config
|
-- Pick a wiki and open its index. The list is read straight from config
|
||||||
-- (no LSP round-trip) so this works from any buffer before a wiki file is
|
-- (no LSP round-trip) so this works from any buffer before a wiki file is
|
||||||
-- ever opened; opening the index then auto-starts the server.
|
-- ever opened; opening the index then auto-starts the server.
|
||||||
|
-- :VimwikiShowVersion / :NuwikiShowVersion — echo the nuwiki version + editor.
|
||||||
|
function M.show_version()
|
||||||
|
local ver = vim.g.nuwiki_version or '?'
|
||||||
|
vim.api.nvim_echo({
|
||||||
|
{ 'nuwiki: ' .. ver .. '\n' },
|
||||||
|
{ 'Neovim: ' .. tostring(vim.version()) },
|
||||||
|
}, false, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vimwiki `auto_header`: on a new wiki page, insert a level-1 header derived
|
||||||
|
-- from the filename. Skips the wiki index + diary index pages, and only acts
|
||||||
|
-- on a genuinely empty buffer. Called from a BufNewFile autocmd.
|
||||||
|
function M.auto_header(bufnr)
|
||||||
|
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
if #lines > 1 or (lines[1] or '') ~= '' then
|
||||||
|
return -- not an empty new buffer
|
||||||
|
end
|
||||||
|
local file = vim.api.nvim_buf_get_name(bufnr)
|
||||||
|
local config = require('nuwiki.config')
|
||||||
|
local space = ' '
|
||||||
|
for _, c in ipairs(config.wiki_list()) do
|
||||||
|
local root = vim.fn.fnamemodify(vim.fn.expand(c.root), ':p')
|
||||||
|
if root:sub(-1) ~= '/' then root = root .. '/' end
|
||||||
|
if file:sub(1, #root) == root then
|
||||||
|
-- Skip the wiki index and the diary index pages.
|
||||||
|
local index_path = root .. c.idx .. c.ext
|
||||||
|
local diary_index_path = root .. c.diary_rel .. '/' .. c.diary_idx .. c.ext
|
||||||
|
if file == index_path or file == diary_index_path then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
space = c.space or ' '
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Convert links_space_char back to spaces for the title (vimwiki parity).
|
||||||
|
local title = vim.fn.fnamemodify(file, ':t:r')
|
||||||
|
if space ~= ' ' and space ~= '' then
|
||||||
|
title = title:gsub(vim.pesc(space), ' ')
|
||||||
|
end
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, { '= ' .. title .. ' =', '' })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- :VimwikiSearch / :VWS {pattern} — search the current wiki's files (upstream
|
||||||
|
-- scopes to the wiki, not the editor CWD). Resolves the wiki whose root holds
|
||||||
|
-- the current file, else the first configured wiki, else CWD. Empty pattern
|
||||||
|
-- reuses the last search. Populates the location list.
|
||||||
|
function M.search(args)
|
||||||
|
local pat = (args == nil or args == '') and vim.fn.getreg('/') or args
|
||||||
|
if pat == '' then
|
||||||
|
vim.notify('nuwiki: no search pattern', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local config = require('nuwiki.config')
|
||||||
|
local file = vim.fn.expand('%:p')
|
||||||
|
local root, ext = '', '.wiki'
|
||||||
|
for _, w in ipairs(config.wiki_list()) 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
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if root == '' then
|
||||||
|
local first = config.wiki_list()[1]
|
||||||
|
if first then
|
||||||
|
root = vim.fn.fnamemodify(vim.fn.expand(first.root), ':p')
|
||||||
|
ext = first.ext or ext
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
if not ok then
|
||||||
|
vim.notify('nuwiki: no match for ' .. pat, vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.cmd('lopen')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- :VimwikiVar / :NuwikiVar [key [value]] — get/set the nuwiki client config
|
||||||
|
-- (upstream's vimwiki#vars#cmd). No arg prints the resolved setup options; one
|
||||||
|
-- arg prints that key; two+ sets it on the in-memory options (server-backed
|
||||||
|
-- settings need a restart).
|
||||||
|
function M.var(arg)
|
||||||
|
local parts = vim.split(arg or '', '%s+', { trimempty = true })
|
||||||
|
local opts = require('nuwiki.config').options
|
||||||
|
if #parts == 0 then
|
||||||
|
local keys = vim.tbl_keys(opts)
|
||||||
|
table.sort(keys)
|
||||||
|
local lines = {}
|
||||||
|
for _, k in ipairs(keys) do
|
||||||
|
lines[#lines + 1] = { string.format('%s = %s\n', k, vim.inspect(opts[k])) }
|
||||||
|
end
|
||||||
|
vim.api.nvim_echo(lines, false, {})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local key = parts[1]
|
||||||
|
if #parts == 1 then
|
||||||
|
vim.api.nvim_echo({ { string.format('%s = %s', key, vim.inspect(opts[key])) } }, false, {})
|
||||||
|
else
|
||||||
|
opts[key] = table.concat(parts, ' ', 2)
|
||||||
|
vim.api.nvim_echo({ {
|
||||||
|
string.format('set %s = %s (restart for server-backed settings)', key, vim.inspect(opts[key])),
|
||||||
|
} }, false, {})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function M.wiki_ui_select()
|
function M.wiki_ui_select()
|
||||||
local wikis = require('nuwiki.config').wiki_list()
|
local wikis = require('nuwiki.config').wiki_list()
|
||||||
if #wikis == 0 then
|
if #wikis == 0 then
|
||||||
@@ -314,9 +418,16 @@ end
|
|||||||
|
|
||||||
-- ===== Diary =====
|
-- ===== Diary =====
|
||||||
|
|
||||||
|
-- `count` is vimwiki's diary `-count` — a wiki number (1-indexed at the user
|
||||||
|
-- level). When > 0 the server acts on that wiki (`wiki = count - 1`) instead
|
||||||
|
-- of the buffer's. next/prev ignore it.
|
||||||
local function _diary_open(cmd_name, tab)
|
local function _diary_open(cmd_name, tab)
|
||||||
return function()
|
return function(count)
|
||||||
exec(cmd_name, uri_args(), function(r)
|
local args = uri_args()
|
||||||
|
if count and count > 0 then
|
||||||
|
args[1].wiki = count - 1
|
||||||
|
end
|
||||||
|
exec(cmd_name, args, function(r)
|
||||||
if r and r.uri then open_uri(r.uri, tab) end
|
if r and r.uri then open_uri(r.uri, tab) end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -361,6 +472,31 @@ function M.cycle_list_item_back()
|
|||||||
exec('nuwiki.list.cycleCheckbox', a)
|
exec('nuwiki.list.cycleCheckbox', a)
|
||||||
end
|
end
|
||||||
M.reject_list_item = _exec_pos('nuwiki.list.rejectCheckbox')
|
M.reject_list_item = _exec_pos('nuwiki.list.rejectCheckbox')
|
||||||
|
|
||||||
|
-- Apply a per-line action across [l1, l2] (visual-range commands). Each call
|
||||||
|
-- targets its own line; the server's version-less single-line edits apply
|
||||||
|
-- independently as they return, so no conflict.
|
||||||
|
local function over_range(l1, l2, fn)
|
||||||
|
local save = vim.api.nvim_win_get_cursor(0)
|
||||||
|
for ln = l1, l2 do
|
||||||
|
vim.api.nvim_win_set_cursor(0, { ln, 0 })
|
||||||
|
fn()
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_win_set_cursor, 0, save)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.toggle_list_item_range(l1, l2)
|
||||||
|
over_range(l1, l2, M.toggle_list_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.list_cycle_symbol_range(direction, l1, l2)
|
||||||
|
over_range(l1, l2, function() M.list_cycle_symbol(direction) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.reject_list_item_range(l1, l2)
|
||||||
|
over_range(l1, l2, M.reject_list_item)
|
||||||
|
end
|
||||||
|
|
||||||
M.heading_add_level = _exec_pos('nuwiki.heading.addLevel')
|
M.heading_add_level = _exec_pos('nuwiki.heading.addLevel')
|
||||||
M.heading_remove_level = _exec_pos('nuwiki.heading.removeLevel')
|
M.heading_remove_level = _exec_pos('nuwiki.heading.removeLevel')
|
||||||
|
|
||||||
@@ -377,10 +513,28 @@ end
|
|||||||
|
|
||||||
-- ===== Generation + workspace =====
|
-- ===== Generation + workspace =====
|
||||||
|
|
||||||
M.toc_generate = function() exec('nuwiki.toc.generate', uri_args()) end
|
M.toc_generate = function()
|
||||||
M.links_generate = function() exec('nuwiki.links.generate', uri_args()) end
|
-- Send the cursor line (0-based) so a fresh TOC is inserted there.
|
||||||
|
local line = vim.api.nvim_win_get_cursor(0)[1] - 1
|
||||||
|
exec('nuwiki.toc.generate', { { uri = buf_uri(), line = line } })
|
||||||
|
end
|
||||||
|
-- `:VimwikiGenerateLinks [rel_path]` — optional path scopes the links to a
|
||||||
|
-- subtree via the server's generateForPath; no arg = every page.
|
||||||
|
M.links_generate = function(path)
|
||||||
|
-- Cursor line (0-based) so a fresh section is inserted there.
|
||||||
|
local line = vim.api.nvim_win_get_cursor(0)[1] - 1
|
||||||
|
if path and path ~= '' then
|
||||||
|
exec('nuwiki.links.generateForPath', { { uri = buf_uri(), path = path, line = line } })
|
||||||
|
else
|
||||||
|
exec('nuwiki.links.generate', { { uri = buf_uri(), line = line } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function M.check_links()
|
-- range>0 restricts the report to the current buffer's [l1, l2] lines
|
||||||
|
-- (`:'<,'>VimwikiCheckLinks`); otherwise the whole workspace.
|
||||||
|
function M.check_links(range, l1, l2)
|
||||||
|
local filter = type(range) == 'number' and range > 0
|
||||||
|
local cur = filter and vim.api.nvim_buf_get_name(0) or nil
|
||||||
exec('nuwiki.workspace.checkLinks', uri_args(), function(r)
|
exec('nuwiki.workspace.checkLinks', uri_args(), function(r)
|
||||||
if type(r) ~= 'table' or #r == 0 then
|
if type(r) ~= 'table' or #r == 0 then
|
||||||
vim.notify('nuwiki: no broken links')
|
vim.notify('nuwiki: no broken links')
|
||||||
@@ -388,12 +542,20 @@ function M.check_links()
|
|||||||
end
|
end
|
||||||
local items = {}
|
local items = {}
|
||||||
for _, b in ipairs(r) do
|
for _, b in ipairs(r) do
|
||||||
table.insert(items, {
|
local fname = vim.uri_to_fname(b.uri)
|
||||||
filename = vim.uri_to_fname(b.uri),
|
local lnum = (b.range and b.range.start and b.range.start.line or 0) + 1
|
||||||
lnum = (b.range and b.range.start and b.range.start.line or 0) + 1,
|
if not filter or (fname == cur and lnum >= l1 and lnum <= l2) then
|
||||||
col = (b.range and b.range.start and b.range.start.character or 0) + 1,
|
table.insert(items, {
|
||||||
text = (b.kind or '?') .. ': ' .. (b.message or ''),
|
filename = fname,
|
||||||
})
|
lnum = lnum,
|
||||||
|
col = (b.range and b.range.start and b.range.start.character or 0) + 1,
|
||||||
|
text = (b.kind or '?') .. ': ' .. (b.message or ''),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #items == 0 then
|
||||||
|
vim.notify('nuwiki: no broken links')
|
||||||
|
return
|
||||||
end
|
end
|
||||||
vim.fn.setqflist({}, ' ', { title = 'nuwiki broken links', items = items })
|
vim.fn.setqflist({}, ' ', { title = 'nuwiki broken links', items = items })
|
||||||
vim.cmd('copen')
|
vim.cmd('copen')
|
||||||
@@ -444,17 +606,22 @@ function M.tags_search(query)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.tags_generate_links(tag)
|
function M.tags_generate_links(tag)
|
||||||
local args = { uri = buf_uri() }
|
-- Cursor line (0-based) so a fresh section is inserted there.
|
||||||
|
local args = { uri = buf_uri(), line = vim.api.nvim_win_get_cursor(0)[1] - 1 }
|
||||||
if tag and tag ~= '' then
|
if tag and tag ~= '' then
|
||||||
args.tag = tag
|
args.tag = tag
|
||||||
end
|
end
|
||||||
exec('nuwiki.tags.generateLinks', { args })
|
exec('nuwiki.tags.generateLinks', { args })
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.tags_rebuild()
|
function M.tags_rebuild(all)
|
||||||
exec('nuwiki.tags.rebuild', uri_args(), function(r)
|
local a = uri_args()
|
||||||
|
a[1].all = all and true or false
|
||||||
|
exec('nuwiki.tags.rebuild', a, function(r)
|
||||||
if r and r.pages then
|
if r and r.pages then
|
||||||
vim.notify(string.format('nuwiki: re-indexed %d page(s)', r.pages))
|
local scope = (r.all and (r.wikis or 0) ~= 1)
|
||||||
|
and string.format(' across %d wikis', r.wikis or 0) or ''
|
||||||
|
vim.notify(string.format('nuwiki: re-indexed %d page(s)%s', r.pages, scope))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -512,6 +679,25 @@ function M.list_remove_done_all()
|
|||||||
exec('nuwiki.list.removeDone', uri_args())
|
exec('nuwiki.list.removeDone', uri_args())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- `:'<,'>VimwikiRemoveDone` — remove done items only within the given line
|
||||||
|
-- range (1-indexed, inclusive; converted to the server's 0-indexed range).
|
||||||
|
function M.list_remove_done_range(l1, l2)
|
||||||
|
exec('nuwiki.list.removeDone', { { uri = buf_uri(), range = { l1 - 1, l2 - 1 } } })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `:[range]VimwikiRemoveDone[!]` dispatcher. `!` → whole buffer; an explicit
|
||||||
|
-- range (`range > 0`, e.g. `:'<,'>`) → that line span; otherwise the current
|
||||||
|
-- list under the cursor. Mirrors upstream's `-bang -range` form.
|
||||||
|
function M.remove_done(bang, range, l1, l2)
|
||||||
|
if bang then
|
||||||
|
M.list_remove_done_all()
|
||||||
|
elseif range and range > 0 then
|
||||||
|
M.list_remove_done_range(l1, l2)
|
||||||
|
else
|
||||||
|
M.list_remove_done()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
-- `:VimwikiRemoveSingleCB` — strip the checkbox from the current item only.
|
||||||
function M.list_remove_checkbox()
|
function M.list_remove_checkbox()
|
||||||
exec('nuwiki.list.removeCheckbox', pos_args())
|
exec('nuwiki.list.removeCheckbox', pos_args())
|
||||||
@@ -541,6 +727,13 @@ function M.list_change_symbol(symbol, whole_list)
|
|||||||
exec('nuwiki.list.changeSymbol', { args })
|
exec('nuwiki.list.changeSymbol', { args })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- `:[range]VimwikiChangeSymbolTo {sym}` / `:[range]VimwikiListChangeSymbolI {sym}`
|
||||||
|
-- — upstream is -range -nargs=1; a range sets the marker on every list item in
|
||||||
|
-- the selection (single-item when no range, since l1==l2==cursor line).
|
||||||
|
function M.list_change_symbol_range(symbol, l1, l2)
|
||||||
|
over_range(l1, l2, function() M.list_change_symbol(symbol, false) end)
|
||||||
|
end
|
||||||
|
|
||||||
function M.list_change_level(delta, whole_subtree)
|
function M.list_change_level(delta, whole_subtree)
|
||||||
local args = pos_args()[1]
|
local args = pos_args()[1]
|
||||||
args.delta = delta
|
args.delta = delta
|
||||||
@@ -548,11 +741,14 @@ function M.list_change_level(delta, whole_subtree)
|
|||||||
exec('nuwiki.list.changeLevel', { args })
|
exec('nuwiki.list.changeLevel', { args })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Vimwiki's `:VimwikiListChangeLvl decrease|increase 0` keeps a single
|
-- `:[range]VimwikiListChangeLvl {increase|decrease} [plus_children]` — upstream
|
||||||
-- entry point. Forward to changeLevel.
|
-- is `-range -nargs=+`. Direction is required; the optional second arg cascades
|
||||||
function M.list_change_lvl(direction)
|
-- to each item's subtree; a range applies it to every list item in the range.
|
||||||
|
function M.list_change_lvl(line1, line2, direction, plus_children)
|
||||||
local delta = (direction == 'increase' or direction == 'indent') and 1 or -1
|
local delta = (direction == 'increase' or direction == 'indent') and 1 or -1
|
||||||
M.list_change_level(delta, false)
|
local n = tonumber(plus_children)
|
||||||
|
local children = n ~= nil and n ~= 0
|
||||||
|
over_range(line1, line2, function() M.list_change_level(delta, children) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Cycle the list marker forward (`direction = 1`) or backward
|
--- Cycle the list marker forward (`direction = 1`) or backward
|
||||||
@@ -563,25 +759,15 @@ local _symbol_order = { '-', '*', '#', '1.', '1)', 'a)', 'A)', 'i)', 'I)' }
|
|||||||
|
|
||||||
local function detect_current_symbol()
|
local function detect_current_symbol()
|
||||||
local line = vim.api.nvim_get_current_line()
|
local line = vim.api.nvim_get_current_line()
|
||||||
-- Match optional indent, marker, then whitespace.
|
-- Non-capturing indent; capture only the marker/separator we need.
|
||||||
local indent, marker = line:match('^(%s*)([%-%*%#]) ')
|
local marker = line:match('^%s*([%-%*%#]) ')
|
||||||
if marker then return marker end
|
if marker then return marker end
|
||||||
local n
|
local sep = line:match('^%s*%d+([%.%)]) ')
|
||||||
indent, n, marker = line:match('^(%s*)(%d+)([%.%)]) ')
|
if sep then return '1' .. sep end
|
||||||
if marker then return '1' .. marker end
|
if line:match('^%s*[ivxlcdm]+%) ') then return 'i)' end
|
||||||
-- Roman lower
|
if line:match('^%s*[IVXLCDM]+%) ') then return 'I)' end
|
||||||
indent, marker = line:match('^(%s*)([ivxlcdm]+)%) ')
|
if line:match('^%s*[a-z]+%) ') then return 'a)' end
|
||||||
if marker then return 'i)' end
|
if line:match('^%s*[A-Z]+%) ') then return 'A)' end
|
||||||
-- Roman upper
|
|
||||||
indent, marker = line:match('^(%s*)([IVXLCDM]+)%) ')
|
|
||||||
if marker then return 'I)' end
|
|
||||||
-- Alpha lower (single letter or short run)
|
|
||||||
indent, marker = line:match('^(%s*)([a-z]+)%) ')
|
|
||||||
if marker then return 'a)' end
|
|
||||||
indent, marker = line:match('^(%s*)([A-Z]+)%) ')
|
|
||||||
if marker then return 'A)' end
|
|
||||||
-- Suppress unused warning
|
|
||||||
local _ = indent
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -806,6 +992,9 @@ function M.smart_return()
|
|||||||
local cr = tc('<CR>')
|
local cr = tc('<CR>')
|
||||||
local esc = tc('<Esc>')
|
local esc = tc('<Esc>')
|
||||||
|
|
||||||
|
-- Accept the completion (plain <CR>) when the popup menu is open, like upstream.
|
||||||
|
if vim.fn.pumvisible() == 1 then return cr end
|
||||||
|
|
||||||
if is_table_row(line) then
|
if is_table_row(line) then
|
||||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
return string.format(
|
return string.format(
|
||||||
@@ -843,6 +1032,41 @@ function M.smart_return()
|
|||||||
return cr
|
return cr
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Insert-mode `<S-CR>`: continue the current list item on a new line WITHOUT a
|
||||||
|
-- new marker, indented to align under the item's text — vimwiki's "multiline
|
||||||
|
-- list item" (`VimwikiReturn 2 2`). Off a list item it's a plain `<CR>`.
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- :VimwikiReturn / :NuwikiReturn — the command form of the smart `<CR>`
|
||||||
|
-- continuation (upstream's mapping-driven VimwikiReturn). Enters insert at end
|
||||||
|
-- of line and triggers the buffer-local `<CR>` mapping (smart_return). Args
|
||||||
|
-- mirror upstream's mode flags and are unused (continuation is inferred).
|
||||||
|
function M.vimwiki_return()
|
||||||
|
vim.api.nvim_feedkeys(
|
||||||
|
vim.api.nvim_replace_termcodes('A<CR>', true, false, true), 'm', false)
|
||||||
|
end
|
||||||
|
|
||||||
--- Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
--- Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||||
--- Used as `<expr>` keymaps so we can fall back to a literal `<Tab>` /
|
--- Used as `<expr>` keymaps so we can fall back to a literal `<Tab>` /
|
||||||
--- `<S-Tab>` when the cursor isn't on a table row. Inside a row both
|
--- `<S-Tab>` when the cursor isn't on a table row. Inside a row both
|
||||||
@@ -922,7 +1146,7 @@ end
|
|||||||
-- Cluster B — table rewriters.
|
-- Cluster B — table rewriters.
|
||||||
|
|
||||||
function M.table_insert(cols, rows)
|
function M.table_insert(cols, rows)
|
||||||
cols = tonumber(cols) or 3
|
cols = tonumber(cols) or 5
|
||||||
rows = tonumber(rows) or 2
|
rows = tonumber(rows) or 2
|
||||||
local p = pos_args()[1]
|
local p = pos_args()[1]
|
||||||
p.cols = cols
|
p.cols = cols
|
||||||
@@ -934,6 +1158,17 @@ function M.table_align()
|
|||||||
exec('nuwiki.table.align', pos_args())
|
exec('nuwiki.table.align', pos_args())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Upstream `gqq` (AlignQ) / `gww` (AlignW): on a table row, align the table;
|
||||||
|
-- otherwise fall back to the native Vim format command (`normal! gqq`/`gww`).
|
||||||
|
-- `normal!` bypasses our mapping so there's no recursion.
|
||||||
|
function M.table_align_or_cmd(cmd)
|
||||||
|
if is_table_row(vim.api.nvim_get_current_line()) then
|
||||||
|
M.table_align()
|
||||||
|
else
|
||||||
|
vim.cmd('normal! ' .. cmd)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function M.table_move_column_left()
|
function M.table_move_column_left()
|
||||||
local p = pos_args()[1]
|
local p = pos_args()[1]
|
||||||
p.dir = 'left'
|
p.dir = 'left'
|
||||||
@@ -951,36 +1186,61 @@ end
|
|||||||
--- the template is `<span style="color:%c">%s</span>` with `%c`
|
--- the template is `<span style="color:%c">%s</span>` with `%c`
|
||||||
--- replaced by the user-supplied colour and `%s` by the wrapped text.
|
--- replaced by the user-supplied colour and `%s` by the wrapped text.
|
||||||
--- Pure-Lua; no server roundtrip needed.
|
--- Pure-Lua; no server roundtrip needed.
|
||||||
|
-- Read the last visual selection from the `'<`/`'>` marks. Only meaningful
|
||||||
|
-- when the caller knows it's acting on a selection (colorize's `visual` flag);
|
||||||
|
-- we deliberately don't gate on vim.fn.visualmode() here since that reflects
|
||||||
|
-- session state, not whether the marks are the selection we want right now.
|
||||||
local function visual_selection_range()
|
local function visual_selection_range()
|
||||||
local mode = vim.fn.visualmode()
|
|
||||||
if mode == '' then return nil end
|
|
||||||
local s = vim.api.nvim_buf_get_mark(0, '<')
|
local s = vim.api.nvim_buf_get_mark(0, '<')
|
||||||
local e = vim.api.nvim_buf_get_mark(0, '>')
|
local e = vim.api.nvim_buf_get_mark(0, '>')
|
||||||
if s[1] == 0 or e[1] == 0 then return nil end
|
if s[1] == 0 or e[1] == 0 then return nil end
|
||||||
return s[1], s[2], e[1], e[2]
|
return s[1], s[2], e[1], e[2]
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.colorize(color)
|
-- colorize(color, visual): wrap text in an inline colour span.
|
||||||
|
--
|
||||||
|
-- `visual` MUST be passed only by the visual-mode mapping — it means "act on
|
||||||
|
-- the `'<`/`'>` selection". We can't infer it from `vim.fn.visualmode()`,
|
||||||
|
-- which returns the *last* visual mode used in the session (with stale marks),
|
||||||
|
-- so the normal-mode command / mapping would otherwise wrap whatever was last
|
||||||
|
-- selected instead of the word under the cursor.
|
||||||
|
function M.colorize(color, visual)
|
||||||
if not color or color == '' then
|
if not color or color == '' then
|
||||||
color = vim.fn.input('Colour: ')
|
color = vim.fn.input('Colour: ')
|
||||||
if color == '' then return end
|
if color == '' then return end
|
||||||
end
|
end
|
||||||
local sl, sc, el, ec = visual_selection_range()
|
|
||||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
local line = vim.api.nvim_get_current_line()
|
local line = vim.api.nvim_get_current_line()
|
||||||
local open_tag = string.format('<span style="color:%s">', color)
|
-- vimwiki `color_tag_template`: split on __CONTENT__ into open/close, with
|
||||||
local close_tag = '</span>'
|
-- __COLORFG__ replaced by the colour. Configurable via the setup option /
|
||||||
if sl and sl == el then
|
-- g:nuwiki_color_tag_template.
|
||||||
-- Single-line visual selection.
|
local tpl = (require('nuwiki.config').options or {}).color_tag_template
|
||||||
local new_line = line:sub(1, sc)
|
or vim.g.nuwiki_color_tag_template
|
||||||
.. open_tag
|
or '<span style="color:__COLORFG__">__CONTENT__</span>'
|
||||||
.. line:sub(sc + 1, ec + 1)
|
-- Function replacement returns the colour verbatim (a string replacement
|
||||||
.. close_tag
|
-- would treat `%` specially).
|
||||||
.. line:sub(ec + 2)
|
tpl = tpl:gsub('__COLORFG__', function() return color end)
|
||||||
vim.api.nvim_buf_set_lines(0, sl - 1, sl, false, { new_line })
|
local open_tag, close_tag = tpl:match('^(.*)__CONTENT__(.*)$')
|
||||||
return
|
if not open_tag then
|
||||||
|
open_tag, close_tag = string.format('<span style="color:%s">', color), '</span>'
|
||||||
end
|
end
|
||||||
-- Fall back to the cword.
|
if visual then
|
||||||
|
local sl, sc, el, ec = visual_selection_range()
|
||||||
|
if sl and sl == el then
|
||||||
|
-- Single-line visual selection.
|
||||||
|
local sline = vim.api.nvim_buf_get_lines(0, sl - 1, sl, false)[1] or ''
|
||||||
|
local new_line = sline:sub(1, sc)
|
||||||
|
.. open_tag
|
||||||
|
.. sline:sub(sc + 1, ec + 1)
|
||||||
|
.. close_tag
|
||||||
|
.. sline:sub(ec + 2)
|
||||||
|
vim.api.nvim_buf_set_lines(0, sl - 1, sl, false, { new_line })
|
||||||
|
vim.fn['nuwiki#colors#refresh']()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Multi-line or no usable selection: fall through to the cword.
|
||||||
|
end
|
||||||
|
-- Wrap the cword.
|
||||||
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]
|
||||||
@@ -1000,6 +1260,7 @@ function M.colorize(color)
|
|||||||
local after = line:sub(right)
|
local after = line:sub(right)
|
||||||
local new_line = before .. open_tag .. word .. close_tag .. after
|
local new_line = before .. open_tag .. word .. close_tag .. after
|
||||||
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
||||||
|
vim.fn['nuwiki#colors#refresh']()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Cluster C — link helpers.
|
-- Cluster C — link helpers.
|
||||||
@@ -1012,11 +1273,25 @@ function M.paste_url()
|
|||||||
exec('nuwiki.link.pasteUrl', pos_args())
|
exec('nuwiki.link.pasteUrl', pos_args())
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Vimwiki `:VimwikiNormalizeLink` — wrap the word at cursor as
|
-- Wrap the single-line `'<`/`'>` visual selection as `[[selection]]`.
|
||||||
--- `[[word]]` without following. The `<CR>` two-step does the same
|
local function wrap_visual_as_wikilink()
|
||||||
--- thing under the hood; expose it as a standalone command + keymap
|
local sl, sc, el, ec = visual_selection_range()
|
||||||
--- for users who want to bind it to `+` per vimwiki defaults.
|
if not (sl and sl == el) then return end
|
||||||
function M.normalize_link()
|
local line = vim.api.nvim_buf_get_lines(0, sl - 1, sl, false)[1] or ''
|
||||||
|
local sel = line:sub(sc + 1, ec + 1)
|
||||||
|
if sel == '' or sel:match('^%[%[.*%]%]$') then return end
|
||||||
|
local new = line:sub(1, sc) .. '[[' .. sel .. ']]' .. line:sub(ec + 2)
|
||||||
|
vim.api.nvim_buf_set_lines(0, sl - 1, sl, false, { new })
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Vimwiki `:VimwikiNormalizeLink [1]` — wrap text as `[[…]]` without
|
||||||
|
--- following. With `visual` (upstream's `1` arg / the `x`-mode `+` mapping)
|
||||||
|
--- wrap the selection; otherwise the word at the cursor.
|
||||||
|
function M.normalize_link(visual)
|
||||||
|
if visual then
|
||||||
|
wrap_visual_as_wikilink()
|
||||||
|
return
|
||||||
|
end
|
||||||
if cursor_inside_wikilink() then return end
|
if cursor_inside_wikilink() then return end
|
||||||
wrap_cword_as_wikilink()
|
wrap_cword_as_wikilink()
|
||||||
end
|
end
|
||||||
|
|||||||
+177
-17
@@ -13,6 +13,11 @@ M.defaults = {
|
|||||||
syntax = 'vimwiki',
|
syntax = 'vimwiki',
|
||||||
log_level = 'warn',
|
log_level = 'warn',
|
||||||
|
|
||||||
|
-- Prefix for the global/buffer wiki command family (`<prefix>w`,
|
||||||
|
-- `<prefix>t`, `<prefix><Leader>w`, …). Mirrors vimwiki's
|
||||||
|
-- `g:vimwiki_map_prefix`. Vim users set `g:nuwiki_map_prefix` instead.
|
||||||
|
map_prefix = '<Leader>w',
|
||||||
|
|
||||||
-- Multi-wiki shape. Each entry honours every per-wiki key the
|
-- Multi-wiki shape. Each entry honours every per-wiki key the
|
||||||
-- server understands. Leave nil to fall through to the shorthand
|
-- server understands. Leave nil to fall through to the shorthand
|
||||||
-- above.
|
-- above.
|
||||||
@@ -21,28 +26,43 @@ M.defaults = {
|
|||||||
-- `doc/nuwiki.txt`):
|
-- `doc/nuwiki.txt`):
|
||||||
-- name, root, file_extension, syntax, index
|
-- name, root, file_extension, syntax, index
|
||||||
-- diary_rel_path, diary_index, diary_frequency,
|
-- diary_rel_path, diary_index, diary_frequency,
|
||||||
|
-- diary_weekly_style, diary_start_week_day,
|
||||||
-- diary_caption_level, diary_sort, diary_header
|
-- diary_caption_level, diary_sort, diary_header
|
||||||
-- html_path, template_path, template_default, template_ext,
|
-- html_path, template_path, template_default, template_ext,
|
||||||
-- template_date_format, css_name, auto_export, auto_toc,
|
-- template_date_format, css_name, auto_export, auto_toc,
|
||||||
-- html_filename_parameterization, exclude_files, color_dic
|
-- auto_generate_links, auto_generate_tags, auto_diary_index,
|
||||||
-- listsyms, listsyms_propagate, list_margin,
|
-- html_filename_parameterization, exclude_files, color_dic,
|
||||||
|
-- custom_wiki2html, custom_wiki2html_args, base_url,
|
||||||
|
-- toc_header, toc_header_level, links_header, links_header_level,
|
||||||
|
-- tags_header, tags_header_level,
|
||||||
|
-- html_header_numbering, html_header_numbering_sym,
|
||||||
|
-- listsyms, listsym_rejected, listsyms_propagate, list_margin,
|
||||||
-- links_space_char
|
-- links_space_char
|
||||||
wikis = nil,
|
wikis = nil,
|
||||||
|
|
||||||
|
-- TOC defaults (per-wiki, also accepted as top-level for single-wiki setups)
|
||||||
|
toc_header = 'Contents',
|
||||||
|
toc_header_level = 1,
|
||||||
|
|
||||||
|
-- HTML export defaults (per-wiki, also accepted as top-level for single-wiki setups)
|
||||||
|
html_header_numbering = 0,
|
||||||
|
html_header_numbering_sym = '',
|
||||||
|
|
||||||
-- Per-buffer glue. Each subgroup mirrors vimwiki's
|
-- Per-buffer glue. Each subgroup mirrors vimwiki's
|
||||||
-- `g:vimwiki_key_mappings` shape and can be flipped off
|
-- `g:vimwiki_key_mappings` shape and can be flipped off
|
||||||
-- independently to suppress that group of keymaps.
|
-- independently to suppress that group of keymaps.
|
||||||
|
-- The <Leader>w prefix in wiki_prefix/links/html_export follows map_prefix.
|
||||||
mappings = {
|
mappings = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
wiki_prefix = true, -- <Leader>w*
|
wiki_prefix = true, -- <Leader>w{w,t,s,i}, <Leader>w<Leader>{w,y,t,m,i}
|
||||||
links = true, -- <CR>, <S-CR>, <Tab>, <BS>, +, …
|
links = true, -- <CR>, <S-CR>, <C-CR>, <C-S-CR>, <D-CR>, <M-CR>, <BS>, <Tab>, <S-Tab>, + (n/x), <Leader>w{n,d,r}, <Leader>wc (n/x)
|
||||||
lists = true, -- <C-Space>, gln/glp/glx, gl<Space>, o/O, …
|
lists = true, -- <C-Space>, gnt, gln, glp, glx, glh, gll, gLh, gLl, glr, gLr, gl{-,*,#,1,i,I,a,A}, gL{…}, gl, gL, o, O; insert <C-D>, <C-T>, <C-L><C-J/K/M>, <CR>
|
||||||
headers = true, -- =, -, ]], [[, ]=, [=, ]u, [u
|
headers = true, -- =, -, ]], [[, ]=, [=, ]u, [u
|
||||||
table_editing = true, -- gqq, gq1, gww, gw1, <A-Left>, <A-Right>
|
table_editing = true, -- gqq, gq1, gww, gw1, <A-Left>, <A-Right>; insert <Tab>, <S-Tab>
|
||||||
diary = true, -- <C-Down>, <C-Up>
|
diary = true, -- <C-Down>, <C-Up>
|
||||||
html_export = true, -- <Leader>wh, <Leader>whh
|
html_export = true, -- <Leader>wh, <Leader>whh, <Leader>wha
|
||||||
text_objects = true, -- ah, ih, aH, iH, al, il, a\, i\, ac, ic
|
text_objects = true, -- ah, ih, aH, iH, al, il, a\, i\, ac, ic
|
||||||
mouse = false, -- <2-LeftMouse>, <RightMouse>, … (opt-in)
|
mouse = false, -- <2-LeftMouse>, <S-2-LeftMouse>, <C-2-LeftMouse>, <MiddleMouse>, <RightMouse> (opt-in)
|
||||||
},
|
},
|
||||||
|
|
||||||
-- Folding. `'lsp'` uses the server's `foldingRange`
|
-- Folding. `'lsp'` uses the server's `foldingRange`
|
||||||
@@ -50,6 +70,23 @@ M.defaults = {
|
|||||||
-- back to a regex-based `foldexpr`. `'off'` skips folding setup.
|
-- back to a regex-based `foldexpr`. `'off'` skips folding setup.
|
||||||
folding = 'lsp',
|
folding = 'lsp',
|
||||||
|
|
||||||
|
-- vimwiki `autowriteall` (default on): while a wiki buffer is current,
|
||||||
|
-- mirror Vim's global `'autowriteall'` so a modified buffer auto-saves on
|
||||||
|
-- page switch / `:make` etc., restoring the prior value on leave.
|
||||||
|
autowriteall = true,
|
||||||
|
|
||||||
|
-- vimwiki `table_auto_fmt` (default on): re-align the table under the
|
||||||
|
-- cursor when leaving insert mode.
|
||||||
|
table_auto_fmt = true,
|
||||||
|
|
||||||
|
-- vimwiki `auto_chdir` (default off): `:lcd` into the owning wiki's root
|
||||||
|
-- when a wiki buffer becomes current.
|
||||||
|
auto_chdir = false,
|
||||||
|
|
||||||
|
-- vimwiki `auto_header` (default off): insert a level-1 header from the
|
||||||
|
-- filename on new wiki pages.
|
||||||
|
auto_header = false,
|
||||||
|
|
||||||
-- Broken-link diagnostic severity. Sent to the server, which downgrades
|
-- Broken-link diagnostic severity. Sent to the server, which downgrades
|
||||||
-- or suppresses the diagnostic accordingly.
|
-- or suppresses the diagnostic accordingly.
|
||||||
-- 'off' | 'hint' | 'warn' | 'error'
|
-- 'off' | 'hint' | 'warn' | 'error'
|
||||||
@@ -66,19 +103,126 @@ M.defaults = {
|
|||||||
|
|
||||||
M.options = vim.deepcopy(M.defaults)
|
M.options = vim.deepcopy(M.defaults)
|
||||||
|
|
||||||
|
-- The raw table the user passed to setup(), kept separate from the
|
||||||
|
-- defaults-merged `options`. Used to tell an explicitly-set value apart from
|
||||||
|
-- a built-in default — e.g. so the LSP client only treats a per-wiki key as a
|
||||||
|
-- global default when the user actually set it.
|
||||||
|
M.user = {}
|
||||||
|
|
||||||
function M.apply(user)
|
function M.apply(user)
|
||||||
|
M.user = user or {}
|
||||||
M.options = vim.tbl_deep_extend('force', M.defaults, user or {})
|
M.options = vim.tbl_deep_extend('force', M.defaults, user or {})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ===== Upstream vimwiki config compatibility (drop-in) =====
|
||||||
|
--
|
||||||
|
-- Single source of truth for the configured wikis, shared by the LSP payload
|
||||||
|
-- (lsp.lua) and the buffer-side commands. Mirrors autoload/nuwiki/config.vim so
|
||||||
|
-- a user who keeps their original vim.g.vimwiki_list + g:vimwiki_* config (e.g.
|
||||||
|
-- swapping the vimwiki plugin for nuwiki under lazy.nvim) needs no rewrite.
|
||||||
|
-- Native config (setup() wikis / g:nuwiki_*) always wins.
|
||||||
|
|
||||||
|
-- Upstream vimwiki per-wiki dict key -> nuwiki/server key.
|
||||||
|
local VIMWIKI_WIKI_MAP = {
|
||||||
|
path = 'root',
|
||||||
|
path_html = 'html_path',
|
||||||
|
template_path = 'template_path',
|
||||||
|
template_default = 'template_default',
|
||||||
|
template_ext = 'template_ext',
|
||||||
|
css_name = 'css_name',
|
||||||
|
auto_export = 'auto_export',
|
||||||
|
auto_toc = 'auto_toc',
|
||||||
|
syntax = 'syntax',
|
||||||
|
ext = 'file_extension',
|
||||||
|
index = 'index',
|
||||||
|
diary_rel_path = 'diary_rel_path',
|
||||||
|
diary_index = 'diary_index',
|
||||||
|
name = 'name',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Per-wiki settings that double as wiki-wide global defaults.
|
||||||
|
local SCALAR_GLOBAL_KEYS = {
|
||||||
|
'toc_header', 'toc_header_level', 'toc_link_format',
|
||||||
|
'links_header', 'links_header_level',
|
||||||
|
'tags_header', 'tags_header_level',
|
||||||
|
'html_header_numbering', 'html_header_numbering_sym',
|
||||||
|
'links_space_char', 'list_margin',
|
||||||
|
'listsyms', 'listsym_rejected',
|
||||||
|
'auto_export', 'auto_toc',
|
||||||
|
'auto_generate_links', 'auto_generate_tags', 'auto_diary_index',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Resolve the global scalar defaults applied to every wiki. Priority (low to
|
||||||
|
-- high): g:vimwiki_<key>, g:nuwiki_<key>, explicit setup() value. Built-in
|
||||||
|
-- defaults are excluded (reads M.user, not M.options) so they don't get folded
|
||||||
|
-- onto every wiki and override per-wiki values.
|
||||||
|
function M.scalar_globals()
|
||||||
|
local g = {}
|
||||||
|
for _, key in ipairs(SCALAR_GLOBAL_KEYS) do
|
||||||
|
if vim.g['vimwiki_' .. key] ~= nil then
|
||||||
|
g[key] = vim.g['vimwiki_' .. key]
|
||||||
|
end
|
||||||
|
if vim.g['nuwiki_' .. key] ~= nil then
|
||||||
|
g[key] = vim.g['nuwiki_' .. key]
|
||||||
|
end
|
||||||
|
if M.user[key] ~= nil then
|
||||||
|
g[key] = M.user[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return g
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Translate vim.g.vimwiki_list into nuwiki's wiki list (server key names),
|
||||||
|
-- folding the global scalars into each entry. Returns {} when unset/malformed.
|
||||||
|
local function wikis_from_vimwiki(globals)
|
||||||
|
local list = vim.g.vimwiki_list
|
||||||
|
if type(list) ~= 'table' or vim.tbl_isempty(list) then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
for _, vw in ipairs(list) do
|
||||||
|
if type(vw) == 'table' then
|
||||||
|
local w = vim.tbl_extend('force', {}, globals)
|
||||||
|
for src, dst in pairs(VIMWIKI_WIKI_MAP) do
|
||||||
|
if vw[src] ~= nil then
|
||||||
|
w[dst] = vw[src]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if w.root ~= nil then
|
||||||
|
table.insert(out, w)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The configured wikis as normalized dicts (nuwiki/server key names), with the
|
||||||
|
-- global scalars folded in (per-wiki value wins). Priority:
|
||||||
|
-- 1. setup() opts.wikis
|
||||||
|
-- 2. g:nuwiki_wikis
|
||||||
|
-- 3. g:vimwiki_list (upstream drop-in)
|
||||||
|
-- Returns {} when none is set — callers fall back to the single-wiki shorthand.
|
||||||
|
function M.wikis()
|
||||||
|
local globals = M.scalar_globals()
|
||||||
|
local native = M.options.wikis or vim.g.nuwiki_wikis
|
||||||
|
if type(native) == 'table' and #native > 0 then
|
||||||
|
local out = {}
|
||||||
|
for i, w in ipairs(native) do
|
||||||
|
out[i] = vim.tbl_extend('force', globals, w)
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
return wikis_from_vimwiki(globals)
|
||||||
|
end
|
||||||
|
|
||||||
-- Resolve the config for wiki #n (0-based) without touching the LSP.
|
-- Resolve the config for wiki #n (0-based) without touching the LSP.
|
||||||
-- Priority: setup() opts.wikis → g:nuwiki_wikis (VimL) → scalar opts/g: vars.
|
-- Returns { name, root, ext, idx, diary_rel, diary_idx, space }.
|
||||||
-- Returns { name, root, ext, idx, diary_rel, diary_idx }.
|
|
||||||
function M.wiki_cfg(n)
|
function M.wiki_cfg(n)
|
||||||
local opts = M.options
|
local wikis = M.wikis()
|
||||||
local wikis = opts.wikis or vim.g.nuwiki_wikis or nil
|
local w = wikis[(n or 0) + 1]
|
||||||
local w = wikis and wikis[(n or 0) + 1] or nil
|
local root = (w and w.root) or M.options.wiki_root or vim.g.nuwiki_wiki_root or '~/vimwiki'
|
||||||
local root = (w and w.root) or opts.wiki_root or vim.g.nuwiki_wiki_root or '~/vimwiki'
|
local ext = (w and w.file_extension) or M.options.file_extension
|
||||||
local ext = (w and w.file_extension) or opts.file_extension or vim.g.nuwiki_file_extension or '.wiki'
|
or vim.g.nuwiki_file_extension or '.wiki'
|
||||||
if not ext:match('^%.') then ext = '.' .. ext end
|
if not ext:match('^%.') then ext = '.' .. ext end
|
||||||
return {
|
return {
|
||||||
name = (w and w.name) or vim.fn.fnamemodify(vim.fn.expand(root), ':t'),
|
name = (w and w.name) or vim.fn.fnamemodify(vim.fn.expand(root), ':t'),
|
||||||
@@ -87,15 +231,17 @@ function M.wiki_cfg(n)
|
|||||||
idx = (w and w.index) or 'index',
|
idx = (w and w.index) or 'index',
|
||||||
diary_rel = (w and w.diary_rel_path) or vim.g.nuwiki_diary_rel_path or 'diary',
|
diary_rel = (w and w.diary_rel_path) or vim.g.nuwiki_diary_rel_path or 'diary',
|
||||||
diary_idx = (w and w.diary_index) or 'diary',
|
diary_idx = (w and w.diary_index) or 'diary',
|
||||||
|
space = (w and w.links_space_char) or M.options.links_space_char
|
||||||
|
or vim.g.nuwiki_links_space_char or ' ',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The full list of configured wikis as wiki_cfg dicts. Falls back to a single
|
-- The full list of configured wikis as wiki_cfg dicts. Falls back to a single
|
||||||
-- entry built from the scalar wiki_root config when no list is configured.
|
-- entry built from the scalar wiki_root config when no list is configured.
|
||||||
function M.wiki_list()
|
function M.wiki_list()
|
||||||
local wikis = M.options.wikis or vim.g.nuwiki_wikis or nil
|
local wikis = M.wikis()
|
||||||
local list = {}
|
local list = {}
|
||||||
if type(wikis) == 'table' and #wikis > 0 then
|
if #wikis > 0 then
|
||||||
for i = 1, #wikis do
|
for i = 1, #wikis do
|
||||||
list[i] = M.wiki_cfg(i - 1)
|
list[i] = M.wiki_cfg(i - 1)
|
||||||
end
|
end
|
||||||
@@ -105,4 +251,18 @@ function M.wiki_list()
|
|||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Absolute root (trailing slash) of the wiki that owns `path`, or nil when the
|
||||||
|
-- file is under no configured wiki (callers no-op / fall back to CWD).
|
||||||
|
-- Used by auto_chdir + search scoping.
|
||||||
|
function M.wiki_root_for(path)
|
||||||
|
for _, w in ipairs(M.wiki_list()) do
|
||||||
|
local wroot = vim.fn.fnamemodify(vim.fn.expand(w.root), ':p')
|
||||||
|
if wroot:sub(-1) ~= '/' then wroot = wroot .. '/' end
|
||||||
|
if path:sub(1, #wroot) == wroot then
|
||||||
|
return wroot
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -46,6 +46,79 @@ local function setup_folding(bufnr, folding_mode)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- vimwiki `autowriteall`: while this wiki buffer is current, mirror the option
|
||||||
|
-- into Vim's global 'autowriteall' (auto-save on switch/make), restoring the
|
||||||
|
-- prior value on leave.
|
||||||
|
local function setup_autowriteall(bufnr, enabled)
|
||||||
|
if not enabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if bufnr == 0 then
|
||||||
|
bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
end
|
||||||
|
local grp = vim.api.nvim_create_augroup('nuwiki_awa_' .. bufnr, { clear = true })
|
||||||
|
local function enter()
|
||||||
|
vim.b[bufnr].nuwiki_awa_saved = vim.o.autowriteall
|
||||||
|
vim.o.autowriteall = true
|
||||||
|
end
|
||||||
|
local function leave()
|
||||||
|
local saved = vim.b[bufnr].nuwiki_awa_saved
|
||||||
|
if saved ~= nil then
|
||||||
|
vim.o.autowriteall = saved
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.api.nvim_create_autocmd('BufEnter', { buffer = bufnr, group = grp, callback = enter })
|
||||||
|
vim.api.nvim_create_autocmd('BufLeave', { buffer = bufnr, group = grp, callback = leave })
|
||||||
|
-- FileType fires after the initial BufEnter, so apply once now too.
|
||||||
|
enter()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vimwiki `table_auto_fmt`: re-align the table under the cursor on InsertLeave.
|
||||||
|
local function setup_table_auto_fmt(bufnr, enabled)
|
||||||
|
if not enabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if bufnr == 0 then
|
||||||
|
bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
end
|
||||||
|
local grp = vim.api.nvim_create_augroup('nuwiki_taf_' .. bufnr, { clear = true })
|
||||||
|
vim.api.nvim_create_autocmd('InsertLeave', {
|
||||||
|
buffer = bufnr,
|
||||||
|
group = grp,
|
||||||
|
callback = function()
|
||||||
|
-- Cheap guard: only round-trip to the server on a table row.
|
||||||
|
if vim.api.nvim_get_current_line():find('|', 1, true) then
|
||||||
|
require('nuwiki.commands').table_align()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vimwiki `auto_chdir`: `:lcd` into the owning wiki's root while this buffer
|
||||||
|
-- is current.
|
||||||
|
local function setup_auto_chdir(bufnr, enabled)
|
||||||
|
if not enabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if bufnr == 0 then
|
||||||
|
bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
end
|
||||||
|
local function chdir()
|
||||||
|
local file = vim.api.nvim_buf_get_name(bufnr)
|
||||||
|
local root = require('nuwiki.config').wiki_root_for(file)
|
||||||
|
if root then
|
||||||
|
vim.cmd('lcd ' .. vim.fn.fnameescape(root))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local grp = vim.api.nvim_create_augroup('nuwiki_chdir_' .. bufnr, { clear = true })
|
||||||
|
vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWinEnter' }, {
|
||||||
|
buffer = bufnr,
|
||||||
|
group = grp,
|
||||||
|
callback = chdir,
|
||||||
|
})
|
||||||
|
chdir()
|
||||||
|
end
|
||||||
|
|
||||||
function M.attach(bufnr)
|
function M.attach(bufnr)
|
||||||
bufnr = bufnr or 0
|
bufnr = bufnr or 0
|
||||||
local config = require('nuwiki.config')
|
local config = require('nuwiki.config')
|
||||||
@@ -59,6 +132,9 @@ function M.attach(bufnr)
|
|||||||
end
|
end
|
||||||
|
|
||||||
setup_folding(bufnr, opts.folding or 'lsp')
|
setup_folding(bufnr, opts.folding or 'lsp')
|
||||||
|
setup_autowriteall(bufnr, opts.autowriteall ~= false)
|
||||||
|
setup_table_auto_fmt(bufnr, opts.table_auto_fmt ~= false)
|
||||||
|
setup_auto_chdir(bufnr, opts.auto_chdir == true)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+56
-8
@@ -48,15 +48,18 @@ local function _setup_global_mappings()
|
|||||||
if vim.g.nuwiki_no_default_mappings then return end
|
if vim.g.nuwiki_no_default_mappings then return end
|
||||||
local kmap = vim.keymap.set
|
local kmap = vim.keymap.set
|
||||||
local function o(d) return { silent = true, desc = 'nuwiki: ' .. d } end
|
local function o(d) return { silent = true, desc = 'nuwiki: ' .. d } end
|
||||||
|
-- Same configurable prefix as the buffer-local maps (vimwiki map_prefix).
|
||||||
|
local p = config.options.map_prefix or '<Leader>w'
|
||||||
|
|
||||||
kmap('n', '<Leader>ww', function() _open(_wiki_index_path(0)) end, o('wiki index'))
|
kmap('n', p .. 'w', function() _open(_wiki_index_path(0)) end, o('wiki index'))
|
||||||
kmap('n', '<Leader>wt', function() _open(_wiki_index_path(0), 'tabedit') end, o('wiki index (tab)'))
|
kmap('n', p .. 't', function() _open(_wiki_index_path(0), 'tabedit') end, o('wiki index (tab)'))
|
||||||
kmap('n', '<Leader>ws', function() require('nuwiki.commands').wiki_ui_select() end, o('pick wiki'))
|
kmap('n', p .. 's', function() require('nuwiki.commands').wiki_ui_select() end, o('pick wiki'))
|
||||||
kmap('n', '<Leader>wi', function() _open(_diary_path(nil)) end, o('diary index'))
|
kmap('n', p .. 'i', function() _open(_diary_path(nil)) end, o('diary index'))
|
||||||
kmap('n', '<Leader>w<Leader>w', function() _open(_diary_path(0)) end, o('today diary'))
|
kmap('n', p .. '<Leader>w', function() _open(_diary_path(0)) end, o('today diary'))
|
||||||
kmap('n', '<Leader>w<Leader>y', function() _open(_diary_path(-1)) end, o('yesterday diary'))
|
kmap('n', p .. '<Leader>y', function() _open(_diary_path(-1)) end, o('yesterday diary'))
|
||||||
kmap('n', '<Leader>w<Leader>t', function() _open(_diary_path(1)) end, o('tomorrow diary'))
|
kmap('n', p .. '<Leader>t', function() _open(_diary_path(0), 'tabedit') end, o('today diary (tab)'))
|
||||||
kmap('n', '<Leader>w<Leader>i', function() require('nuwiki.commands').diary_generate_index() end, o('rebuild diary index'))
|
kmap('n', p .. '<Leader>m', function() _open(_diary_path(1)) end, o('tomorrow diary'))
|
||||||
|
kmap('n', p .. '<Leader>i', function() require('nuwiki.commands').diary_generate_index() end, o('rebuild diary index'))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Global wiki-picker commands so a wiki can be chosen from any buffer before
|
-- Global wiki-picker commands so a wiki can be chosen from any buffer before
|
||||||
@@ -67,11 +70,40 @@ local function _setup_global_commands()
|
|||||||
local function ui_select() require('nuwiki.commands').wiki_ui_select() end
|
local function ui_select() require('nuwiki.commands').wiki_ui_select() end
|
||||||
vim.api.nvim_create_user_command('VimwikiUISelect', ui_select, {})
|
vim.api.nvim_create_user_command('VimwikiUISelect', ui_select, {})
|
||||||
vim.api.nvim_create_user_command('NuwikiUISelect', ui_select, {})
|
vim.api.nvim_create_user_command('NuwikiUISelect', ui_select, {})
|
||||||
|
|
||||||
|
local function show_version() require('nuwiki.commands').show_version() end
|
||||||
|
vim.api.nvim_create_user_command('VimwikiShowVersion', show_version, {})
|
||||||
|
vim.api.nvim_create_user_command('NuwikiShowVersion', show_version, {})
|
||||||
|
|
||||||
|
-- Global Index / TabIndex entry points (upstream defines the Index family
|
||||||
|
-- globally). The buffer-local copies shadow these inside wiki buffers.
|
||||||
|
local function index(o) require('nuwiki.commands').wiki_index(o.count) end
|
||||||
|
local function tab_index(o) require('nuwiki.commands').wiki_tab_index(o.count) end
|
||||||
|
vim.api.nvim_create_user_command('VimwikiIndex', index, { count = 0 })
|
||||||
|
vim.api.nvim_create_user_command('NuwikiIndex', index, { count = 0 })
|
||||||
|
vim.api.nvim_create_user_command('VimwikiTabIndex', tab_index, { count = 0 })
|
||||||
|
vim.api.nvim_create_user_command('NuwikiTabIndex', tab_index, { count = 0 })
|
||||||
|
|
||||||
|
-- Get/set nuwiki client config (upstream's :VimwikiVar).
|
||||||
|
local function var(o) require('nuwiki.commands').var(o.args) end
|
||||||
|
vim.api.nvim_create_user_command('VimwikiVar', var, { nargs = '*' })
|
||||||
|
vim.api.nvim_create_user_command('NuwikiVar', var, { nargs = '*' })
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.setup(opts)
|
function M.setup(opts)
|
||||||
config.apply(opts or {})
|
config.apply(opts or {})
|
||||||
|
|
||||||
|
-- Bridge a Lua-only `setup({ wikis = … })` config to VimL. The buffer-side
|
||||||
|
-- VimL helpers (autoload/nuwiki/config.vim) and the vimwiki compat shim
|
||||||
|
-- (autoload/vimwiki/vars.vim — used by third-party plugins such as
|
||||||
|
-- vimwiki-sync) read `g:nuwiki_wikis`, which can't see a config that only
|
||||||
|
-- lives in Lua. Publishing the resolved list keeps both worlds in sync. (A
|
||||||
|
-- `g:nuwiki_wikis` / `g:vimwiki_list` config is already VimL-visible, so this
|
||||||
|
-- only fires for the pure-`setup()` case.)
|
||||||
|
if type(config.options.wikis) == 'table' and #config.options.wikis > 0 then
|
||||||
|
vim.g.nuwiki_wikis = config.wikis()
|
||||||
|
end
|
||||||
|
|
||||||
-- Neovim 0.7+ resolves filetypes via `vim.filetype.match` before any
|
-- Neovim 0.7+ resolves filetypes via `vim.filetype.match` before any
|
||||||
-- `ftdetect/*.vim` autocmd fires, and its bundled rule for `*.wiki`
|
-- `ftdetect/*.vim` autocmd fires, and its bundled rule for `*.wiki`
|
||||||
-- returns `mediawiki`. Register an authoritative rule here so this
|
-- returns `mediawiki`. Register an authoritative rule here so this
|
||||||
@@ -92,6 +124,22 @@ function M.setup(opts)
|
|||||||
vim.g.nuwiki_no_calendar = 1
|
vim.g.nuwiki_no_calendar = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- vimwiki `auto_header` (default off): insert a level-1 header from the
|
||||||
|
-- filename on new wiki pages. BufNewFile (not FileType) so it only fires for
|
||||||
|
-- genuinely new files.
|
||||||
|
if config.options.auto_header == true then
|
||||||
|
local ext = (config.options.file_extension or '.wiki'):gsub('^%.', '')
|
||||||
|
local pats = { '*.wiki' }
|
||||||
|
if ext ~= 'wiki' and ext ~= '' then
|
||||||
|
pats[#pats + 1] = '*.' .. ext
|
||||||
|
end
|
||||||
|
vim.api.nvim_create_autocmd('BufNewFile', {
|
||||||
|
pattern = pats,
|
||||||
|
group = vim.api.nvim_create_augroup('nuwiki_auto_header', { clear = true }),
|
||||||
|
callback = function(a) require('nuwiki.commands').auto_header(a.buf) end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
lsp.register()
|
lsp.register()
|
||||||
_setup_global_mappings()
|
_setup_global_mappings()
|
||||||
_setup_global_commands()
|
_setup_global_commands()
|
||||||
|
|||||||
@@ -74,8 +74,11 @@ local function download_release(dest)
|
|||||||
if vim.fn.executable('curl') ~= 1 or vim.fn.executable('tar') ~= 1 then
|
if vim.fn.executable('curl') ~= 1 or vim.fn.executable('tar') ~= 1 then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
-- Gitea serves the most recent release under the `latest` tag segment
|
||||||
|
-- (`/releases/download/latest/<asset>`). Note this is NOT GitHub's
|
||||||
|
-- `/releases/latest/download/<asset>` shape, which Gitea 404s.
|
||||||
local url = string.format(
|
local url = string.format(
|
||||||
'https://code.gfran.co/gffranco/nuwiki/releases/latest/download/nuwiki-ls-%s.tar.gz',
|
'https://code.gfran.co/gffranco/nuwiki/releases/download/latest/nuwiki-ls-%s.tar.gz',
|
||||||
target
|
target
|
||||||
)
|
)
|
||||||
local archive = vim.fn.tempname() .. '.tar.gz'
|
local archive = vim.fn.tempname() .. '.tar.gz'
|
||||||
|
|||||||
+46
-23
@@ -143,17 +143,22 @@ function M.attach(bufnr, mappings)
|
|||||||
return mappings[group] ~= false
|
return mappings[group] ~= false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== Wiki prefix (`<Leader>w*`) =====
|
-- Configurable wiki command prefix (vimwiki's `g:vimwiki_map_prefix`).
|
||||||
|
-- Every `<prefix>…` mapping below is built from this so users can relocate
|
||||||
|
-- the whole family by setting `map_prefix` in setup().
|
||||||
|
local p = require('nuwiki.config').options.map_prefix or '<Leader>w'
|
||||||
|
|
||||||
|
-- ===== Wiki prefix (`<prefix>*`, default `<Leader>w*`) =====
|
||||||
if on('wiki_prefix') then
|
if on('wiki_prefix') then
|
||||||
map('n', '<Leader>ww', function() cmd.wiki_index(0) end, { desc = 'nuwiki: open wiki index' }, bufnr)
|
map('n', p .. 'w', function() cmd.wiki_index(0) end, { desc = 'nuwiki: open wiki index' }, bufnr)
|
||||||
map('n', '<Leader>wt', function() cmd.wiki_tab_index(0) end, { desc = 'nuwiki: open wiki index (tab)' }, bufnr)
|
map('n', p .. 't', function() cmd.wiki_tab_index(0) end, { desc = 'nuwiki: open wiki index (tab)' }, bufnr)
|
||||||
map('n', '<Leader>ws', cmd.wiki_ui_select, { desc = 'nuwiki: pick wiki' }, bufnr)
|
map('n', p .. 's', cmd.wiki_ui_select, { desc = 'nuwiki: pick wiki' }, bufnr)
|
||||||
map('n', '<Leader>wi', cmd.diary_index, { desc = 'nuwiki: open diary index' }, bufnr)
|
map('n', p .. 'i', cmd.diary_index, { desc = 'nuwiki: open diary index' }, bufnr)
|
||||||
map('n', '<Leader>w<Leader>w', cmd.diary_today, { desc = 'nuwiki: today' }, bufnr)
|
map('n', p .. '<Leader>w', cmd.diary_today, { desc = 'nuwiki: today' }, bufnr)
|
||||||
map('n', '<Leader>w<Leader>y', cmd.diary_yesterday, { desc = 'nuwiki: yesterday' }, bufnr)
|
map('n', p .. '<Leader>y', cmd.diary_yesterday, { desc = 'nuwiki: yesterday' }, bufnr)
|
||||||
map('n', '<Leader>w<Leader>t', cmd.diary_tomorrow, { desc = 'nuwiki: tomorrow' }, bufnr)
|
map('n', p .. '<Leader>t', cmd.diary_today_tab, { desc = 'nuwiki: today in a new tab' }, bufnr)
|
||||||
map('n', '<Leader>w<Leader>m', cmd.diary_tomorrow, { desc = 'nuwiki: tomorrow (vimwiki <Leader>w<Leader>m)' }, bufnr)
|
map('n', p .. '<Leader>m', cmd.diary_tomorrow, { desc = 'nuwiki: tomorrow (vimwiki <prefix><Leader>m)' }, bufnr)
|
||||||
map('n', '<Leader>w<Leader>i', cmd.diary_generate_index, { desc = 'nuwiki: rebuild diary index' }, bufnr)
|
map('n', p .. '<Leader>i', cmd.diary_generate_index, { desc = 'nuwiki: rebuild diary index' }, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== Links =====
|
-- ===== Links =====
|
||||||
@@ -166,17 +171,25 @@ function M.attach(bufnr, mappings)
|
|||||||
{ desc = 'nuwiki: follow link in vsplit' }, bufnr)
|
{ desc = 'nuwiki: follow link in vsplit' }, bufnr)
|
||||||
map('n', '<C-S-CR>', cmd.follow_link_drop,
|
map('n', '<C-S-CR>', cmd.follow_link_drop,
|
||||||
{ desc = 'nuwiki: follow link in tab (reuse existing)' }, bufnr)
|
{ desc = 'nuwiki: follow link in tab (reuse existing)' }, bufnr)
|
||||||
|
map('n', '<D-CR>', cmd.follow_link_drop,
|
||||||
|
{ desc = 'nuwiki: follow link in tab (macOS Cmd alias of <C-S-CR>)' }, bufnr)
|
||||||
|
map('n', '<M-CR>', cmd.badd_link,
|
||||||
|
{ desc = 'nuwiki: add link target to buffer list' }, bufnr)
|
||||||
map('n', '<BS>', '<C-o>', { desc = 'nuwiki: go back', remap = true }, bufnr)
|
map('n', '<BS>', '<C-o>', { desc = 'nuwiki: go back', remap = true }, bufnr)
|
||||||
map('n', '<Tab>', link.next, { desc = 'nuwiki: next wikilink' }, bufnr)
|
map('n', '<Tab>', link.next, { desc = 'nuwiki: next wikilink' }, bufnr)
|
||||||
map('n', '<S-Tab>', link.prev, { desc = 'nuwiki: prev wikilink' }, bufnr)
|
map('n', '<S-Tab>', link.prev, { desc = 'nuwiki: prev wikilink' }, bufnr)
|
||||||
map('n', '+', cmd.normalize_link, { desc = 'nuwiki: wrap word as wikilink' }, bufnr)
|
map('n', '+', cmd.normalize_link, { desc = 'nuwiki: wrap word as wikilink' }, bufnr)
|
||||||
map('x', '+', cmd.normalize_link, { desc = 'nuwiki: wrap word as wikilink' }, bufnr)
|
map('x', '+', function() cmd.normalize_link(true) end,
|
||||||
map('n', '<Leader>wn', cmd.wiki_goto_page, { desc = 'nuwiki: goto page' }, bufnr)
|
{ desc = 'nuwiki: wrap selection as wikilink' }, bufnr)
|
||||||
map('n', '<Leader>wd', cmd.delete_file, { desc = 'nuwiki: delete page' }, bufnr)
|
-- Upstream binds visual <CR> to the same normalize-link as visual `+`.
|
||||||
map('n', '<Leader>wr', cmd.rename_file, { desc = 'nuwiki: rename page' }, bufnr)
|
map('x', '<CR>', function() cmd.normalize_link(true) end,
|
||||||
map('n', '<Leader>wc', function() cmd.colorize() end,
|
{ desc = 'nuwiki: wrap selection as wikilink' }, bufnr)
|
||||||
|
map('n', p .. 'n', cmd.wiki_goto_page, { desc = 'nuwiki: goto page' }, bufnr)
|
||||||
|
map('n', p .. 'd', cmd.delete_file, { desc = 'nuwiki: delete page' }, bufnr)
|
||||||
|
map('n', p .. 'r', cmd.rename_file, { desc = 'nuwiki: rename page' }, bufnr)
|
||||||
|
map('n', p .. 'c', function() cmd.colorize() end,
|
||||||
{ desc = 'nuwiki: wrap word in colour span' }, bufnr)
|
{ desc = 'nuwiki: wrap word in colour span' }, bufnr)
|
||||||
map('x', '<Leader>wc', function() cmd.colorize() end,
|
map('x', p .. 'c', function() cmd.colorize(nil, true) end,
|
||||||
{ desc = 'nuwiki: wrap selection in colour span' }, bufnr)
|
{ desc = 'nuwiki: wrap selection in colour span' }, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -215,10 +228,17 @@ function M.attach(bufnr, mappings)
|
|||||||
{ desc = 'nuwiki: list dedent subtree' }, bufnr)
|
{ desc = 'nuwiki: list dedent subtree' }, bufnr)
|
||||||
map('n', 'gLl', function() cmd.list_change_level(1, true) end,
|
map('n', 'gLl', function() cmd.list_change_level(1, true) end,
|
||||||
{ desc = 'nuwiki: list indent subtree' }, bufnr)
|
{ desc = 'nuwiki: list indent subtree' }, bufnr)
|
||||||
|
-- Upstream binds case-variant aliases gLH/gLL/gLR == gLh/gLl/gLr.
|
||||||
|
map('n', 'gLH', function() cmd.list_change_level(-1, true) end,
|
||||||
|
{ desc = 'nuwiki: list dedent subtree' }, bufnr)
|
||||||
|
map('n', 'gLL', function() cmd.list_change_level(1, true) end,
|
||||||
|
{ desc = 'nuwiki: list indent subtree' }, bufnr)
|
||||||
map('n', 'glr', cmd.list_renumber,
|
map('n', 'glr', cmd.list_renumber,
|
||||||
{ desc = 'nuwiki: renumber list' }, bufnr)
|
{ desc = 'nuwiki: renumber list' }, bufnr)
|
||||||
map('n', 'gLr', cmd.list_renumber_all,
|
map('n', 'gLr', cmd.list_renumber_all,
|
||||||
{ desc = 'nuwiki: renumber all lists' }, bufnr)
|
{ desc = 'nuwiki: renumber all lists' }, bufnr)
|
||||||
|
map('n', 'gLR', cmd.list_renumber_all,
|
||||||
|
{ desc = 'nuwiki: renumber all lists' }, bufnr)
|
||||||
-- gl<sym>/gL<sym>: set the current item's / whole list's marker (upstream
|
-- gl<sym>/gL<sym>: set the current item's / whole list's marker (upstream
|
||||||
-- VimwikiChangeSymbolTo / ChangeSymbolInListTo). 1) is command-only.
|
-- VimwikiChangeSymbolTo / ChangeSymbolInListTo). 1) is command-only.
|
||||||
for _, pair in ipairs({
|
for _, pair in ipairs({
|
||||||
@@ -260,6 +280,9 @@ function M.attach(bufnr, mappings)
|
|||||||
-- result is the keystrokes fed — keeps undo coherent.
|
-- result is the keystrokes fed — keeps undo coherent.
|
||||||
map('i', '<CR>', cmd.smart_return,
|
map('i', '<CR>', cmd.smart_return,
|
||||||
{ desc = 'nuwiki: smart return (insert)', expr = true }, bufnr)
|
{ desc = 'nuwiki: smart return (insert)', expr = true }, bufnr)
|
||||||
|
-- Smart <S-CR>: multiline list item (continuation with no new marker).
|
||||||
|
map('i', '<S-CR>', cmd.smart_shift_return,
|
||||||
|
{ desc = 'nuwiki: multiline list item (insert)', expr = true }, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== Insert-mode table navigation (Cluster 6) =====
|
-- ===== Insert-mode table navigation (Cluster 6) =====
|
||||||
@@ -286,19 +309,19 @@ function M.attach(bufnr, mappings)
|
|||||||
|
|
||||||
-- ===== Tables =====
|
-- ===== Tables =====
|
||||||
if on('table_editing') then
|
if on('table_editing') then
|
||||||
map('n', 'gqq', cmd.table_align, { desc = 'nuwiki: align table' }, bufnr)
|
map('n', 'gqq', function() cmd.table_align_or_cmd('gqq') end, { desc = 'nuwiki: align table / gqq' }, bufnr)
|
||||||
map('n', 'gq1', cmd.table_align, { desc = 'nuwiki: align table' }, bufnr)
|
map('n', 'gq1', function() cmd.table_align_or_cmd('gqq') end, { desc = 'nuwiki: align table / gqq' }, bufnr)
|
||||||
map('n', 'gww', cmd.table_align, { desc = 'nuwiki: align table (wide)' }, bufnr)
|
map('n', 'gww', function() cmd.table_align_or_cmd('gww') end, { desc = 'nuwiki: align table / gww' }, bufnr)
|
||||||
map('n', 'gw1', cmd.table_align, { desc = 'nuwiki: align table (wide)' }, bufnr)
|
map('n', 'gw1', function() cmd.table_align_or_cmd('gww') end, { desc = 'nuwiki: align table / gww' }, bufnr)
|
||||||
map('n', '<A-Left>', cmd.table_move_column_left, { desc = 'nuwiki: move column left' }, bufnr)
|
map('n', '<A-Left>', cmd.table_move_column_left, { desc = 'nuwiki: move column left' }, bufnr)
|
||||||
map('n', '<A-Right>', cmd.table_move_column_right, { desc = 'nuwiki: move column right' }, bufnr)
|
map('n', '<A-Right>', cmd.table_move_column_right, { desc = 'nuwiki: move column right' }, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== HTML =====
|
-- ===== HTML =====
|
||||||
if on('html_export') then
|
if on('html_export') then
|
||||||
map('n', '<Leader>wh', cmd.export_current, { desc = 'nuwiki: export to HTML' }, bufnr)
|
map('n', p .. 'h', cmd.export_current, { desc = 'nuwiki: export to HTML' }, bufnr)
|
||||||
map('n', '<Leader>whh', cmd.export_browse, { desc = 'nuwiki: export + browse' }, bufnr)
|
map('n', p .. 'hh', cmd.export_browse, { desc = 'nuwiki: export + browse' }, bufnr)
|
||||||
map('n', '<Leader>wha', cmd.export_all, { desc = 'nuwiki: export all' }, bufnr)
|
map('n', p .. 'ha', cmd.export_all, { desc = 'nuwiki: export all' }, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== Mouse (opt-in via `mappings.mouse = true`) =====
|
-- ===== Mouse (opt-in via `mappings.mouse = true`) =====
|
||||||
|
|||||||
+14
-5
@@ -14,13 +14,22 @@ local function command()
|
|||||||
return { install.expected_path() }
|
return { install.expected_path() }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Merge g:nuwiki_wikis (VimL config) into opts.wikis when the user has not
|
-- Build the server config payload. The wiki list comes from the shared
|
||||||
-- passed wikis through setup(). This lets users who configure via a .vim
|
-- resolver (config.wikis) — native setup() wikis, g:nuwiki_wikis, or an
|
||||||
-- file avoid duplicating the list in their Lua setup() call.
|
-- upstream g:vimwiki_list, with global scalar defaults already folded — so the
|
||||||
|
-- payload and the buffer-side commands agree on which wikis exist.
|
||||||
local function resolved_opts()
|
local function resolved_opts()
|
||||||
local opts = config.options
|
local opts = config.options
|
||||||
if not opts.wikis and vim.g.nuwiki_wikis then
|
local wikis = config.wikis()
|
||||||
opts = vim.tbl_extend('force', opts, { wikis = vim.g.nuwiki_wikis })
|
if #wikis > 0 then
|
||||||
|
opts = vim.tbl_extend('force', opts, { wikis = wikis })
|
||||||
|
else
|
||||||
|
-- Single-wiki shorthand: fold the global scalars into the top-level payload
|
||||||
|
-- so the server's single-wiki desugaring honours them.
|
||||||
|
local globals = config.scalar_globals()
|
||||||
|
if not vim.tbl_isempty(globals) then
|
||||||
|
opts = vim.tbl_extend('keep', opts, globals)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return opts
|
return opts
|
||||||
end
|
end
|
||||||
|
|||||||
+54
-8
@@ -12,6 +12,12 @@ if exists('g:loaded_nuwiki')
|
|||||||
endif
|
endif
|
||||||
let g:loaded_nuwiki = 1
|
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.4.2'
|
||||||
|
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
|
||||||
" <sfile> is still valid. Commands that need this path must reference
|
" <sfile> is still valid. Commands that need this path must reference
|
||||||
" s:plugin_root — re-expanding <sfile> inside a command body evaluates
|
" s:plugin_root — re-expanding <sfile> inside a command body evaluates
|
||||||
@@ -31,6 +37,11 @@ endif
|
|||||||
augroup nuwiki_calendar_detect
|
augroup nuwiki_calendar_detect
|
||||||
autocmd!
|
autocmd!
|
||||||
autocmd FileType vimwiki call s:nuwiki_setup_calendar()
|
autocmd FileType vimwiki call s:nuwiki_setup_calendar()
|
||||||
|
" Track the wiki of the active buffer so the calendar diaries into the wiki
|
||||||
|
" you're editing (not always the first one). FileType covers initial load;
|
||||||
|
" BufEnter covers switching back to an already-open wiki buffer.
|
||||||
|
autocmd FileType vimwiki call nuwiki#diary#track_wiki(expand('%:p'))
|
||||||
|
autocmd BufEnter * if &filetype ==# 'vimwiki' | call nuwiki#diary#track_wiki(expand('%:p')) | endif
|
||||||
augroup END
|
augroup END
|
||||||
function! s:nuwiki_setup_calendar() abort
|
function! s:nuwiki_setup_calendar() abort
|
||||||
if !get(g:, 'nuwiki_use_calendar', 1) || get(g:, 'nuwiki_no_calendar', 0)
|
if !get(g:, 'nuwiki_use_calendar', 1) || get(g:, 'nuwiki_no_calendar', 0)
|
||||||
@@ -63,6 +74,19 @@ augroup nuwiki_vim_lsp_start
|
|||||||
autocmd FileType vimwiki call nuwiki#lsp#start()
|
autocmd FileType vimwiki call nuwiki#lsp#start()
|
||||||
augroup END
|
augroup END
|
||||||
|
|
||||||
|
" vimwiki `auto_header` (default off): insert a level-1 header from the filename
|
||||||
|
" on new wiki pages. BufNewFile fires only for genuinely new files. Matches the
|
||||||
|
" configured extension(s).
|
||||||
|
if get(g:, 'nuwiki_auto_header', 0)
|
||||||
|
let s:awh_ext = get(g:, 'nuwiki_file_extension', '.wiki')
|
||||||
|
let s:awh_ext = s:awh_ext[0] ==# '.' ? s:awh_ext : '.' . s:awh_ext
|
||||||
|
augroup nuwiki_auto_header
|
||||||
|
autocmd!
|
||||||
|
execute 'autocmd BufNewFile *' . s:awh_ext . ' call nuwiki#commands#auto_header()'
|
||||||
|
augroup END
|
||||||
|
unlet s:awh_ext
|
||||||
|
endif
|
||||||
|
|
||||||
" Global wiki-picker commands — available from any buffer so a wiki can be
|
" Global wiki-picker commands — available from any buffer so a wiki can be
|
||||||
" chosen before a .wiki file exists. The picker reads its list from config
|
" chosen before a .wiki file exists. The picker reads its list from config
|
||||||
" (no LSP); opening the chosen index then auto-starts the server. The
|
" (no LSP); opening the chosen index then auto-starts the server. The
|
||||||
@@ -71,16 +95,38 @@ augroup END
|
|||||||
command! -nargs=0 VimwikiUISelect call nuwiki#commands#wiki_ui_select()
|
command! -nargs=0 VimwikiUISelect call nuwiki#commands#wiki_ui_select()
|
||||||
command! -nargs=0 NuwikiUISelect call nuwiki#commands#wiki_ui_select()
|
command! -nargs=0 NuwikiUISelect call nuwiki#commands#wiki_ui_select()
|
||||||
|
|
||||||
|
" Print the nuwiki version + host editor (upstream's :VimwikiShowVersion).
|
||||||
|
command! -nargs=0 VimwikiShowVersion call nuwiki#commands#show_version()
|
||||||
|
command! -nargs=0 NuwikiShowVersion call nuwiki#commands#show_version()
|
||||||
|
|
||||||
|
" Global :VimwikiIndex / :VimwikiTabIndex entry points (upstream defines the
|
||||||
|
" Index family globally in plugin/). The buffer-local copies in
|
||||||
|
" ftplugin/vimwiki.vim shadow these inside wiki buffers; these let a wiki be
|
||||||
|
" opened from any buffer. `[count]` selects the wiki number.
|
||||||
|
command! -count=0 VimwikiIndex call nuwiki#commands#wiki_index(<count>)
|
||||||
|
command! -count=0 NuwikiIndex call nuwiki#commands#wiki_index(<count>)
|
||||||
|
command! -count=0 VimwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
|
||||||
|
command! -count=0 NuwikiTabIndex call nuwiki#commands#wiki_tab_index(<count>)
|
||||||
|
|
||||||
|
" Get/set nuwiki client globals (upstream's :VimwikiVar).
|
||||||
|
command! -nargs=* VimwikiVar call nuwiki#commands#var(<q-args>)
|
||||||
|
command! -nargs=* NuwikiVar call nuwiki#commands#var(<q-args>)
|
||||||
|
|
||||||
" Global entry-point mappings — work from any buffer, not just .wiki files.
|
" Global entry-point mappings — work from any buffer, not just .wiki files.
|
||||||
" Files are opened directly from config (no LSP required); the server
|
" Files are opened directly from config (no LSP required); the server
|
||||||
" auto-starts via the FileType autocmd once the vimwiki buffer loads.
|
" auto-starts via the FileType autocmd once the vimwiki buffer loads.
|
||||||
if !get(g:, 'nuwiki_no_default_mappings', 0)
|
if !get(g:, 'nuwiki_no_default_mappings', 0)
|
||||||
nnoremap <silent> <Leader>ww :call nuwiki#commands#open_wiki_path(0)<CR>
|
" Prefix mirrors vimwiki's g:vimwiki_map_prefix (default <Leader>w). Built
|
||||||
nnoremap <silent> <Leader>wt :call nuwiki#commands#open_wiki_path(1)<CR>
|
" with :exe so a custom prefix relocates the whole entry-point family.
|
||||||
nnoremap <silent> <Leader>ws :call nuwiki#commands#wiki_ui_select()<CR>
|
let s:prefix = get(g:, 'nuwiki_map_prefix', '<Leader>w')
|
||||||
nnoremap <silent> <Leader>wi :call nuwiki#commands#open_diary_index_path()<CR>
|
exe 'nnoremap <silent> ' . s:prefix . 'w :call nuwiki#commands#open_wiki_path(0)<CR>'
|
||||||
nnoremap <silent> <Leader>w<Leader>w :call nuwiki#commands#open_diary_path(0)<CR>
|
exe 'nnoremap <silent> ' . s:prefix . 't :call nuwiki#commands#open_wiki_path(1)<CR>'
|
||||||
nnoremap <silent> <Leader>w<Leader>y :call nuwiki#commands#open_diary_path(-1)<CR>
|
exe 'nnoremap <silent> ' . s:prefix . 's :call nuwiki#commands#wiki_ui_select()<CR>'
|
||||||
nnoremap <silent> <Leader>w<Leader>t :call nuwiki#commands#open_diary_path(1)<CR>
|
exe 'nnoremap <silent> ' . s:prefix . 'i :call nuwiki#commands#open_diary_index_path()<CR>'
|
||||||
nnoremap <silent> <Leader>w<Leader>i :call nuwiki#commands#diary_generate_index()<CR>
|
exe 'nnoremap <silent> ' . s:prefix . '<Leader>w :call nuwiki#commands#open_diary_path(0)<CR>'
|
||||||
|
exe 'nnoremap <silent> ' . s:prefix . '<Leader>y :call nuwiki#commands#open_diary_path(-1)<CR>'
|
||||||
|
exe 'nnoremap <silent> ' . s:prefix . '<Leader>t :tabnew<CR>:call nuwiki#commands#open_diary_path(0)<CR>'
|
||||||
|
exe 'nnoremap <silent> ' . s:prefix . '<Leader>m :call nuwiki#commands#open_diary_path(1)<CR>'
|
||||||
|
exe 'nnoremap <silent> ' . s:prefix . '<Leader>i :call nuwiki#commands#diary_generate_index()<CR>'
|
||||||
|
unlet s:prefix
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
"
|
"
|
||||||
" Invoked by Dein / vim-plug build hooks:
|
" Invoked by Dein / vim-plug build hooks:
|
||||||
" vim -e -s -c "source scripts/download_bin.vim" -c "q"
|
" vim -e -s -c "source scripts/download_bin.vim" -c "q"
|
||||||
|
"
|
||||||
|
" That invocation runs in 'compatible' mode, where leading-backslash line
|
||||||
|
" continuations aren't recognised (they'd raise E10, and any error in silent
|
||||||
|
" Ex mode makes Vim exit non-zero — a spurious build failure). Reset
|
||||||
|
" 'cpoptions' to the Vim default for the duration so the script parses, and
|
||||||
|
" restore it at the end.
|
||||||
|
let s:cpo_save = &cpo
|
||||||
|
set cpo&vim
|
||||||
|
|
||||||
let s:plugin_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h:h')
|
let s:plugin_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h:h')
|
||||||
let s:bin_dir = s:plugin_dir . '/bin'
|
let s:bin_dir = s:plugin_dir . '/bin'
|
||||||
@@ -56,7 +64,10 @@ function! s:download_release() abort
|
|||||||
if !executable('curl') || !executable('tar')
|
if !executable('curl') || !executable('tar')
|
||||||
return 0
|
return 0
|
||||||
endif
|
endif
|
||||||
let l:url = 'https://code.gfran.co/gffranco/nuwiki/releases/latest/download/nuwiki-ls-'
|
" Gitea serves the most recent release under the `latest` tag segment
|
||||||
|
" (/releases/download/latest/<asset>). NOT GitHub's
|
||||||
|
" /releases/latest/download/<asset> shape, which Gitea 404s.
|
||||||
|
let l:url = 'https://code.gfran.co/gffranco/nuwiki/releases/download/latest/nuwiki-ls-'
|
||||||
\ . l:target . '.tar.gz'
|
\ . l:target . '.tar.gz'
|
||||||
let l:archive = tempname() . '.tar.gz'
|
let l:archive = tempname() . '.tar.gz'
|
||||||
call system('curl -fsSL -o ' . shellescape(l:archive) . ' ' . shellescape(l:url))
|
call system('curl -fsSL -o ' . shellescape(l:archive) . ' ' . shellescape(l:url))
|
||||||
@@ -86,3 +97,5 @@ else
|
|||||||
echohl ErrorMsg | echom 'nuwiki: install failed' | echohl None
|
echohl ErrorMsg | echom 'nuwiki: install failed' | echohl None
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
let &cpo = s:cpo_save
|
||||||
|
|||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/set-version.sh — stamp a release version into every hardcoded spot.
|
||||||
|
#
|
||||||
|
# This is the single source of truth for *where* the version lives. The
|
||||||
|
# release pipeline (.gitea/workflows/release.yaml) calls it with the
|
||||||
|
# workflow-dispatch input; run it the same way to prepare a release by hand:
|
||||||
|
#
|
||||||
|
# scripts/set-version.sh 0.4.0
|
||||||
|
#
|
||||||
|
# It rewrites:
|
||||||
|
# 1. Cargo.toml — [workspace.package] version
|
||||||
|
# 2. crates/nuwiki-lsp/Cargo.toml — nuwiki-core path-dep version pin
|
||||||
|
# 3. crates/nuwiki-ls/Cargo.toml — nuwiki-lsp path-dep version pin
|
||||||
|
# 4. plugin/nuwiki.vim — g:nuwiki_version (the Lua client reads it too)
|
||||||
|
# 5. Cargo.lock — the version of our three own crates
|
||||||
|
#
|
||||||
|
# Cargo.lock is patched directly (awk) so this needs no cargo/toolchain —
|
||||||
|
# the only thing that changes for our crates is their version string.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ver="${1:-}"
|
||||||
|
ver="${ver#v}" # tolerate a leading "v"
|
||||||
|
if ! printf '%s' "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$'; then
|
||||||
|
echo "usage: $0 <semver> e.g. $0 0.4.0" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
# 1. Workspace package version (root Cargo.toml, [workspace.package]).
|
||||||
|
sed -i -E "s/^version = \"[0-9][^\"]*\"/version = \"$ver\"/" Cargo.toml
|
||||||
|
|
||||||
|
# 2-3. Internal path-dep pins must track the workspace version.
|
||||||
|
sed -i -E "s|(nuwiki-core = \{ path = \"\.\./nuwiki-core\", version = )\"[^\"]*\"|\1\"$ver\"|" \
|
||||||
|
crates/nuwiki-lsp/Cargo.toml
|
||||||
|
sed -i -E "s|(nuwiki-lsp = \{ path = \"\.\./nuwiki-lsp\", version = )\"[^\"]*\"|\1\"$ver\"|" \
|
||||||
|
crates/nuwiki-ls/Cargo.toml
|
||||||
|
|
||||||
|
# 4. VimL client version global (the Lua client reads this same g: var).
|
||||||
|
sed -i -E "s/(let g:nuwiki_version = ')[^']*(')/\1$ver\2/" plugin/nuwiki.vim
|
||||||
|
|
||||||
|
# 5. Cargo.lock entries for our own crates. Each [[package]] block lists the
|
||||||
|
# name line immediately followed by the version line; rewrite only those.
|
||||||
|
for crate in nuwiki-core nuwiki-lsp nuwiki-ls; do
|
||||||
|
awk -v c="$crate" -v v="$ver" '
|
||||||
|
$0 == "name = \"" c "\"" {
|
||||||
|
print
|
||||||
|
if (getline > 0) { sub(/version = "[^"]*"/, "version = \"" v "\"") }
|
||||||
|
print
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' Cargo.lock > Cargo.lock.tmp && mv Cargo.lock.tmp Cargo.lock
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Set version to $ver"
|
||||||
+10
-1
@@ -54,9 +54,12 @@ if has('nvim')
|
|||||||
|
|
||||||
" Misc.
|
" Misc.
|
||||||
highlight default link @vimwikiKeyword Todo
|
highlight default link @vimwikiKeyword Todo
|
||||||
highlight default link @vimwikiColor Constant
|
|
||||||
highlight default link @vimwikiComment Comment
|
highlight default link @vimwikiComment Comment
|
||||||
highlight default link @vimwikiDefinitionTerm Statement
|
highlight default link @vimwikiDefinitionTerm Statement
|
||||||
|
" NOTE: no default for @vimwikiColor. Colour spans are coloured client-side
|
||||||
|
" in their *actual* colour by nuwiki#colors#refresh() (conceal + per-colour
|
||||||
|
" highlight); linking the LSP token to a fixed group here would override that
|
||||||
|
" real colour on the concealed text in Neovim.
|
||||||
endif
|
endif
|
||||||
|
|
||||||
" ===== Static fallback (no LSP required) =====
|
" ===== Static fallback (no LSP required) =====
|
||||||
@@ -205,4 +208,10 @@ endfunction
|
|||||||
|
|
||||||
call s:nuwiki_setup_nested()
|
call s:nuwiki_setup_nested()
|
||||||
|
|
||||||
|
" Conceal `:VimwikiColorize` spans (`<span style="color:…">…</span>`) down to
|
||||||
|
" their coloured text. Reset the per-buffer cache first so a syntax reload
|
||||||
|
" (e.g. on :colorscheme) re-establishes the regions cleared by `:syntax clear`.
|
||||||
|
let b:nuwiki_color_seen = {}
|
||||||
|
call nuwiki#colors#refresh()
|
||||||
|
|
||||||
let b:current_syntax = 'vimwiki'
|
let b:current_syntax = 'vimwiki'
|
||||||
|
|||||||
Reference in New Issue
Block a user