Merge remote-tracking branch 'origin/calendar-support' into feature-gap
CI / cargo fmt --check (push) Successful in 35s
CI / cargo clippy (push) Successful in 32s
CI / cargo test (push) Successful in 39s
CI / editor keymaps (push) Successful in 1m32s

# Conflicts:
#	development/vimwiki-gap.md
This commit is contained in:
2026-06-03 01:46:07 +00:00
18 changed files with 682 additions and 45 deletions
+4
View File
@@ -100,3 +100,7 @@ jobs:
run: ./development/tests/test-config.sh run: ./development/tests/test-config.sh
- name: run Vim config parity harness - name: run Vim config parity harness
run: ./development/tests/test-config-vim.sh run: ./development/tests/test-config-vim.sh
- name: run Neovim calendar integration harness
run: ./development/tests/test-calendar.sh
- name: run Vim calendar integration harness
run: ./development/tests/test-calendar-vim.sh
+96
View File
@@ -0,0 +1,96 @@
" autoload/nuwiki/diary.vim — diary-specific functions for nuwiki
" Includes calendar-vim integration functions
" ===== Calendar-Vim Integration =====
" Function: nuwiki#diary#calendar_action(day, month, year, week, dir)
" 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
" other value means a horizontal split). week is unused.
" Opens or creates the diary entry for the given date.
function! nuwiki#diary#calendar_action(day, month, year, week, dir) abort
" 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)
" Get wiki configuration for the first wiki (index 0)
let l:c = s:wiki_cfg(0)
" 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
" Remember the calendar window so we can close it once the diary opens.
let l:cal_winid = win_getid()
" Open the diary file in a different window. If a previous window exists go
" there; otherwise split / vsplit / new depending on a:dir.
if winnr('#') > 0
execute 'wincmd p'
if !&hidden && &modified
execute 'new'
endif
elseif a:dir ==# 'V'
execute 'vsplit'
else
execute 'split'
endif
execute 'edit ' . fnameescape(l:file_path)
" A brand-new entry has no file on disk yet, so ftdetect may not have run;
" ensure the vimwiki filetype so syntax/maps are active immediately.
if &filetype !=# 'vimwiki'
setfiletype vimwiki
endif
" Close the calendar window now that the diary is open (keep at least one).
if win_getid() != l:cal_winid && win_id2win(l:cal_winid) > 0 && winnr('$') > 1
let l:diary_winid = win_getid()
if win_gotoid(l:cal_winid)
close
endif
call win_gotoid(l:diary_winid)
endif
endfunction
" Function: nuwiki#diary#calendar_sign(day, month, year)
" Called by calendar-vim to determine which dates show markers (*)
" day, month, year are numeric integers (1-based).
" Returns a single-character string to use as marker (e.g. '*' or ''),
" or 1/0 for 1-indexed calendars.
" calendar-vim's default expects the return value to be a single character
" to display; returning any non-empty string shows a marker.
function! nuwiki#diary#calendar_sign(day, month, year) abort
" 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)
" Get wiki configuration for the first wiki (index 0)
let l:c = s:wiki_cfg(0)
" Construct expected file path
let l:file_path = l:c.root . '/' . l:c.diary_rel . '/' . l:date_str . l:c.ext
" Return a marker if the file exists, empty string otherwise
if filereadable(l:file_path)
return '*'
endif
return ''
endfunction
" Helper: get wiki configuration for wiki n (0-based).
" Mirrors the same logic used by nuwiki#commands#open_diary_path etc.
function! s:wiki_cfg(n) abort
let l:w = {}
if exists('g:nuwiki_wikis') && len(g:nuwiki_wikis) > a:n
let l:w = g:nuwiki_wikis[a:n]
endif
let l:root = expand(get(l:w, 'root',
\ get(g:, 'nuwiki_wiki_root', '~/vimwiki')))
let l:ext = get(l:w, 'file_extension',
\ get(g:, 'nuwiki_file_extension', '.wiki'))
if l:ext[0] !=# '.' | let l:ext = '.' . l:ext | endif
let l:idx = get(l:w, 'index', 'index')
let l:diary_rel = get(l:w, 'diary_rel_path',
\ get(g:, 'nuwiki_diary_rel_path', 'diary'))
let l:diary_idx = get(l:w, 'diary_index', 'diary')
return { 'root': l:root, 'ext': l:ext, 'idx': l:idx,
\ 'diary_rel': l:diary_rel, 'diary_idx': l:diary_idx }
endfunction
+14
View File
@@ -0,0 +1,14 @@
" autoload/vimwiki/diary.vim — vimwiki namespace aliases for nuwiki diary functions
" Provides drop-in compatibility for vimwiki-dependent plugins like calendar-vim
" vimwiki#diary#calendar_action(day, month, year, week, dir)
" Alias to nuwiki#diary#calendar_action for calendar-vim compatibility
function! vimwiki#diary#calendar_action(day, month, year, week, dir) abort
return nuwiki#diary#calendar_action(a:day, a:month, a:year, a:week, a:dir)
endfunction
" vimwiki#diary#calendar_sign(day, month, year)
" Alias to nuwiki#diary#calendar_sign for calendar-vim compatibility
function! vimwiki#diary#calendar_sign(day, month, year) abort
return nuwiki#diary#calendar_sign(a:day, a:month, a:year)
endfunction
+6 -1
View File
@@ -79,7 +79,12 @@ nuwiki/
│ ├── test-keymaps.sh # Neovim keymap harness │ ├── test-keymaps.sh # Neovim keymap harness
│ ├── test-keymaps.lua │ ├── test-keymaps.lua
│ ├── test-keymaps-vim.sh # Vim keymap harness │ ├── test-keymaps-vim.sh # Vim keymap harness
── test-keymaps-vim.vim ── test-keymaps-vim.vim
│ ├── test-calendar.sh # Neovim calendar-vim integration harness
│ ├── test-calendar.lua
│ ├── test-calendar-vim.sh # Vim calendar-vim integration harness
│ ├── test-calendar-vim.vim
│ └── test-calendar-vim-optout.vim
└── .gitea/ └── .gitea/
└── workflows/ └── workflows/
+14
View File
@@ -290,6 +290,20 @@ reset_dirs() {
done done
} }
# ensure_calendar_vim — clone calendar-vim into dev dir for testing
ensure_calendar_vim() {
local repo="$DEV_DIR/calendar-vim"
if [[ -d "$repo/.git" ]]; then
return
fi
if ! command -v git >/dev/null 2>&1; then
log "git not found — skipping calendar-vim"
return 1
fi
log "cloning https://github.com/mattn/calendar-vim.git → $repo"
git clone --depth 1 https://github.com/mattn/calendar-vim.git "$repo" >/dev/null 2>&1
}
seed_wiki() { seed_wiki() {
if [[ "${NUWIKI_DEV_NO_SEED:-0}" == "1" ]]; then if [[ "${NUWIKI_DEV_NO_SEED:-0}" == "1" ]]; then
log "NUWIKI_DEV_NO_SEED=1 — skipping wiki seed (using $WIKI_DIR as-is)" log "NUWIKI_DEV_NO_SEED=1 — skipping wiki seed (using $WIKI_DIR as-is)"
+15 -7
View File
@@ -4,10 +4,15 @@
This document describes how nuwiki (vimwiki) integrates with calendar-vim to provide graphical calendar navigation for wiki diary entries. This document describes how nuwiki (vimwiki) integrates with calendar-vim to provide graphical calendar navigation for wiki diary entries.
## Integration Mechanism ## Integration Mechanism
Nuwiki activates the integration by setting calendar-vim's hook variables when `g:vimwiki_use_calendar` is enabled (default: true): Nuwiki auto-detects calendar-vim on the runtimepath (via a `FileType vimwiki`
autocmd in `plugin/nuwiki.vim`) and wires calendar-vim's hook variables, unless
the integration is disabled (default: enabled):
- `g:calendar_action = 'vimwiki#diary#calendar_action'` - `g:calendar_action = 'vimwiki#diary#calendar_action'`
- `g:calendar_sign = 'vimwiki#diary#calendar_sign'` - `g:calendar_sign = 'vimwiki#diary#calendar_sign'`
These hooks are only set when unset or still at calendar-vim's own defaults
(`calendar#diary` / `calendar#sign`), so a user-chosen custom hook is preserved.
## Primary Use Case: Viewing/Create Diary Entries ## Primary Use Case: Viewing/Create Diary Entries
1. User executes `:Calendar` command 1. User executes `:Calendar` command
2. Calendar-vim displays navigable calendar interface 2. Calendar-vim displays navigable calendar interface
@@ -15,9 +20,9 @@ Nuwiki activates the integration by setting calendar-vim's hook variables when `
4. User presses `<Enter>` on selected date 4. User presses `<Enter>` on selected date
5. `vimwiki#diary#calendar_action` function executes: 5. `vimwiki#diary#calendar_action` function executes:
- Formats date as YYYY-MM-DD - Formats date as YYYY-MM-DD
- Opens existing diary file or creates new one - Opens existing diary file or creates new one (split/vsplit per `dir`)
- Applies appropriate wiki filetype - Applies the vimwiki filetype to a brand-new entry
- Sets up autocmd to refresh calendar on file close - Closes the calendar window once the diary entry is open
6. User views/edits diary entry for selected date 6. User views/edits diary entry for selected date
## Date Marking Functionality ## Date Marking Functionality
@@ -30,12 +35,15 @@ Nuwiki activates the integration by setting calendar-vim's hook variables when `
- Automatic file creation/opening for selected dates - Automatic file creation/opening for selected dates
- Visual indicators for dates with existing content - Visual indicators for dates with existing content
- Proper window splitting based on calendar orientation - Proper window splitting based on calendar orientation
- Automatic calendar refresh after editing diary entries - Calendar window closed automatically once the diary entry opens
## Configuration ## Configuration
Controlled by: Controlled by:
- `g:vimwiki_use_calendar` (global toggle) - `use_calendar` Lua option (Neovim `setup()`), default `true`
- Individual wiki configurations in `g:vimwiki_list` (path, diary_rel_path, ext) - `g:nuwiki_use_calendar` / `g:nuwiki_no_calendar` (VimL global toggles)
- Individual wiki configurations in `g:nuwiki_wikis` (root, diary_rel_path,
file_extension), or the scalar `g:nuwiki_wiki_root` / `g:nuwiki_diary_rel_path`
/ `g:nuwiki_file_extension` fallbacks
- Standard calendar-vim variables for appearance/customization - Standard calendar-vim variables for appearance/customization
This integration leverages calendar-vim's strong date presentation while using nuwiki's diary system for file management, creating a cohesive experience for journal-style wiki usage. This integration leverages calendar-vim's strong date presentation while using nuwiki's diary system for file management, creating a cohesive experience for journal-style wiki usage.
+7 -10
View File
@@ -54,17 +54,13 @@ vim.env.XDG_DATA_HOME = '$DATA_DIR'
vim.env.XDG_CONFIG_HOME = '$CONFIG_DIR' vim.env.XDG_CONFIG_HOME = '$CONFIG_DIR'
vim.opt.runtimepath:prepend('$REPO_ROOT') vim.opt.runtimepath:prepend('$REPO_ROOT')
vim.opt.runtimepath:append('$DEV_DIR/calendar-vim')
vim.g.nuwiki_binary_path = '$REPO_ROOT/bin/nuwiki-ls' vim.g.nuwiki_binary_path = '$REPO_ROOT/bin/nuwiki-ls'
vim.g.nuwiki_wiki_root = '$WIKI_DIR'
-- Friendly defaults for an interactive smoke test. vim.g.nuwiki_file_extension = '.wiki'
vim.opt.number = true vim.g.nuwiki_syntax = 'vimwiki'
vim.opt.signcolumn = 'yes' vim.g.nuwiki_diary_rel_path = 'diary'
vim.opt.termguicolors = true
vim.opt.swapfile = false
vim.opt.writebackup = false
vim.opt.undofile = false
vim.g.mapleader = ' '
require('nuwiki').setup({ require('nuwiki').setup({
wiki_root = '$WIKI_DIR', wiki_root = '$WIKI_DIR',
@@ -74,13 +70,14 @@ require('nuwiki').setup({
-- they're swallowed; with it they land in :messages. -- they're swallowed; with it they land in :messages.
vim.lsp.set_log_level('info') vim.lsp.set_log_level('info')
print('[nuwiki-dev] ready — wiki root: $WIKI_DIR') print('[nuwiki-dev] ready — wiki root: ' .. '$WIKI_DIR')
print('[nuwiki-dev] :LspInfo to inspect, :checkhealth nuwiki for diagnostics') print('[nuwiki-dev] :LspInfo to inspect, :checkhealth nuwiki for diagnostics')
EOF EOF
} }
main() { main() {
ensure_binary ensure_binary
ensure_calendar_vim
seed_wiki seed_wiki
write_init write_init
reset_dirs "$STATE_DIR" "$DATA_DIR" "$CONFIG_DIR" reset_dirs "$STATE_DIR" "$DATA_DIR" "$CONFIG_DIR"
+15 -15
View File
@@ -114,25 +114,27 @@ write_vimrc() {
" Confine state to the dev cache so we never touch the user's real Vim " Confine state to the dev cache so we never touch the user's real Vim
" install. " install.
set viminfo='100,n$VIM_STATE/viminfo set viminfo='100,n${VIM_STATE}/viminfo
let &directory = '$VIM_STATE/swap//' let &directory = '${VIM_STATE}/swap//'
let &backupdir = '$VIM_STATE/backup//' let &backupdir = '${VIM_STATE}/backup//'
let &undodir = '$VIM_STATE/undo//' let &undodir = '${VIM_STATE}/undo//'
" Make Vim look for runtime paths in the dev cache. vim-lsp is intentionally " Make Vim look for runtime paths in the dev cache. vim-lsp is intentionally
" absent so nuwiki dispatches jump-to-definition through coc. " absent so nuwiki dispatches jump-to-definition through coc.
set nocompatible set nocompatible
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath let &runtimepath = '${REPO_ROOT}' . ',' . &runtimepath
let &runtimepath .= ',$COC_DIR' let &runtimepath .= ',${COC_DIR}'
let &runtimepath .= ',${DEV_DIR}/calendar-vim'
" Point coc at the generated settings + a private data dir. " Point coc at the generated settings + a private data dir.
let g:coc_config_home = '$DEV_DIR' 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_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_diary_rel_path = 'diary'
let g:nuwiki_log_level = 'info' let g:nuwiki_log_level = 'info'
filetype plugin indent on filetype plugin indent on
@@ -153,11 +155,8 @@ augroup NuwikiFtdetect
autocmd BufRead,BufNewFile *.wiki setfiletype vimwiki autocmd BufRead,BufNewFile *.wiki setfiletype vimwiki
augroup END augroup END
" coc starts the server automatically from coc-settings.json once a vimwiki echo
" buffer loads — no explicit nuwiki#lsp#start() needed. echomsg '[nuwiki-dev] coc.preferences.jumpCommand = ' . '\$COC_JUMP'
echomsg '[nuwiki-dev] ready (coc.nvim) — wiki root: $WIKI_DIR'
echomsg '[nuwiki-dev] coc.preferences.jumpCommand = $COC_JUMP'
echomsg '[nuwiki-dev] <CR> on a [[link]] should open in the CURRENT window' echomsg '[nuwiki-dev] <CR> on a [[link]] should open in the CURRENT window'
echomsg '[nuwiki-dev] :CocList services for status, :messages for log' echomsg '[nuwiki-dev] :CocList services for status, :messages for log'
EOF EOF
@@ -167,6 +166,7 @@ main() {
ensure_node ensure_node
ensure_binary ensure_binary
ensure_coc ensure_coc
ensure_calendar_vim
write_coc_settings write_coc_settings
seed_wiki seed_wiki
write_vimrc write_vimrc
+12 -11
View File
@@ -70,19 +70,20 @@ write_vimrc() {
" Confine state to the dev cache so we never touch the user's real Vim " Confine state to the dev cache so we never touch the user's real Vim
" install. " install.
set viminfo='100,n$VIM_STATE/viminfo set viminfo='100,n${VIM_STATE}/viminfo
let &directory = '$VIM_STATE/swap//' let &directory = '${VIM_STATE}/swap//'
let &backupdir = '$VIM_STATE/backup//' let &backupdir = '${VIM_STATE}/backup//'
let &undodir = '$VIM_STATE/undo//' let &undodir = '${VIM_STATE}/undo//'
" Make Vim look for runtime paths in the dev cache. " Make Vim look for runtime paths in the dev cache.
set nocompatible set nocompatible
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath let &runtimepath = '${REPO_ROOT}' . ',' . &runtimepath
let &runtimepath .= ',$VIM_LSP_DIR' let &runtimepath .= ',${VIM_LSP_DIR}'
let &runtimepath .= ',$ASYNC_DIR' let &runtimepath .= ',${ASYNC_DIR}'
let &runtimepath .= ',${DEV_DIR}/calendar-vim'
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_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'
@@ -113,8 +114,7 @@ augroup NuwikiStart
autocmd FileType vimwiki call nuwiki#lsp#start() autocmd FileType vimwiki call nuwiki#lsp#start()
augroup END augroup END
" Status messages go to :messages so they don't trigger "Press ENTER". echomsg '[nuwiki-dev] ready — wiki root: ' . g:nuwiki_wiki_root
echomsg '[nuwiki-dev] ready — wiki root: $WIKI_DIR'
echomsg '[nuwiki-dev] :LspStatus for diagnostics, :messages for log' echomsg '[nuwiki-dev] :LspStatus for diagnostics, :messages for log'
EOF EOF
} }
@@ -122,6 +122,7 @@ EOF
main() { main() {
ensure_binary ensure_binary
ensure_vim_lsp ensure_vim_lsp
ensure_calendar_vim
seed_wiki seed_wiki
write_vimrc write_vimrc
reset_dirs "$VIM_STATE" reset_dirs "$VIM_STATE"
@@ -0,0 +1,40 @@
" development/tests/test-calendar-vim-optout.vim — driven by test-calendar-vim.sh.
"
" Asserts that the calendar-vim integration stays OFF when the user opts out
" (g:nuwiki_use_calendar = 0 or g:nuwiki_no_calendar = 1). In that case the
" FileType autocmd must leave calendar-vim's own defaults untouched, so the
" hooks still point at 'calendar#diary' / 'calendar#sign' (set by the stub).
let s:results = []
let s:pass = 0
let s:fail = 0
let s:out = $NUWIKI_CALENDAR_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
" The buffer is already a loaded vimwiki file (FileType has fired). nuwiki must
" NOT have rewired the hooks — they should still be calendar-vim's defaults.
call s:record(&filetype ==# 'vimwiki', 'ftplugin.attached',
\ &filetype ==# 'vimwiki' ? '' : 'filetype=' . &filetype)
call s:record(get(g:, 'calendar_action', '') !=# 'vimwiki#diary#calendar_action',
\ 'optout.action_not_wired',
\ 'g:calendar_action=' . get(g:, 'calendar_action', ''))
call s:record(get(g:, 'calendar_sign', '') !=# 'vimwiki#diary#calendar_sign',
\ 'optout.sign_not_wired',
\ 'g:calendar_sign=' . get(g:, 'calendar_sign', ''))
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!')
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
#
# development/tests/test-calendar-vim.sh — calendar-vim integration harness (Vim).
#
# Exercises the pure-VimL diary hooks (nuwiki#diary#calendar_action /
# nuwiki#diary#calendar_sign) plus plugin/nuwiki.vim's auto-wiring of
# g:calendar_action / g:calendar_sign. A stub calendar-vim is placed on the
# runtimepath so :Calendar exists and the hooks start at calendar-vim's own
# defaults — the exact state nuwiki must detect and override.
#
# Two runs:
# 1. default → integration ON, hooks rewired, diary paths from wiki root
# 2. opt-out → g:nuwiki_use_calendar=0 / g:nuwiki_no_calendar=1 leave
# calendar-vim's defaults untouched
#
# No LSP binary is needed — the diary hooks are entirely client-side VimL.
# Exit code: 0 when all assertions pass, 1 otherwise. Skips (0) without vim.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TMP="$(mktemp -d -t nuwiki-calendar-vim-test-XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
log() { printf '\033[1;34m[calendar-vim]\033[0m %s\n' "$*"; }
if ! command -v vim >/dev/null 2>&1; then
log 'vim not installed — skipping'
exit 0
fi
# --- Scratch wiki with one seeded diary entry (2026-05-30) ----------------
WIKI="$TMP/wiki"
mkdir -p "$WIKI/diary"
echo "= index =" > "$WIKI/index.wiki"
echo "= 2026-05-30 =" > "$WIKI/diary/2026-05-30.wiki"
# --- Stub calendar-vim ----------------------------------------------------
# Mimics the real plugin closely enough for detection + the override path:
# defines :Calendar and seeds calendar-vim's default hook names.
CAL="$TMP/calendar-vim"
mkdir -p "$CAL/plugin" "$CAL/autoload"
cat > "$CAL/plugin/calendar.vim" <<'EOF'
if exists('g:loaded_calendar_stub') | finish | endif
let g:loaded_calendar_stub = 1
let g:calendar_action = get(g:, 'calendar_action', 'calendar#diary')
let g:calendar_sign = get(g:, 'calendar_sign', 'calendar#sign')
command! -nargs=* Calendar echo 'stub calendar'
EOF
cat > "$CAL/autoload/calendar.vim" <<'EOF'
function! calendar#open(...) abort
endfunction
function! calendar#diary(...) abort
endfunction
function! calendar#sign(...) abort
return 0
endfunction
EOF
run_harness() {
local label="$1" vimrc="$2" harness="$3"
local results="$TMP/results-$label.txt"
log "running $label harness…"
NUWIKI_CALENDAR_RESULTS="$results" \
NUWIKI_CALENDAR_WIKI="$WIKI" \
timeout 30 vim -e -s -u "$vimrc" -c "edit $WIKI/index.wiki" \
-c "source $harness" </dev/null >"$TMP/$label.log" 2>&1 || true
if [[ ! -f "$results" ]]; then
echo "calendar $label harness produced no output" >&2
echo "--- vim log ---" >&2
cat "$TMP/$label.log" >&2 || true
return 1
fi
cat "$results"
grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$results"
}
# --- Run 1: default (integration ON) --------------------------------------
VIMRC="$TMP/vimrc"
cat > "$VIMRC" <<EOF
set nocompatible
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
let &runtimepath .= ',$CAL'
let g:nuwiki_wiki_root = '$WIKI'
let g:nuwiki_file_extension = '.wiki'
let g:nuwiki_diary_rel_path = 'diary'
set hidden
filetype plugin indent on
syntax enable
EOF
# --- Run 2: opt-out via g:nuwiki_use_calendar = 0 -------------------------
VIMRC_OPTOUT="$TMP/vimrc-optout"
cat > "$VIMRC_OPTOUT" <<EOF
set nocompatible
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
let &runtimepath .= ',$CAL'
let g:nuwiki_use_calendar = 0
let g:nuwiki_wiki_root = '$WIKI'
filetype plugin indent on
syntax enable
EOF
# --- Run 3: opt-out via g:nuwiki_no_calendar = 1 --------------------------
VIMRC_NOCAL="$TMP/vimrc-nocal"
cat > "$VIMRC_NOCAL" <<EOF
set nocompatible
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
let &runtimepath .= ',$CAL'
let g:nuwiki_no_calendar = 1
let g:nuwiki_wiki_root = '$WIKI'
filetype plugin indent on
syntax enable
EOF
rc=0
run_harness 'default' "$VIMRC" "$REPO_ROOT/development/tests/test-calendar-vim.vim" || rc=1
run_harness 'optout-use' "$VIMRC_OPTOUT" "$REPO_ROOT/development/tests/test-calendar-vim-optout.vim" || rc=1
run_harness 'optout-no' "$VIMRC_NOCAL" "$REPO_ROOT/development/tests/test-calendar-vim-optout.vim" || rc=1
exit $rc
+97
View File
@@ -0,0 +1,97 @@
" development/tests/test-calendar-vim.vim — driven by development/tests/test-calendar-vim.sh.
"
" Pure-VimL regression suite for the calendar-vim integration. The diary
" hooks (nuwiki#diary#calendar_action / nuwiki#diary#calendar_sign) are
" implemented entirely in VimL and shared verbatim by the Vim and Neovim
" clients, so this harness is the canonical coverage for them. The Vim
" plugin-side hook wiring (plugin/nuwiki.vim's FileType autocmd) is also
" asserted here; the Neovim setup() wiring has its own harness in
" development/tests/test-calendar.lua.
"
" A stub calendar-vim is placed on the runtimepath by the shell wrapper so
" :Calendar exists and g:calendar_action/g:calendar_sign already hold
" calendar-vim's own defaults ('calendar#diary'/'calendar#sign') — exactly
" the state nuwiki must detect and override.
let s:results = []
let s:pass = 0
let s:fail = 0
let s:out = $NUWIKI_CALENDAR_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
let s:wiki_root = $NUWIKI_CALENDAR_WIKI
" ===== Hook wiring: nuwiki overrides calendar-vim's defaults =====
" The stub set g:calendar_action='calendar#diary' / g:calendar_sign='calendar#sign'
" at load. Opening this .wiki buffer fired plugin/nuwiki.vim's FileType autocmd,
" which must have rewired both to the nuwiki delegators.
call s:record(&filetype ==# 'vimwiki', 'ftplugin.attached',
\ &filetype ==# 'vimwiki' ? '' : 'filetype=' . &filetype)
call s:record(get(g:, 'calendar_action', '') ==# 'vimwiki#diary#calendar_action',
\ 'hook.action_wired', 'g:calendar_action=' . get(g:, 'calendar_action', ''))
call s:record(get(g:, 'calendar_sign', '') ==# 'vimwiki#diary#calendar_sign',
\ 'hook.sign_wired', 'g:calendar_sign=' . get(g:, 'calendar_sign', ''))
" ===== calendar_sign: marker reflects diary-file existence =====
" The shell seeded a diary entry for 2026-05-30 and left 2026-05-31 absent.
call s:record(nuwiki#diary#calendar_sign(30, 5, 2026) ==# '*',
\ 'sign.existing_date_marked',
\ 'got ' . string(nuwiki#diary#calendar_sign(30, 5, 2026)))
call s:record(nuwiki#diary#calendar_sign(31, 5, 2026) ==# '',
\ 'sign.absent_date_unmarked',
\ 'got ' . string(nuwiki#diary#calendar_sign(31, 5, 2026)))
" The vimwiki# alias must return the identical result.
call s:record(vimwiki#diary#calendar_sign(30, 5, 2026) ==# '*',
\ 'sign.alias_matches', '')
" ===== calendar_action: opens WIKI_ROOT/diary/<date>.wiki, not ~/diary =====
" Set up two windows so the "previous window" branch runs and we can assert
" the calendar window is closed afterwards.
" Window A: the seed wiki (already current). Open a fresh window to stand in
" for the calendar, then invoke the action from it.
new
file [stub-calendar]
let s:cal_winid = win_getid()
let s:wins_before = winnr('$')
call nuwiki#diary#calendar_action(31, 5, 2026, 0, '')
let s:opened = expand('%:p')
let s:want = s:wiki_root . '/diary/2026-05-31.wiki'
call s:record(s:opened ==# s:want, 'action.opens_wiki_diary_path',
\ 'opened=' . s:opened)
call s:record(s:opened !~# '/diary/2026-05-31.wiki$' ? 0 : 1,
\ 'action.path_has_date_filename', 'opened=' . s:opened)
call s:record(s:opened !~# expand('~') . '/diary',
\ 'action.not_home_diary', 'opened=' . s:opened)
" The calendar window must be closed once the diary is open.
call s:record(win_id2win(s:cal_winid) == 0, 'action.closes_calendar_window',
\ 'cal still at win ' . win_id2win(s:cal_winid))
call s:record(winnr('$') == s:wins_before - 1, 'action.window_count_decremented',
\ 'before=' . s:wins_before . ' after=' . winnr('$'))
" ===== calendar_action sets the wiki filetype on a new entry =====
call s:record(&filetype ==# 'vimwiki', 'action.sets_filetype',
\ 'filetype=' . &filetype)
" ===== Wrap up =====
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!')
+92
View File
@@ -0,0 +1,92 @@
-- development/tests/test-calendar.lua — driven by development/tests/test-calendar.sh.
--
-- Headless Neovim harness for the calendar-vim integration wired through
-- `require('nuwiki').setup()`. The diary hooks themselves are pure VimL and
-- covered exhaustively by development/tests/test-calendar-vim.vim; this
-- harness focuses on the Neovim-only pieces:
-- * setup() registering the FileType autocmd that overrides calendar-vim's
-- own default hooks ('calendar#diary' / 'calendar#sign'),
-- * the window-close behaviour of calendar_action under the real (non-ex)
-- window API,
-- * disabling via use_calendar = false.
--
-- The opt-out case runs in a second nvim invocation (see the shell wrapper)
-- because setup() registers the autocmd once per process.
--
-- Results are written to $NUWIKI_CALENDAR_RESULTS so the wrapper can render
-- them without scraping nvim's output.
local OUT = vim.env.NUWIKI_CALENDAR_RESULTS or '/tmp/nuwiki-calendar-results.txt'
local WIKI = vim.env.NUWIKI_CALENDAR_WIKI
local MODE = vim.env.NUWIKI_CALENDAR_MODE or 'default'
local out_lines = {}
local pass, fail = 0, 0
local function record(ok, name, detail)
local suffix = (detail and detail ~= '') and ('' .. detail) or ''
table.insert(out_lines, (ok and 'PASS' or 'FAIL') .. ' ' .. name .. suffix)
if ok then pass = pass + 1 else fail = fail + 1 end
end
local function finish()
table.insert(out_lines, '')
table.insert(out_lines, string.format('SUMMARY: %d passed, %d failed', pass, fail))
vim.fn.writefile(out_lines, OUT)
vim.cmd(fail == 0 and 'qall!' or 'cquit!')
end
-- Open the seeded wiki index so the FileType vimwiki autocmd fires.
vim.cmd('edit ' .. vim.fn.fnameescape(WIKI .. '/index.wiki'))
if MODE == 'optout' then
-- use_calendar = false: setup() must not register the wiring autocmd, so
-- calendar-vim's own defaults (set by the stub) survive untouched.
record(vim.bo.filetype == 'vimwiki', 'optout.ftplugin_attached',
'filetype=' .. vim.bo.filetype)
record(vim.g.calendar_action ~= 'vimwiki#diary#calendar_action',
'optout.action_not_wired', 'g:calendar_action=' .. tostring(vim.g.calendar_action))
record(vim.g.calendar_sign ~= 'vimwiki#diary#calendar_sign',
'optout.sign_not_wired', 'g:calendar_sign=' .. tostring(vim.g.calendar_sign))
finish()
return
end
-- ===== Hook wiring overrides calendar-vim defaults =====
record(vim.bo.filetype == 'vimwiki', 'ftplugin_attached',
'filetype=' .. vim.bo.filetype)
record(vim.g.calendar_action == 'vimwiki#diary#calendar_action',
'hook.action_wired', 'g:calendar_action=' .. tostring(vim.g.calendar_action))
record(vim.g.calendar_sign == 'vimwiki#diary#calendar_sign',
'hook.sign_wired', 'g:calendar_sign=' .. tostring(vim.g.calendar_sign))
-- ===== calendar_sign reflects diary-file existence =====
record(vim.fn['nuwiki#diary#calendar_sign'](30, 5, 2026) == '*',
'sign.existing_date_marked',
'got ' .. tostring(vim.fn['nuwiki#diary#calendar_sign'](30, 5, 2026)))
record(vim.fn['nuwiki#diary#calendar_sign'](31, 5, 2026) == '',
'sign.absent_date_unmarked',
'got ' .. tostring(vim.fn['nuwiki#diary#calendar_sign'](31, 5, 2026)))
-- ===== calendar_action: path + window-close (real window API) =====
-- Open a second window to act as the calendar, invoke from it, and confirm
-- the diary lands at WIKI/diary/<date>.wiki while the calendar window closes.
vim.cmd('new')
local cal_win = vim.api.nvim_get_current_win()
local wins_before = #vim.api.nvim_list_wins()
vim.fn['nuwiki#diary#calendar_action'](31, 5, 2026, 0, '')
local opened = vim.fn.expand('%:p')
local want = WIKI .. '/diary/2026-05-31.wiki'
record(opened == want, 'action.opens_wiki_diary_path', 'opened=' .. opened)
record(not opened:match(vim.fn.expand('~') .. '/diary'),
'action.not_home_diary', 'opened=' .. opened)
record(not vim.api.nvim_win_is_valid(cal_win),
'action.closes_calendar_window',
'cal_win valid=' .. tostring(vim.api.nvim_win_is_valid(cal_win)))
record(#vim.api.nvim_list_wins() == wins_before - 1,
'action.window_count_decremented',
'before=' .. wins_before .. ' after=' .. #vim.api.nvim_list_wins())
record(vim.bo.filetype == 'vimwiki', 'action.sets_filetype',
'filetype=' .. vim.bo.filetype)
finish()
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
#
# development/tests/test-calendar.sh — calendar-vim integration harness (Neovim).
#
# Drives development/tests/test-calendar.lua under headless Neovim to cover the
# pieces specific to the Lua entry point:
# * require('nuwiki').setup() wiring g:calendar_action / g:calendar_sign and
# overriding calendar-vim's own defaults,
# * calendar_action's window-close behaviour under the real window API,
# * opting out with use_calendar = false.
#
# A stub calendar-vim on the runtimepath provides :Calendar and seeds the
# default hook names. The LSP binary is built (setup() registers the client);
# the calendar paths themselves don't need the server.
#
# Exit: 0 when both runs are green, 1 otherwise. Skips (0) without nvim.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TMP="$(mktemp -d -t nuwiki-calendar-test-XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
log() { printf '\033[1;34m[calendar]\033[0m %s\n' "$*"; }
if ! command -v nvim >/dev/null 2>&1; then
log 'nvim not installed — skipping'
exit 0
fi
# setup() registers the LSP client, so build the binary like the other
# Neovim harnesses do (incremental, fast).
BIN="$REPO_ROOT/bin/nuwiki-ls"
log 'building nuwiki-ls (release, incremental)…'
cargo build --release -p nuwiki-ls --manifest-path "$REPO_ROOT/Cargo.toml" >/dev/null
mkdir -p "$REPO_ROOT/bin"
ln -sf "$REPO_ROOT/target/release/nuwiki-ls" "$BIN"
# --- Scratch wiki with one seeded diary entry (2026-05-30) ----------------
WIKI="$TMP/wiki"
mkdir -p "$WIKI/diary"
echo "= index =" > "$WIKI/index.wiki"
echo "= 2026-05-30 =" > "$WIKI/diary/2026-05-30.wiki"
# --- Stub calendar-vim ----------------------------------------------------
CAL="$TMP/calendar-vim"
mkdir -p "$CAL/plugin" "$CAL/autoload"
cat > "$CAL/plugin/calendar.vim" <<'EOF'
if exists('g:loaded_calendar_stub') | finish | endif
let g:loaded_calendar_stub = 1
let g:calendar_action = get(g:, 'calendar_action', 'calendar#diary')
let g:calendar_sign = get(g:, 'calendar_sign', 'calendar#sign')
command! -nargs=* Calendar echo 'stub calendar'
EOF
cat > "$CAL/autoload/calendar.vim" <<'EOF'
function! calendar#open(...) abort
endfunction
function! calendar#diary(...) abort
endfunction
function! calendar#sign(...) abort
return 0
endfunction
EOF
write_init() {
local file="$1" use_calendar="$2"
cat > "$file" <<EOF
vim.opt.runtimepath:prepend('$REPO_ROOT')
vim.opt.runtimepath:append('$CAL')
vim.g.nuwiki_binary_path = '$BIN'
vim.g.nuwiki_wiki_root = '$WIKI'
vim.g.nuwiki_file_extension = '.wiki'
vim.g.nuwiki_diary_rel_path = 'diary'
require('nuwiki').setup({ wiki_root = '$WIKI', use_calendar = $use_calendar })
EOF
}
run() {
local label="$1" init="$2" mode="$3"
local results="$TMP/results-$label.txt"
log "running $label harness…"
NUWIKI_CALENDAR_RESULTS="$results" \
NUWIKI_CALENDAR_WIKI="$WIKI" \
NUWIKI_CALENDAR_MODE="$mode" \
timeout 60 nvim --clean -u "$init" --headless \
-c "luafile $REPO_ROOT/development/tests/test-calendar.lua" \
>"$TMP/$label.log" 2>&1 || true
if [[ ! -f "$results" ]]; then
echo "calendar $label harness produced no output" >&2
echo "--- nvim log ---" >&2
cat "$TMP/$label.log" >&2 || true
return 1
fi
cat "$results"
grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$results"
}
INIT_ON="$TMP/init-on.lua"
INIT_OFF="$TMP/init-off.lua"
write_init "$INIT_ON" 'true'
write_init "$INIT_OFF" 'false'
rc=0
run 'default' "$INIT_ON" 'default' || rc=1
run 'optout' "$INIT_OFF" 'optout' || rc=1
exit $rc
+6 -1
View File
@@ -611,7 +611,6 @@ audits don't re-flag them.
backed by LSP foldingRange. backed by LSP foldingRange.
- `key_mappings` (dict) — replaced by Lua `mappings.<group>` + the - `key_mappings` (dict) — replaced by Lua `mappings.<group>` + the
`g:nuwiki_no_<group>_mappings` globals. `g:nuwiki_no_<group>_mappings` globals.
- `use_calendar` — no calendar.vim integration.
- `auto_tags` (upstream `0`) — upstream auto-updates an on-disk tag *metadata* - `auto_tags` (upstream `0`) — upstream auto-updates an on-disk tag *metadata*
file on save. nuwiki has no such file: the LSP re-indexes tags on every file on save. nuwiki has no such file: the LSP re-indexes tags on every
`didChange`, so tag search/jump/completion are always fresh. The setting's `didChange`, so tag search/jump/completion are always fresh. The setting's
@@ -708,3 +707,9 @@ audits don't re-flag them.
fallback, now via `table_align_or_cmd`); `:VimwikiSearch`/`VWS` scoped to the fallback, now via `table_align_or_cmd`); `:VimwikiSearch`/`VWS` scoped to the
wiki root; and global `Index`/`TabIndex` entry points. All client-side (no wiki root; and global `Index`/`TabIndex` entry points. All client-side (no
server change). Both harnesses green (Neovim 299, Vim 291/18/21). server change). Both harnesses green (Neovim 299, Vim 291/18/21).
- Merged the `calendar-support` branch (2026-06-03): calendar-vim integration
now wires `g:calendar_action`/`g:calendar_sign` to nuwiki's diary paths via a
`FileType vimwiki` autocmd (`plugin/nuwiki.vim`, both clients), with a
`use_calendar` opt-out (`g:nuwiki_no_calendar` / setup `use_calendar = false`).
So `use_calendar` is no longer an intentional divergence — removed from that
list above. See `development/calendar-vim-integration.md`.
+6
View File
@@ -76,6 +76,12 @@ M.defaults = {
diagnostic = { diagnostic = {
link_severity = 'warn', link_severity = 'warn',
}, },
-- Calendar-vim integration. On by default — set to `false` to opt out.
-- When on, nuwiki detects calendar-vim on the runtimepath and wires
-- `g:calendar_action` / `g:calendar_sign` automatically.
-- Also respects `g:nuwiki_no_calendar` set before setup() to disable.
use_calendar = true,
} }
M.options = vim.deepcopy(M.defaults) M.options = vim.deepcopy(M.defaults)
+7
View File
@@ -106,6 +106,13 @@ function M.setup(opts)
vim.filetype.add({ extension = extensions }) vim.filetype.add({ extension = extensions })
end end
-- Calendar-vim wiring lives in plugin/nuwiki.vim's FileType autocmd, which
-- runs for both Vim and Neovim. Translate the Lua-side opt-out into the
-- g: flag that autocmd reads so `use_calendar = false` fully disables it.
if config.options.use_calendar == false then
vim.g.nuwiki_no_calendar = 1
end
lsp.register() lsp.register()
_setup_global_mappings() _setup_global_mappings()
_setup_global_commands() _setup_global_commands()
+24
View File
@@ -33,6 +33,30 @@ else
command! -nargs=0 NuwikiInstall execute 'source' fnameescape(s:plugin_root . '/scripts/download_bin.vim') command! -nargs=0 NuwikiInstall execute 'source' fnameescape(s:plugin_root . '/scripts/download_bin.vim')
endif endif
" Calendar-Vim Integration — auto-detect when calendar-vim is loaded.
augroup nuwiki_calendar_detect
autocmd!
autocmd FileType vimwiki call s:nuwiki_setup_calendar()
augroup END
function! s:nuwiki_setup_calendar() abort
if !get(g:, 'nuwiki_use_calendar', 1) || get(g:, 'nuwiki_no_calendar', 0)
return
endif
if exists('*calendar#open') || exists(':Calendar') == 2
" Wire nuwiki's diary hooks unless the user set a custom (non-default) one.
" calendar-vim's own default is 'calendar#diary' (writes to ~/diary); we
" overwrite that so paths come from the wiki root config instead.
let l:act = get(g:, 'calendar_action', '')
if l:act ==# '' || l:act ==# 'calendar#diary'
let g:calendar_action = 'vimwiki#diary#calendar_action'
endif
let l:sign = get(g:, 'calendar_sign', '')
if l:sign ==# '' || l:sign ==# 'calendar#sign'
let g:calendar_sign = 'vimwiki#diary#calendar_sign'
endif
endif
endfunction
if has('nvim') if has('nvim')
" Neovim path is initialised by `require('nuwiki').setup()`. Nothing else " Neovim path is initialised by `require('nuwiki').setup()`. Nothing else
" to do at vimscript boot — wait until the user runs setup. " to do at vimscript boot — wait until the user runs setup.