diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 2eed90b..2ce8798 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -100,3 +100,7 @@ jobs: run: ./development/tests/test-config.sh - name: run Vim config parity harness 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 diff --git a/autoload/nuwiki/diary.vim b/autoload/nuwiki/diary.vim new file mode 100644 index 0000000..f0c2cbd --- /dev/null +++ b/autoload/nuwiki/diary.vim @@ -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 diff --git a/autoload/vimwiki/diary.vim b/autoload/vimwiki/diary.vim new file mode 100644 index 0000000..e554c7e --- /dev/null +++ b/autoload/vimwiki/diary.vim @@ -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 diff --git a/development/ONBOARDING.md b/development/ONBOARDING.md index c9370cb..1909afd 100644 --- a/development/ONBOARDING.md +++ b/development/ONBOARDING.md @@ -79,7 +79,12 @@ nuwiki/ │ ├── test-keymaps.sh # Neovim keymap harness │ ├── test-keymaps.lua │ ├── 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/ └── workflows/ diff --git a/development/_common.sh b/development/_common.sh index 71b2011..59332f9 100644 --- a/development/_common.sh +++ b/development/_common.sh @@ -290,6 +290,20 @@ reset_dirs() { 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() { if [[ "${NUWIKI_DEV_NO_SEED:-0}" == "1" ]]; then log "NUWIKI_DEV_NO_SEED=1 — skipping wiki seed (using $WIKI_DIR as-is)" diff --git a/development/calendar-vim-integration.md b/development/calendar-vim-integration.md index 7eb711a..af929cc 100644 --- a/development/calendar-vim-integration.md +++ b/development/calendar-vim-integration.md @@ -4,10 +4,15 @@ This document describes how nuwiki (vimwiki) integrates with calendar-vim to provide graphical calendar navigation for wiki diary entries. ## 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_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 1. User executes `:Calendar` command 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 `` on selected date 5. `vimwiki#diary#calendar_action` function executes: - Formats date as YYYY-MM-DD - - Opens existing diary file or creates new one - - Applies appropriate wiki filetype - - Sets up autocmd to refresh calendar on file close + - Opens existing diary file or creates new one (split/vsplit per `dir`) + - Applies the vimwiki filetype to a brand-new entry + - Closes the calendar window once the diary entry is open 6. User views/edits diary entry for selected date ## 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 - Visual indicators for dates with existing content - Proper window splitting based on calendar orientation -- Automatic calendar refresh after editing diary entries +- Calendar window closed automatically once the diary entry opens ## Configuration Controlled by: -- `g:vimwiki_use_calendar` (global toggle) -- Individual wiki configurations in `g:vimwiki_list` (path, diary_rel_path, ext) +- `use_calendar` Lua option (Neovim `setup()`), default `true` +- `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 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. \ No newline at end of file diff --git a/development/start-nvim.sh b/development/start-nvim.sh index 6963e04..e7f9ee4 100755 --- a/development/start-nvim.sh +++ b/development/start-nvim.sh @@ -54,17 +54,13 @@ vim.env.XDG_DATA_HOME = '$DATA_DIR' vim.env.XDG_CONFIG_HOME = '$CONFIG_DIR' vim.opt.runtimepath:prepend('$REPO_ROOT') +vim.opt.runtimepath:append('$DEV_DIR/calendar-vim') vim.g.nuwiki_binary_path = '$REPO_ROOT/bin/nuwiki-ls' - --- Friendly defaults for an interactive smoke test. -vim.opt.number = true -vim.opt.signcolumn = 'yes' -vim.opt.termguicolors = true -vim.opt.swapfile = false -vim.opt.writebackup = false -vim.opt.undofile = false -vim.g.mapleader = ' ' +vim.g.nuwiki_wiki_root = '$WIKI_DIR' +vim.g.nuwiki_file_extension = '.wiki' +vim.g.nuwiki_syntax = 'vimwiki' +vim.g.nuwiki_diary_rel_path = 'diary' require('nuwiki').setup({ wiki_root = '$WIKI_DIR', @@ -74,13 +70,14 @@ require('nuwiki').setup({ -- they're swallowed; with it they land in :messages. 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') EOF } main() { ensure_binary + ensure_calendar_vim seed_wiki write_init reset_dirs "$STATE_DIR" "$DATA_DIR" "$CONFIG_DIR" diff --git a/development/start-vim-coc.sh b/development/start-vim-coc.sh index a6dd59c..a72b0d2 100755 --- a/development/start-vim-coc.sh +++ b/development/start-vim-coc.sh @@ -114,25 +114,27 @@ write_vimrc() { " Confine state to the dev cache so we never touch the user's real Vim " install. -set viminfo='100,n$VIM_STATE/viminfo -let &directory = '$VIM_STATE/swap//' -let &backupdir = '$VIM_STATE/backup//' -let &undodir = '$VIM_STATE/undo//' +set viminfo='100,n${VIM_STATE}/viminfo +let &directory = '${VIM_STATE}/swap//' +let &backupdir = '${VIM_STATE}/backup//' +let &undodir = '${VIM_STATE}/undo//' " Make Vim look for runtime paths in the dev cache. vim-lsp is intentionally " absent so nuwiki dispatches jump-to-definition through coc. set nocompatible -let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath -let &runtimepath .= ',$COC_DIR' +let &runtimepath = '${REPO_ROOT}' . ',' . &runtimepath +let &runtimepath .= ',${COC_DIR}' +let &runtimepath .= ',${DEV_DIR}/calendar-vim' " Point coc at the generated settings + a private data dir. -let g:coc_config_home = '$DEV_DIR' -let g:coc_data_home = '$COC_DATA' +let g:coc_config_home = '${DEV_DIR}' +let g:coc_data_home = '${COC_DATA}' -let g:nuwiki_binary_path = '$REPO_ROOT/bin/nuwiki-ls' -let g:nuwiki_wiki_root = '$WIKI_DIR' +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_diary_rel_path = 'diary' let g:nuwiki_log_level = 'info' filetype plugin indent on @@ -153,11 +155,8 @@ augroup NuwikiFtdetect autocmd BufRead,BufNewFile *.wiki setfiletype vimwiki augroup END -" coc starts the server automatically from coc-settings.json once a vimwiki -" buffer loads — no explicit nuwiki#lsp#start() needed. - -echomsg '[nuwiki-dev] ready (coc.nvim) — wiki root: $WIKI_DIR' -echomsg '[nuwiki-dev] coc.preferences.jumpCommand = $COC_JUMP' +echo +echomsg '[nuwiki-dev] coc.preferences.jumpCommand = ' . '\$COC_JUMP' echomsg '[nuwiki-dev] on a [[link]] should open in the CURRENT window' echomsg '[nuwiki-dev] :CocList services for status, :messages for log' EOF @@ -167,6 +166,7 @@ main() { ensure_node ensure_binary ensure_coc + ensure_calendar_vim write_coc_settings seed_wiki write_vimrc diff --git a/development/start-vim.sh b/development/start-vim.sh index cc32d28..1311d21 100755 --- a/development/start-vim.sh +++ b/development/start-vim.sh @@ -70,19 +70,20 @@ write_vimrc() { " Confine state to the dev cache so we never touch the user's real Vim " install. -set viminfo='100,n$VIM_STATE/viminfo -let &directory = '$VIM_STATE/swap//' -let &backupdir = '$VIM_STATE/backup//' -let &undodir = '$VIM_STATE/undo//' +set viminfo='100,n${VIM_STATE}/viminfo +let &directory = '${VIM_STATE}/swap//' +let &backupdir = '${VIM_STATE}/backup//' +let &undodir = '${VIM_STATE}/undo//' " Make Vim look for runtime paths in the dev cache. set nocompatible -let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath -let &runtimepath .= ',$VIM_LSP_DIR' -let &runtimepath .= ',$ASYNC_DIR' +let &runtimepath = '${REPO_ROOT}' . ',' . &runtimepath +let &runtimepath .= ',${VIM_LSP_DIR}' +let &runtimepath .= ',${ASYNC_DIR}' +let &runtimepath .= ',${DEV_DIR}/calendar-vim' -let g:nuwiki_binary_path = '$REPO_ROOT/bin/nuwiki-ls' -let g:nuwiki_wiki_root = '$WIKI_DIR' +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_log_level = 'info' @@ -113,8 +114,7 @@ augroup NuwikiStart autocmd FileType vimwiki call nuwiki#lsp#start() augroup END -" Status messages go to :messages so they don't trigger "Press ENTER". -echomsg '[nuwiki-dev] ready — wiki root: $WIKI_DIR' +echomsg '[nuwiki-dev] ready — wiki root: ' . g:nuwiki_wiki_root echomsg '[nuwiki-dev] :LspStatus for diagnostics, :messages for log' EOF } @@ -122,6 +122,7 @@ EOF main() { ensure_binary ensure_vim_lsp + ensure_calendar_vim seed_wiki write_vimrc reset_dirs "$VIM_STATE" diff --git a/development/tests/test-calendar-vim-optout.vim b/development/tests/test-calendar-vim-optout.vim new file mode 100644 index 0000000..07340d1 --- /dev/null +++ b/development/tests/test-calendar-vim-optout.vim @@ -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!') diff --git a/development/tests/test-calendar-vim.sh b/development/tests/test-calendar-vim.sh new file mode 100755 index 0000000..455f479 --- /dev/null +++ b/development/tests/test-calendar-vim.sh @@ -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" "$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" <