aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* perf: cache modeline VC data per bufferCraig Jennings2026-05-032-23/+196
| | | | | | | | | | | | The custom modeline's VC `:eval` form was calling `vc-backend`, `vc-working-revision`, `vc-git--symbolic-ref`, and `vc-state` on every redisplay. Mode-line eval runs every keystroke. For a large git repo or a TRAMP buffer over SSH, the round-trip cost shows up as visible input lag. I split the inline form into helpers and added a buffer-local cache. `cj/modeline-vc-info` returns the cached plist when its TTL hasn't expired and the cache key still matches. The TTL defaults to 5 seconds via `cj/modeline-vc-cache-ttl`. Save and revert hooks invalidate the cache so the user sees state changes promptly. The render path (`cj/modeline-vc-render`) is now a separate function so it can be tested without touching VC at all. Remote files are skipped by default. `cj/modeline-vc-show-remote` opts back in for cases where TRAMP VC is fast enough to be worth it. Measured on this repo: uncached reads were about 2.4 ms each, cached reads were about 0.0025 ms each, and remote-skipped reads pay only the cheap `file-remote-p` check. I added five tests in `tests/test-modeline-config-vc-cache.el`: cache reuse within TTL (backend called once for two reads), refresh after TTL expiry (called twice), remote-file bypass (no backend call, nil result), cache clear (buffer-locals reset to nil), and render output (branch text + face metadata preserved).
* chore: gitignore Emacs backup, auto-save, and lock filesCraig Jennings2026-05-031-0/+5
|
* refactor: invoke git via argv in coverage diff helpersCraig Jennings2026-05-033-25/+96
| | | | | | | | | | | | `coverage-core.el` was running git through `shell-command-to-string`, which has two practical problems for central tooling: shell parsing surfaces (especially the `$(git merge-base ...)` substitution), and silent failure modes when git exits non-zero (the bad output just becomes empty parse results). I extracted three small helpers. `cj/--coverage-git-string` runs git via `process-file` against a temp buffer and signals `user-error` on non-zero exit, with the argv, status, and trimmed output included. `cj/--coverage-git-merge-base` does its own `git merge-base HEAD <base>` invocation. `cj/--coverage-git-diff` is the diff wrapper that always appends `--unified=0`. `cj/--coverage-changed-lines` now uses `pcase` over the scope symbol and composes the helpers. Branch-vs-main and branch-vs-parent compute the merge-base in a separate call before running `git diff <merge-base>..HEAD`, with no shell substitution involved. One behavior change is worth flagging. A git failure used to disappear into an empty hash table. It now signals a `user-error` with the failing command, exit status, and git's stderr output. Tests: I added two argv-boundary cases (working-tree and branch-vs-parent both assert the exact argv list seen) plus a non-zero-exit case that asserts the user-error path. The existing `test-coverage-core--command.el` smoke test gets its `shell-command-to-string` stub upgraded to a `process-file` stub.
* fix: use buffer-file-name for C single-file compile commandCraig Jennings2026-05-031-3/+11
| | | | | | | | The fallback compile command in `cj/c-compile-command` was building paths from `(buffer-name)`. That broke for renamed buffers, uniquified names like `foo.c<2>`, and files outside `default-directory`. The buffer name is a display label, not a path, so `gcc -o name name` would compile (or fail to compile) the wrong target whenever the two diverged. I extracted `cj/c--single-file-compile-command` that takes the source path explicitly, shell-quotes both source and output paths, and signals a clear `user-error` for non-file buffers. The fallback now passes `buffer-file-name` instead of `(buffer-name)`. Tests for this helper landed in commit f619cbf alongside other prog-c coverage work.
* test: cover C mode hooks and project compile branchesCraig Jennings2026-05-033-0/+162
| | | | | | | | | | | | | | I added 7 new tests across 3 files, filling coverage gaps in `prog-c.el`. Two functions were untested (`cj/c-mode-settings`, `cj/c-mode-keybindings`) and `cj/c-compile-command` only had its single-file fallback covered. `cj/c-compile-command` now has the Makefile and CMake branches tested, plus a Boundary case for a Makefile path with spaces being shell-quoted in the `cd` target. I added these to the existing `test-prog-c-compile-command.el` since the helper and dispatcher already lived there. `cj/c-mode-settings` gets three tests. One covers the buffer-local invariants (`indent-tabs-mode` nil, `c-basic-offset` 4, `tab-width` 4, `fill-column` 80, `comment-auto-fill-only-comments` t). The other two cover the LSP branch: `lsp-deferred` runs when the function is fbound and `executable-find` returns a clangd path, and skips when clangd is missing. `cj/c-mode-keybindings` gets one test asserting S-F5 binds to `cj/disabled` and S-F6 binds to `gdb` in the buffer's local keymap. No realistic Boundary or Error cases for installing two static bindings, so the single Normal case carries it. I stubbed `auto-fill-mode`, `electric-pair-mode`, `lsp-deferred`, `executable-find`, and `locate-dominating-file` at the boundaries via `cl-letf`. Buffer-local state was exercised real in `with-temp-buffer`. 12 prog-c tests pass together: 5 existing plus 7 new.
* fix: make test scratch paths sandbox-friendlyCraig Jennings2026-05-033-13/+82
| | | | | | | | | | | | `tests/testutil-general.el` hard-coded `~/.temp-emacs-tests/` as the test root. That worked locally but blew up under sandboxed `make` runs and CI environments that can't write outside the repo or `/tmp`. A clean sandbox `make test` run reported 32 failing test files purely from the home-directory write attempt, even though the same suite passed when run with normal write permission. I rewrote `cj/test-base-dir` to honor `CJ_EMACS_TEST_DIR` if set, otherwise create a unique directory under `temporary-file-directory` via `make-temp-file`. So sandbox and CI paths just work, and a stable local debug root is still one env-var away. I also tightened the path-containment checks. The old `(string-prefix-p base fullpath)` was a textual hack. Relative paths and weird trailing slashes could fool it. I extracted `cj/test--assert-inside-base` using `file-in-directory-p`, which is the proper API. While I was there, I added `cj/test--safe-base-dir-p` so `cj/delete-test-base-dir` refuses to recursively wipe `/`, `~/`, `temporary-file-directory`, `user-emacs-directory`, `default-directory`, or any path of length under six characters. That guards against an env-var typo or a misaligned `let` binding accidentally deleting something important. I updated the Makefile's `clean-tests` target to nuke the new `$TMPDIR/cj-emacs-tests-*` pattern plus an explicit `CJ_EMACS_TEST_DIR` (if set) and the legacy `~/.temp-emacs-tests` directory. I added `tests/test-testutil-general.el` with five tests: default base lives under `temporary-file-directory`, env override resolves correctly, parent-escape paths are rejected, broad roots are refused for deletion, and a specific selected root is cleaned cleanly.
* fix: validate mail transport executables and default debug offCraig Jennings2026-05-032-6/+146
| | | | | | | | | | `mail-config.el` had three related issues. SMTP transport debug was hard-coded to t, which is sensitive since mail bodies and headers land in debug buffers. The use-package `:config` was also setting `sendmail-program` and `mu4e-get-mail-command` directly from `executable-find` results. So a host without msmtp or mbsync silently got `nil` or `(concat nil " -a")` instead of a clear failure mode. I added `cj/smtpmail-debug-enabled` (default nil) plus `cj/set-smtpmail-debug` and `cj/toggle-smtpmail-debug` for temporary troubleshooting, mirroring the pattern from `auth-config.el`. I extracted `cj/mail--executable-or-warn` so a missing program emits a one-time `display-warning` and returns nil. `cj/mail-configure-smtpmail` and `cj/mail--mbsync-command` both use it. Missing msmtp now leaves `sendmail-program` nil with a warning. Missing mbsync produces a nil sync command instead of the broken `(concat nil " -a")` string. I also wrapped the mbsync executable path in `shell-quote-argument` so unusual install paths don't fall apart on the `" -a"` concat. I added `tests/test-mail-config-transport.el` with seven tests across Normal / Boundary / Error: debug-default-off, toggle wiring, msmtp present and missing, mbsync present, mbsync path with spaces, and mbsync missing. The `test-mail-config--with-executables` macro stubs `executable-find` from an alist so each test names its own environment.
* fix: shell-quote F6 test-runner command argumentsCraig Jennings2026-05-032-5/+48
| | | | | | | | | | `cj/--f6-test-runner-cmd-for` was building shell command strings with raw paths and stems via `format`. For ordinary names (`tests/test_foo.py`, `pkg/foo`) that worked fine. But a path with spaces or a stem with shell metacharacters would break or misbehave once the string hit `compile`. A Python test file under `dir with spaces/` would get tokenized as separate arguments. I added `cj/--f6-shell-quote-argument` that escapes only when the argument doesn't match `cj/--f6-shell-safe-argument-regexp` (alphanumerics, slash, dot, dash, plus a small handful of safe punctuation). Ordinary paths skip the quoter and stay readable. Risky paths route through `shell-quote-argument`. I wrapped the four interpolations in the test-runner builder: the elisp `FILE=` basename, the elisp `TEST=^test-stem-` regex, both pytest paths, and the Go `./rel-dir`. The Go branch also handles an empty rel-dir explicitly so the result stays `go test ./` instead of constructing `./` via format with an empty string. I added three boundary tests: a Python path with spaces, an elisp stem with `;`, and a Go directory with spaces. Existing tests for ordinary paths continue to pass since the safe regex covers them.
* fix: clarify reset-auth-cache failure messageCraig Jennings2026-05-031-1/+1
| | | | | | | | `cj/reset-auth-cache`'s error path read "Failed to clear gpg-agent cache". A user seeing that warning could reasonably think nothing happened. But at that point the Emacs-side caches (auth-source + EPA file handler) have already been cleared. Only the gpg-agent cache failed. I rewrote the message as "Emacs caches cleared, but failed to clear gpg-agent cache" so the user sees both the partial success and the remaining problem. The error-path test from the previous commit asserts a substring match on "Failed to clear gpg-agent cache", so it still passes after the rewording.
* test: cover auth-config helpers and oauth2 cache fixCraig Jennings2026-05-035-0/+314
| | | | | | | | | | | | I added 13 new tests across 5 files, covering the auth-config functions that lacked tests. Categories follow Normal / Boundary / Error where applicable. `cj/toggle-auth-source-debug` flips state once and back through two toggles. `cj/oauth2-auto--plstore-read-fixed` gets three tests: cache miss reads then caches, cache hit skips `plstore-open` entirely, and the `unwind-protect` runs `plstore-close` even when `plstore-get` signals. `cj/reset-auth-cache` covers no-prefix (skip `shell-command`), with-prefix success, and with-prefix shell failure (Emacs caches still clear). `cj/kill-gpg-agent` covers shell exit 0 and non-zero. `cj/clear-oauth2-auto-cache` covers bound-with-entries, bound-but-empty, and unbound. The unbound test restores the binding via `unwind-protect` so other tests in the same Emacs session don't void-variable. I stubbed every boundary via `cl-letf` (`plstore-open`, `plstore-get`, `plstore-close`, `oauth2-auto--compute-id`, `auth-source-forget-all-cached`, `epa-file-clear-cache`, `shell-command`, `message`, `call-process`) and didn't stub any internal helpers. 15 auth-config tests pass together: 2 existing plus 13 new.
* fix: default auth-source debug logging to disabledCraig Jennings2026-05-032-2/+66
| | | | | | | | | | `auth-config.el` was setting `auth-source-debug` to t at startup. That meant every credential lookup printed verbose context to *Messages*. The flag was useful while debugging GPG flow but not appropriate for steady state, since the same config handles Slack, AI, REST, mail, and transcription credentials. I added a `cj/auth-source-debug-enabled` defcustom (default nil) and wired the use-package block to read its value. For temporary troubleshooting I added two commands: `cj/set-auth-source-debug` (prompted on / off via `y-or-n-p`) and `cj/toggle-auth-source-debug` (M-x convenience). I also scanned the nearby auth callers. The visible failure messages name hosts and logins but don't print secret values directly. So this change closes the practical exposure path without losing useful diagnostics. I added `tests/test-auth-config-debug.el` covering the disabled-by-default invariant and the setter wiring through both public variables.
* fix: close drill capture template source linkCraig Jennings2026-05-032-2/+28
| | | | | | | | | | | | | The "d" drill capture template had a malformed source link with only one closing bracket and a literal `n` where a newline should have been: Source: [[%:link][%:description] nCaptured On: %U So new drill captures wrote a broken link and `nCaptured On:` instead of a clean `Captured On:` line on the next row. I closed the link with the missing `]`, and removed the stray `n` so the line break before "Captured On" is just a real newline. The "f" (PDF drill) template was already correct, so I left it alone. I added `tests/test-org-capture-config-drill-template.el`. It loads the module, picks up the "d" template, and asserts both the closed link and the `\nCaptured On: %U` form, plus negative assertions against the broken pre-fix shapes.
* fix: use file basename when moving buffer + fileCraig Jennings2026-05-032-2/+46
| | | | | | | | `cj/--move-buffer-and-file` was building the destination as `(concat dir "/" (buffer-name))`. If the buffer had been renamed via `M-x rename-buffer`, or uniquified by Emacs with a `<2>` suffix when a second buffer visited the same filename, the move wrote a file with the wrong name on disk. I derived the destination basename from `buffer-file-name` instead, in both the internal helper and the interactive wrapper. The wrapper's overwrite-prompt now also formats the real target filename rather than the buffer name. I added two regression tests: one for a renamed buffer visiting `original.txt`, and one for a `<2>` uniquified buffer with a trailing-slash target directory.
* fix: keep C-; ! as system command prefixCraig Jennings2026-05-032-13/+57
| | | | | | | | The module was binding `cj/system-command-map` under `C-; !`, then a few lines later overwriting the same prefix with `cj/system-command-menu`. The second bind won, so every documented subkey, like `C-; ! r` for reboot and `C-; ! l` for lock, was unreachable. I kept the prefix map and folded the completing-read menu into it at `C-; ! !`. So `C-; !` still opens the prefix, the menu is one extra `!` away, and the single-letter shortcuts work again. I also added which-key labels for every documented subkey so the popup actually says what each one does. I added `tests/test-system-commands-keymap.el`. It asserts the prefix stays mounted and that every binding (`!`, `L`, `r`, `s`, `S`, `l`, `E`, `e`) resolves to the right command.
* fix: set vc-follow-symlinks explicitly to tCraig Jennings2026-05-032-1/+67
| | | | | | | | The line read `(setq-default vc-follow-symlinks)` with no value. That left the variable at nil, so the comment "don't ask to follow symlinks if target is version controlled" was a lie. Opening any version-controlled symlink still prompted. I checked the Emacs docs first. The value `t` is the one that follows the link without asking, so that's what I set. I added `tests/test-system-defaults-vc-follow-symlinks.el` as a regression test. It loads the module with the unrelated side effects stubbed and asserts `vc-follow-symlinks` ends up as `t`.
* fix: expand local ELPA mirror paths with expand-file-nameCraig Jennings2026-05-032-10/+129
| | | | | | | | `(concat user-home-dir ".elpa-mirrors/")` was producing `/home/cjennings.elpa-mirrors/` because `getenv HOME` doesn't return a trailing slash on Linux. The local mirrors were silently dropping out of `package-archives` because `file-accessible-directory-p` couldn't find the bogus path. I replaced the `concat` calls for `elpa-mirror-location` and `localrepo-location` with `expand-file-name`, which handles the slash for us. I also lifted the four per-archive subdirs into their own constants (`elpa-mirror-gnu-location`, `nongnu`, `melpa`, `stable-melpa`) so the archive registration block stops splicing `concat` strings inline. I added `tests/test-early-init-paths.el`. It loads `early-init.el` against a temp HOME with the package side effects stubbed and asserts each constant and each `package-archives` entry resolves to the right path.
* feat(dev-fkeys): revert projectile cache on failed-and-modified compileCraig Jennings2026-05-035-0/+405
| | | | | | | | | | | | | | | | Without this, a one-off typo at projectile's compile/test/run prompt poisons the per-project cache: every subsequent invocation pre-fills the broken value. I hit it during the Phase 2a live-test, where projectile's "All tests" prompt was replaying `go test ../.` and there was no clean way to get the prior known-good back. Three pieces of machinery, all in `dev-fkeys.el`: `cj/--projectile-capture-cmd' captures the current cached cmd at the project root before each invocation, stashing a plist with :map / :root / :prior in `cj/--projectile-revert-state'. `cj/--projectile-revert-on-fail' is a `compilation-finish-functions' hook that reads that state. If the compile failed AND the cmd was modified from the captured prior value AND the prior was non-nil, it puts the prior back in projectile's cmd-map. Test-fails-because-of-real-bug (cmd unchanged through the run) leaves the cache alone. The hook self-removes on first invocation regardless of outcome and clears the state. `cj/--projectile-around-revert' is the around-advice that wires the two together. I added the advice to all three projectile cmd-runners — `projectile-compile-project', `projectile-test-project', `projectile-run-project' — so the auto-revert applies whether the user invoked via F4 / F6 or directly via `M-x'. Plus the manual escape-hatch: `cj/projectile-reset-cmds' clears compile/test/run cache for the current project. Bound to `C-; P' under the personal keymap. Use when projectile's auto-derived default was wrong from the start and you want to start fresh — the next F4 / F6 invocation re-derives projectile's project-type default. TDD: 18 new tests across 4 files, one per helper. The around-advice tests build the capture/install/orig-fn flow against stub cmd-maps and verify state captured, hook installed, orig-fn invoked. The revert hook tests cover failure-and-modified (revert), success (leave alone), failure-but-unchanged (leave alone), nil prior (leave alone), nil state (no-op), and self-removal. The reset-cmds tests cover the all-three-maps clear, no-cached-entry no-op, and no-project user-error.
* feat(dev-fkeys): propagate prefix-arg to projectile through F4 / F6Craig Jennings2026-05-034-4/+51
| | | | | | | | | | | | | I had four call sites passing literal nil to `projectile-compile-project' / `projectile-run-project' / `projectile-test-project'. The literal nil ignored whatever prefix arg the user gave. So `C-u F4 → Compile' or `C-u F6 → All tests' didn't actually force projectile's re-prompt — the prefix arg got swallowed at our wrapper layer. Switched all four to `current-prefix-arg': - `cj/--f4-dispatch' — `compile-only' and `run-only' actions. - `cj/f4-compile-only' — the C-F4 fast path's compiled-project branch. - `cj/f6-test-runner' — the "All tests" menu entry. Use case: when projectile's cached cmd is wrong (typo, stale, or whatever), `C-u' on any of these forces projectile to re-prompt instead of replaying the bad cmd silently. The compile-and-run and clean-rebuild paths still pass nil to their chained projectile calls because those run inside an async `compilation-finish-functions' hook, where `current-prefix-arg' has already reverted to nil. Refining those would need to capture the prefix at entry and thread it through; left for later. TDD: 4 new tests (one per call site) bind `current-prefix-arg' to t and verify projectile receives t. Each test failed against the literal-nil version and passes against `current-prefix-arg'.
* chore(prog-general): disable auto-close of *compilation* windowCraig Jennings2026-05-031-10/+15
| | | | | | I commented out the global `compilation-finish-functions' hook that closed the *compilation* window 1.5 seconds after a successful compile. With the F6 test runner now landing test output in *compilation*, I want the buffer to stay open afterward so I can read the results, not have it slide out from under me. The block stays in the file as a commented-out reference so I can flip it back on later if I want. A prog-mode-only variant is noted in the comment for the day I want the auto-close back for non-prog compiles (org-export, etc.) — that needs advice on `compile' to capture the originating buffer's `major-mode' at compile-start, since the hook fires after `compilation-mode' has already taken over the current buffer. Skipped for now per the simpler path.
* fix(dev-fkeys): F6 elisp runner uses basename, not rel-pathCraig Jennings2026-05-033-7/+13
| | | | | | | | | | I shipped Phase 2a with `cj/--f6-test-runner-cmd-for' building `make test-file FILE=<rel-path>' for elisp test files (e.g., FILE=tests/test-foo.el). The project Makefile prepends `tests/' to FILE itself, so the full invocation expands to `tests/tests/test-foo.el' and emacs reports "Cannot open load file". The bug surfaced on a live-test in step 7 of the Phase 2a smoke plan. Fix: pass `(file-name-nondirectory rel-path)' so the Makefile gets just `test-foo.el' and re-prepends `tests/' itself. Two unit tests in `test-dev-fkeys--f6-test-runner-cmd-for.el' had encoded the wrong expectation (the rel-path form). Two orchestrator tests in `test-dev-fkeys--f6-current-file-tests-impl.el' inherited the same wrong assertion via integration. Updated all four to assert the basename form. Verified: full suite green, including the 4 updated tests. Live re-test on `tests/test-dev-fkeys--f6-language-detect.el' should now produce the working `make test-file FILE=test-dev-fkeys--f6-language-detect.el'.
* feat(dev-fkeys): add F6 test runner menu (Phase 2a)Craig Jennings2026-05-038-12/+798
| | | | | | | | | | | | | | | | | I extended `dev-fkeys.el` with the F6 dispatcher half of the spec. F6 prompts via `completing-read` between two candidates: "All tests" delegates to `projectile-test-project`, and "Current file's tests" detects the buffer's language by extension, derives the runner command, and pipes through `compile' from the projectile root. C-F6 is the fast path straight to "Current file's tests". Per-language coverage: - Elisp source files map to `make test-name TEST=^test-<stem>-`. Elisp test files run with `make test-file FILE=<rel-path>` so a per-helper file like `test-foo--bar.el' runs only its own tests. - Python source files map to `pytest tests/test_<stem>.py'. Python test files run with `pytest <rel-path>'. - Go runs the package containing the file: `go test ./<rel-dir>'. Source and test files use the same command since Go test scope is per-package. Limit: this runs every `_test.go' in the package, not just the buffer's file. Phase 2b can refine via test-name discovery. - TypeScript and JavaScript are detected but punted for v1. The runner-command builder returns nil and the orchestrator signals a user-error rather than guessing. The F6 binding moved from the Phase 1 stopgap (`projectile-test-project') to `cj/f6-test-runner'. C-F6 is newly bound to `cj/f6-current-file-tests'. M-F6 stays unbound, reserved for Phase 2b's "Run a test..." menu entry. TDD: 68 new tests across 7 files. Production code split into small testable internals (`cj/--f6-language-detect', `cj/--f6-buffer-is-test-file-p', `cj/--f6-source-stem', `cj/--f6-test-runner-cmd-for', `cj/--f6-current-file-tests-impl') plus two thin interactive wrappers. Smoke tests confirm bindings register on load. I also updated the module commentary with the Phase 2b plan, the capture-then-filter approach for tree-sitter discovery, and a pointer to Emacs bug #79687. The bug is the predicate-syntax mismatch that breaks `:match' / `:equal' / `:pred' queries on Emacs 30.2 with libtree-sitter 0.26. The fix lives on Emacs master (commit b0143530), targets Emacs 31, and has not been backported to the emacs-30 branch as of today. Phase 2b will use queries without predicates and filter results in Elisp, sidestepping the issue. Mike Olson's `treesit-predicate-rewrite.el' applies the same idea to font-lock if you want it before Phase 2b lands.
* feat(dev-fkeys): add project-aware F4 compile/run dispatcherCraig Jennings2026-05-0317-43/+1166
| | | | | | | | | | | | | | | | | | | I added a new module `modules/dev-fkeys.el` that owns the dev F-key block. F4 prompts via `completing-read` with a candidate set filtered by project type (compiled / interpreted / unknown). C-F4 is the compile-only fast path. M-F4 is clean + rebuild. It runs a heuristic clean command derived from the project markers (go.mod, Cargo.toml, Eask, Makefile, CMakeLists.txt) and chains `projectile-compile-project` on success. S-F4 stays on `recompile` and now lives globally instead of duplicated across prog-general.el and prog-c.el. F6 is bound globally to `projectile-test-project` as a Phase 1 stopgap. Phase 2 replaces it with the polyglot test runner spec'd in todo.org. Project-type detection runs against the projectile root and falls back to `unknown` when no marker matches. Interpreted markers are checked first so a Python or Node project with a Makefile for tasks classifies as interpreted instead of compiled. Compile + Run sequencing uses a one-shot `compilation-finish-functions` hook that self-removes on first invocation and only fires the follow-up when the status string starts with `finished`. Cleanup in the same commit: - Dropped F4/F5/F6 from `prog-general.el`'s prog-mode-hook. They are now global. - Dropped F6→format bindings from prog-c.el / prog-python.el / prog-shell.el. C-; f was already bound in each, so this is pure removal. - Dropped the duplicate S-F4 from prog-c.el. The global binding covers it. - Updated the keybinding header in prog-general.el and the workflow comments in prog-c.el / prog-shell.el. - Wired `(require 'dev-fkeys)` in init.el alongside coverage-core. TDD: 73 tests across 11 files, one per helper. Production code is split into small testable internals (`cj/--detect-project-type`, `cj/--f4-candidates`, `cj/--f4-derive-clean-cmd`, `cj/--f4-make-once-hook`, `cj/--f4-dispatch`, `cj/--f4-compile-and-run-impl`, `cj/--f4-clean-rebuild-impl`, `cj/--f4-project-root`) plus three thin interactive wrappers. Smoke tests confirm bindings register on load. Known limitation: if another `compilation-finish-functions` hook fires between my add-hook and the compile finishing, the chain can fire on the wrong compile. The hook self-removes on first invocation regardless of which compile it sees. Documented in the impl docstring. Acceptable for v1. Phase 2 will replace F6 with the polyglot test runner (tree-sitter queries for Python/Go/TS, sexp scan for Elisp, buffer-local last-test memory).
* test(calendar-sync): fix weekday-string lookup to match productionCraig Jennings2026-05-031-1/+8
| | | | | | | | | | The boundary test for `calendar-sync--expand-weekly` with a 5-element UNTIL built its byday string from `'("SU" "MO" "TU" "WE" "TH" "FR" "SA")`, a 0-indexed Sunday-first array. The production code uses Monday=1, Sunday=7 throughout: `calendar-sync--date-weekday` returns it that way and `calendar-sync--weekday-to-number` expects it. When start-date landed on a Sunday (start-weekday=7), `(nth 7 array)` overran the 7-element list and returned nil. Then byday=(nil), and inside expand-weekly `(mod (- nil current-weekday) 7)` raised "wrong-type-argument number-or-marker-p nil". The mismatch made the test fail every Saturday (when "tomorrow" is Sunday) and pass the other six days. The flake had been blamed on stale `.elc` in earlier triage, but `make clean && make test` reproduced the failure on Saturdays. I switched the lookup to `(nth (1- start-weekday) '("MO" "TU" "WE" "TH" "FR" "SA" "SU"))`, the same convention as every other weekday-mapping in the codebase. I verified across all 7 weekdays via a faked `current-time`: each produces exactly 1 occurrence as expected. Production code is internally consistent. No production change needed.
* fix(line-paragraph): join-line-or-region strands space on next lineCraig Jennings2026-05-033-27/+108
| | | | | | | | | | The region branch's `(while (< (point) end) (join-line 1))` ran one iteration too many. After the final in-region join, point sat just before the end marker, so the loop fired once more. That extra `join-line 1` consumed the next line's preceding newline and replaced it with a space. Then `(goto-char end)` + `(newline)` reinserted a newline at the original end position, before the inserted space, so the space ended up stranded at BOL of the next line. I replaced the position-based loop with `count-lines` + `dotimes` to do exactly the right number of joins. I also swapped the trailing `(newline)` for `(forward-line 1)`. The bullet-list use case now lands directly on the next existing line with no blank gap. The trailing-newline change ripples to `cj/join-paragraph` (which delegates here), so paragraphs now also stop adding a trailing newline when the input lacks one. `require-final-newline` handles file-end discipline on save anyway. I added 3 new tests that fail against the old loop and pass against the fix. I also updated 11 existing tests whose assertions baked in the old trailing-newline behavior. While in there I wrapped the `cj/custom-keymap` defvar stub in `eval-and-compile` in both test files. The bare defvar wasn't evaluated at byte-compile time, so the `require` of `custom-line-paragraph` would hit a void symbol when the validate-el hook ran.
* test(prog): drop ignore-errors on prog-module require, add featurep checkCraig Jennings2026-04-304-4/+27
| | | | | | | | | | | | | | | | | | Two small tightening passes on the formatter wiring tests just shipped. Drops `(ignore-errors ...)` from each test file's `(require 'prog-PKG)' call. Soft use-package warnings (e.g. lsp-pyright not being installed) still emit messages without aborting the load. A hard load failure (syntax error, missing required dep) would now surface as a test error rather than being silently swallowed. Adds a `(should (featurep PKG))' assertion per language so the test output makes "package loaded" visible alongside the fboundp and binding checks. For webdev the assertion is `(featurep 'prog-webdev)' since the formatter command is defined directly in prog-webdev.el (it shells out to the prettier CLI, no separate package to load). 17 tests total now (up from 13), all passing.
* test(prog): cover formatter wiring for python, go, shell, typescriptCraig Jennings2026-04-305-0/+226
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four test files plus a shared testutil that locks in the formatter bindings on C-; f across the four languages. Each test file checks: - the format command is fboundp after the relevant package loads - the C-; f binding resolves to that command (in the relevant mode-map, or in the buffer-local map for hook-based wiring) - the underlying executable is on PATH (skipped via ert-skip if not installed) No production change. The bindings were already at C-; f via two mechanisms. Use-package :bind handles python and shell. The other two install via local-set-key inside a hook. This regression net catches silent breakage if any of those wirings get reshaped later. The shared tests/testutil-format-wiring.el carries format-test--ensure-packages-init, which calls package-initialize once per batch run, since make test runs Emacs with --no-site-file --no-site-lisp. Without it, use-package can't find blacken / shfmt / go-mode in elpa/. Also format-test--skip-unless-executable wraps ert-skip with a clear "not on PATH" message so missing tools fail informatively. Per-language wiring inventory (no changes, just locked in): - Python: blacken-buffer in python-ts-mode-map (use-package :bind) - Shell: shfmt-buffer in sh-mode-map and bash-ts-mode-map (use-package :bind, gated on :if executable-find) - Go: gofmt via cj/go-mode-keybindings hook - TS/JS/Web: cj/webdev-format-buffer via cj/webdev-keybindings hook 13 tests across 4 files, all passing.
* chore(deps): add commented :load-path hints for local-dev swapsCraig Jennings2026-04-304-0/+4
| | | | | | | | | | | | Each :vc-installed package whose source repo is also cloned under ~/code now carries a commented :load-path line directly under the :vc form. Uncomment the :load-path and comment the :vc to flip into local development without rewriting the use-package block. Covered: gloss, org-drill, wttrin (emacs-wttrin), chime. Skipped: org-msg. The previous local clone at ~/code/org-msg is no longer present; if it gets re-cloned later, add the same hint there.
* chore(deps): move remaining packages from :load-path to :vc; drop archived ↵Craig Jennings2026-04-304-222/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | org-gcal The three packages that still loaded from local checkouts now install via :vc: - chime → git@cjennings.net:chime.git (was :load-path "~/code/chime") - wttrin → git@cjennings.net:emacs-wttrin.git (was :load-path "/home/cjennings/code/emacs-wttrin") - org-msg → https://github.com/jeremy-compostella/org-msg (was :load-path "/home/cjennings/code/org-msg"; switching to upstream rather than a fork since the previous fork wasn't carrying any active changes) For the two cjennings.net repos this matches the org-drill (be3e227) and gloss (2e12131) shape: primary on cjennings.net, post-receive hook mirroring to GitHub. The previously-commented :vc URLs in chime and wttrin pointed at GitHub directly, which would have lost the cjennings-first convention if uncommented later. Also drops :ensure nil on chime (only relevant under package.el, not :vc) and removes modules/archived/org-gcal-config.el. Nothing in init.el or any module references org-gcal, so the file is genuinely unused.
* feat(gloss): wire gloss into init via :vc against cjennings.netCraig Jennings2026-04-302-0/+31
| | | | | | | | | | | | Adds modules/gloss-config.el with a use-package form that installs gloss from the cjennings.net bare repo. The bare's post-receive hook mirrors to GitHub, so the package shows up in both places. Eager-loaded so gloss-prefix-map exists at startup. :config calls gloss-install-prefix to bind C-h g. Lands in the "Modules In Test" section of init.el for v1. Can move out after the first-week shakedown shows the package is steady.
* test(config-utilities): cover validate-timestamps and format-report helpersCraig Jennings2026-04-302-0/+190
| | | | | | | | | | | | | | | | | | Two test files covering the extracted timestamp-validation helpers. cj/--validate-timestamps-in-buffer (8 tests): empty buffer no-op, buffer with no timestamps, buffer with all valid timestamps, DEADLINE flagged with "DEADLINE" property, SCHEDULED flagged with "SCHEDULED", inline-timestamp flagged with "inline timestamp", multiple invalid collected in document order, mixed valid+invalid returning only the invalid one. Tests use real org parsing and mock org-time-string-to-absolute at the boundary so an arbitrary timestamp can be marked invalid for a given test. cj/--format-validation-report-section (4 tests): no-entries says "No invalid timestamps found", single-entry produces the file: link + Property/Type + Invalid timestamp lines, multiple-entry preserves input order, every section ends with a trailing blank line.
* fix(config-utilities): repair validate-org-agenda-timestamps property checkCraig Jennings2026-04-301-31/+65
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two bugs in cj/validate-org-agenda-timestamps surfaced while extracting testable helpers. 1. The DEADLINE / SCHEDULED / TIMESTAMP property lookup used (intern (downcase prop)) as the key, producing 'deadline, 'scheduled, 'timestamp. org-element-property expects keywords (:deadline, :scheduled, :timestamp) and returns nil for plain symbols. The property-check branch had never reported anything since the function was written. Only inline-regex matches inside headline contents have ever been flagged. Fixed by building the keyword form: (intern (concat ":" (downcase prop))). 2. Once #1 is fixed, every property timestamp would also match the inline-timestamp regex during the contents scan (since the DEADLINE: / SCHEDULED: / TIMESTAMP lines fall inside contents-begin/end on a parsed headline), producing duplicate reports. Added a per-headline list of property timestamp strings and a member check before pushing an inline match. The function is also restructured into three pieces to make it testable: - cj/--validate-timestamps-in-buffer FILE — pure-ish: walks the current buffer, returns a list of (FILE POS HEAD PROP TS) tuples. - cj/--format-validation-report-section FILE INVALID — pure: returns the per-file org-formatted string. - cj/validate-org-agenda-timestamps (interactive) — orchestrates both helpers across org-agenda-files into a report buffer. The interactive entry-point's behaviour is unchanged from the user's side except that DEADLINE / SCHEDULED / TIMESTAMP property timestamps are now actually checked.
* test(config-utilities): cover cj/--recompile-emacs-homeCraig Jennings2026-04-301-0/+122
| | | | | | | | | | | | Six tests against real temp directories. Mocks only the actual compile invocations (native-compile-async, byte-recompile-directory) so the deletion side runs end-to-end against real files. Covers: native dispatch returns 'native and calls native-compile-async, byte dispatch returns 'byte and calls byte-recompile-directory, recursive deletion of every .elc/.eln (including in subdirs), removal of the eln cache dir on the native path, removal of the elc cache dir on the byte path, and the missing-cache-dir no-op.
* refactor(config-utilities): extract cj/--recompile-emacs-homeCraig Jennings2026-04-301-24/+29
| | | | | | | | | | | | Splits the delete-then-recompile work out of cj/recompile-emacs-home so it takes DIR and an explicit NATIVE-P flag instead of probing boundp inside the work loop. Returns 'native or 'byte to surface which path actually ran. The interactive wrapper still asks `yes-or-no-p' against user-emacs-directory and probes `(boundp 'native-compile-async)' once to decide the dispatch and the prompt's wording. The cancellation message is unchanged.
* test(config-utilities): cover cj/--benchmark-methodCraig Jennings2026-04-301-0/+57
| | | | | | | Four tests against a temporarily fbound test target: runs the symbol once and propagates its return value, raises user-error on a nil symbol, raises user-error naming the symbol when it isn't fboundp, and verifies the with-timer announce/done messages fire.
* refactor(config-utilities): extract cj/--benchmark-methodCraig Jennings2026-04-301-8/+16
| | | | | | | | | | | | Splits the timer dispatch out of cj/benchmark-this-method so it can be tested with a known method symbol instead of going through read-string + completing-read. The interactive wrapper still prompts for both inputs, and now catches the user-error from the internal so the user-facing behaviour on invalid input is unchanged (the message goes to the echo area). Returns the funcall's value to the caller, which is observable through the timer.
* test(config-utilities): cover cj/--delete-compiled-files-in-dirCraig Jennings2026-04-301-0/+91
| | | | | | | | Five tests against real temp directories: mixed .el/.elc/.eln content, recursive descent through subdirectories, no-compiled-files no-op, empty directory, and the suffix-vs-substring boundary that ensures a file like "looks.elc.bak" isn't deleted (string-suffix-p, not string-match).
* refactor(config-utilities): extract cj/--delete-compiled-files-in-dirCraig Jennings2026-04-301-8/+17
| | | | | | | | | | | | Splits the file-walking work out of cj/delete-emacs-home-compiled-files so it takes a directory parameter and returns a count. The interactive wrapper still hardcodes user-emacs-directory and prints the same status messages, just with the count interpolated. The split is scope-aligned with adding tests for the file-walking behaviour. The original function couldn't be tested without spawning files inside user-emacs-directory itself, which would pollute the running config.
* test(org-noter-config): cover preferred-split, slug, template, predicatesCraig Jennings2026-04-304-0/+289
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four new test files for the pure-and-near-pure helpers in org-noter-config.el. The interactive heavyweights (cj/org-noter-start, cj/org-noter-insert-note-dwim, cj/org-noter--toggle-notes-window, cj/org-noter--find-notes-file, cj/org-noter--create-notes-file) are out of scope for this pass — they need org-noter loaded and a real session, or a substantial internal/wrapper split before they can be tested without spinning up real PDFs and EPUBs. - preferred-split: 5 tests walking the 1.4 width-to-height threshold from both sides plus a square-frame boundary. - title-to-slug: 7 tests covering multi-word, single-word, mixed punctuation, leading/trailing junk, collapsed runs, digits, and empty input. - generate-notes-template: 5 tests with org-id-uuid mocked, asserting the rendered template carries the UUID, the dual ROAM_REFS / NOTER_DOCUMENT pointers, the title-and-category lines, the ReadingNotes filetag, and the trailing Notes heading. - predicates: 13 tests covering cj/org-noter--in-document-p, cj/org-noter--in-notes-file-p, cj/org-noter--session-active-p, cj/org-noter--get-document-path, and cj/org-noter--extract-document-title. Mocks derived-mode-p, org-entry-get, and buffer-file-name at the boundary so the suite doesn't require pdf-tools or nov. The require chain in each test file is user-constants → keybindings → org-noter-config because cj/org-noter-notes-directory captures roam-dir from user-constants and the prefix-map binding at the bottom of org-noter-config references cj/custom-keymap. 30 new tests, all passing. Full suite green.
* test(config-utilities): cover with-timer, compile-buffer, summary, info commandsCraig Jennings2026-04-304-0/+286
| | | | | | | | | | | | | | | | | | | | | | | | | | Four new test files extending the existing coverage of cj/emacs-build--format-build-time. The interactive heavyweights (cj/recompile-emacs-home, cj/delete-emacs-home-compiled-files, cj/benchmark-this-method, cj/validate-org-agenda-timestamps) are out of scope for this pass — each needs an internal/wrapper split first before tests can exercise the logic without UI. - with-timer macro: 4 tests asserting it returns the FORMS' value, evaluates the body exactly once, emits both announce and done messages, and returns the last form when given multiple. - cj/compile-this-elisp-buffer: 6 tests dispatching across native-async, native-sync, and byte-compile fallbacks, plus the not-elisp / no-buffer-file-name error paths and the sync-native error catch. - cj/emacs-build--summary-string: 5 tests asserting the shape of the multi-line report (Version, System, Build date, Capabilities section, yes/no flag rendering) without locking exact wording. - info-commands smoke: 5 tests exercising cj/info-emacs-build, cj/info-loaded-packages, cj/info-loaded-features, cj/reload-init-file, and cj/org-alert-list-timers via boundary-mocked pop-to-buffer and load-file, asserting buffer creation, content shape, or echo-area message as appropriate. 20 new tests, all passing. Full suite green.
* test(host-environment): cover laptop/desktop, platform, display, timezone ↵Craig Jennings2026-04-304-0/+345
| | | | | | | | | | | | | | | | | | | | | | | | | | predicates Four new test files extending the existing test-host-environment.el (which already covered the two battery helpers). - platform-predicates: env-linux-p, env-bsd-p, env-macos-p, env-windows-p walked across every supported system-type value. 8 tests. - display-predicates: env-x-p, env-x11-p, env-wayland-p, env-terminal-p, env-gui-p exercised under every relevant combination of window-system, WAYLAND_DISPLAY, and display-graphic-p. 13 tests. - env-laptop-p: composition over the helpers, with Linux dispatch isolated from non-Linux dispatch via system-type binding. 8 tests including env-desktop-p as the inverse. battery-status-function is forward-declared in this test file (initialized to nil) so cl-letf's symbol-value place can read the prior value without hitting void-variable. - detect-system-timezone: the four-method priority chain. Mocks cj/match-localtime-to-zoneinfo and getenv at the boundary; uses cl-letf on file-exists-p / insert-file-contents to exercise the /etc/timezone fall-through without touching real system files. 5 tests. 34 new tests for host-environment, all passing. Full suite green.
* fix(host-environment): correct docstring order in cj/detect-system-timezoneCraig Jennings2026-04-301-2/+2
| | | | | | | | | The numbered list in the docstring had file-comparison and TZ env var swapped relative to what the code does. The code tries cj/match-localtime-to-zoneinfo first, then falls back to TZ. Updated the docstring so the numbering matches the actual `or' chain. Surfaced while writing tests for the priority chain.
* test(keybindings): cover cj/jump-open-var and the jump-commands wiringCraig Jennings2026-04-302-0/+142
| | | | | | | | | | | | | | | | | | | Two test files for keybindings.el. cj/jump-open-var gets full N/B/E coverage (6 tests): existing-file happy path, plus error paths for unbound symbol, nil value, non-string value, empty string, and missing file. The smoke file for the auto-generated cj/jump-to-NAME commands asserts that each spec entry has an fbound command, that the command is bound in cj/jump-map at the spec's key, that calling each command invokes cj/jump-open-var with the spec's var, and that cj/jump-map is mounted under cj/custom-keymap at "j". The test fixture variable is declared at top level. If it were let-bound inside a test under lexical-binding, the let would create a lexical binding that shadows the dynamic one. The production code's symbol-value would then miss what setq writes. find-file is mocked at the boundary so the existing-file test doesn't actually open a buffer. 10 tests pass. No production change in keybindings.el.
* chore(deps): point org-drill :vc at cjennings.net primaryCraig Jennings2026-04-291-1/+1
| | | | The cjennings.net bare for org-drill has a GitHub mirror as of today's earlier remote migration. Update the :vc URL in modules/org-drill-config.el to point at the primary instead of the mirror so a fresh install clones from the source-of-truth.
* fix(eshell): correct call shape in eshell/find-using-diredCraig Jennings2026-04-291-2/+2
| | | | The body had `(find-name-dired . escaped-pattern)`, a dotted pair instead of a function call. The reader accepts it, but the form crashes the moment the `f` alias runs. find-name-dired takes (DIR PATTERN), so the right shape passes default-directory and the escaped pattern.
* docs(design): add gloss package design docCraig Jennings2026-04-291-0/+316
| | | | Captures the v1 design for the gloss Emacs package: layered five-module split, Wiktionary REST as the online source, side-buffer picker for ambiguous terms, libxml HTML strip, mtime-based cache invalidation. The implementation is a separate repo, but the design work happened in this tree, so the doc lives alongside the other design archives here.
* chore(hooks): skip out-of-tree .el files in validate-el.shCraig Jennings2026-04-291-0/+7
| | | | This PostToolUse hook validates .el edits via check-parens and byte-compile-file. It was firing on edits to files outside the project root too, which meant byte-compiling without the foreign package's load-path set up and leaving .elc droppings in the wrong tree. Added a four-line PROJECT_ROOT guard so out-of-tree files exit silently.
* feat(mail): add work account and reorganize C-; e bindingsCraig Jennings2026-04-271-29/+70
| | | | | | | | | | Adds a third mu4e context for a work email account. Reorganizes cj/email-map under C-; e: attach (A) and delete (D) move to uppercase to free c, d, g as account submaps. Each submap has i/u/s/l for inbox/unread/starred/large. Trims mu4e-bookmarks to one unread query per account on b c, b g, b d. The full grid lives under C-; e. mbsync and msmtp config for the new account lives in a separate dotfiles repo.
* fix(mail): default cj/custom-keymap so the file byte-compiles standaloneCraig Jennings2026-04-271-0/+8
| | | | | | | | | | | | cj/custom-keymap is defined in keybindings.el, which init.el loads before mail-config.el. The use-package org-msg :preface block calls keymap-set on it, and use-package wraps :preface in eval-and-compile. So byte-compiling mail-config.el on its own tries to call keymap-set when cj/custom-keymap is still void. Wrapping a defvar with a make-sparse-keymap default in eval-and-compile gives the symbol a value during compilation. At runtime keybindings.el has already populated the real keymap, so defvar does nothing.
* docs(design): add debug-profiling.el module brainstorm outputCraig Jennings2026-04-261-0/+203
| | | | | | | | | | Captures the agreed v1 shape for a new =debug-profiling.el= module: targeted slow-command investigation, two features ("profile next command" and "time region or sexp"), each split into pure helper plus interactive wrapper. Migrates the existing =profiler-*= bindings and =cj/benchmark-this-method= out of =config-utilities.el=. Stays on the existing =C-c d= debug umbrella prefix. Six approaches were considered: three conventional, plus three tail samples (macro-first, log-and-grep, treesit picker). Recommendation is the boring named-operation surface backed by a thin wrapper over the built-in =profiler.el= and =benchmark.el=. The other five options are recorded with reasons-rejected so a future reader can see what was weighed. Design covers architecture, data flow, error handling, testing approach, and observability. Two open questions are parked: default REPS for =cj/time--expr=, and whether to capture =cpu+mem= or just =cpu=. Both are fine to defer until v1 has been used on the queued org-capture target-building investigation. Implementation will run via =/start-work= against this design.
* feat(lsp): add common build/cache dirs to file-watch ignore listCraig Jennings2026-04-262-1/+125
| | | | | | | | | | Extends `lsp-file-watch-ignored-directories' with thirteen build, cache, and tooling directories: `node_modules', `dist', `coverage', `target', `__pycache__', `.venv', `venv', `.pytest_cache', `.mypy_cache', `.ruff_cache', `test-results', `playwright-report', `tf/.terraform'. Uses `add-to-list', so lsp-mode's own defaults (`.git', `.svn', `.idea', etc.) stay in place. Setting these in a project's `.dir-locals.el' doesn't work. lsp-mode reads `lsp-file-watch-ignored-directories' once at workspace init, from the global value, so a buffer-local override never reaches the watch list. I confirmed this today: in a Python buffer where dir-locals had applied, `M-: lsp-file-watch-ignored-directories' returned the lsp-mode default, not the project's overrides. Setting it globally is what works. The goal is to push typical workspaces under `lsp-file-watch-threshold' (1000), so the "watch all files? (y or n)" prompt stops firing on every fresh LSP start. Also added a forward defvar for `lsp-enable-remote' to silence the matching free-variable warning under `make compile'.