diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | docs/subr-mock-migration-spec.org | 158 | ||||
| -rw-r--r-- | modules/custom-buffer-file.el | 280 | ||||
| -rw-r--r-- | scripts/theme-studio/WIP.json | 136 | ||||
| -rw-r--r-- | tests/test-custom-buffer-file--buffer-differs-prompt.el | 197 | ||||
| -rw-r--r-- | tests/test-custom-buffer-file--diff-whitespace-only.el | 146 | ||||
| -rw-r--r-- | tests/test-custom-buffer-file--save-some-buffers.el | 123 | ||||
| -rw-r--r-- | themes/WIP-theme.el | 15 |
8 files changed, 1031 insertions, 27 deletions
diff --git a/.gitignore b/.gitignore index 274ac4b0a..af0d2ca0c 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,6 @@ __pycache__/ # editor/image backup files *.bak smoke/ + +# personal task list — kept local, not tracked +todo.org diff --git a/docs/subr-mock-migration-spec.org b/docs/subr-mock-migration-spec.org new file mode 100644 index 000000000..26f1dd576 --- /dev/null +++ b/docs/subr-mock-migration-spec.org @@ -0,0 +1,158 @@ +#+TITLE: Spec: Migrate Tests Off Mocking C Primitives +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-30 +#+STATUS: Draft — for discussion + +* Status + +Draft. Pulled out of =todo.org= (=** TODO [#C] Migrate tests off mocking +primitives (native-comp robustness) :test:refactor:solo:=) so the scope and +approach can be settled before any code moves. Execution is deferred; this +document is the discussion vehicle. + +Companion reference: [[file:native-comp-subr-mocking.org][native-comp-subr-mocking.org]] holds the full mechanism, the +upstream research, and the 2026-06-21 decision. This spec does not restate the +mechanism; it plans the remaining work that decision deferred. + +* Background — how we hit this + +We re-enabled native compilation config-wide (early-init.el, commit 3fd28987, +2026-06-20). Tests that had been green for months immediately started failing +with no change to their source — the first 8 were window-primitive mocks in +=test-dirvish-config-wrappers.el= and =test-calibredb-epub-config.el= +(=window-body-width=, =window-margins=, =current-window-configuration=, +=get-buffer-window=), throwing =wrong-number-of-arguments= for a zero-arg mock +lambda called with one argument. + +** What we struggled with (the consequences) + +- *Intermittent, non-deterministic failure.* The same test passed, then + crashed, then passed again within a session. Native-comp generates a + per-primitive trampoline =.eln= lazily and caches it on disk; whether a mock + "works" depends on whether that trampoline has been built yet. The + non-determinism was the tell, and it made the failures hard to trust or + reproduce. +- *Three distinct failure modes from one cause* (full detail in the companion + doc): (1) trampoline generation failure under =--batch=; (2) silent bypass — + natively-compiled callers ignore the mock and run the real primitive, so a + test passes for the wrong reason; (3) arity mismatch — the trampoline calls + the mock with the primitive's *maximum* arity, so a fixed-arity mock narrower + than the primitive throws. Mode 3 is the one that bit us; modes 1 and 2 sit + latent. +- *The tempting quick fix is the dangerous one.* Disabling subr trampolines + (=native-comp-enable-subr-trampolines nil=) is the most-cited workaround, but + in our native-comp-heavy setup it produces mode 2 — tests that pass while + asserting against the real primitive. A quiet false pass is worse than a loud + crash. +- *Scale of the latent surface.* The suite mocks subrs in hundreds of places. + The variadic sweep touched 188 arity-narrow mocks. + +** What is already done (the stopgap, 2026-06-21) + +Not currently broken — two things shipped (commits 571da499, b62c3c88): + +1. *Variadic sweep.* Every arity-narrow subr mock got =&rest _= appended, which + tolerates the trampoline's full-arity call. Deterministic, keeps trampolines + on, so no silent bypass. Fixes mode 3. +2. *Meta-test gate.* =tests/test-meta-subr-mock-arity.el= statically scans every + test file for =symbol-function= / =fset= / =setf= subr redefinitions and + fails =make test= if any mock can't accept the primitive's maximum arity. A + new arity-narrow mock can't merge silently. + +The stopgap fixes the mode we actually suffered. It leaves modes 1 and 2 latent. +The durable fix the ecosystem (and our own =elisp-testing.md=) points to is to +*not redefine primitives at all*. That is the work this spec scopes. + +* The real scope — most mocks should NOT move + +The raw inventory is large, but the headline number is misleading. =testing.md= +says to mock external boundaries; converting those to "drive real state" would +mean running real shells and touching the real filesystem in unit tests — the +opposite of what we want. So the actual migration target is narrow. + +Current subr-mock sites across 261 test files (=cl-letf= / =fset= / =advice-add= +on the named primitive): + +| Primitive | Sites | Classification | Disposition | +|-------------------------+-------+-------------------------+-----------------------------------| +| shell-command-to-string | 62 | external boundary | keep mocked (variadic) | +|-------------------------+-------+-------------------------+-----------------------------------| +| executable-find | 60 | external boundary | keep mocked (variadic) | +|-------------------------+-------+-------------------------+-----------------------------------| +| shell-command | 29 | external boundary | keep mocked (variadic) | +|-------------------------+-------+-------------------------+-----------------------------------| +| call-process | 17 | external boundary | keep mocked (variadic) | +|-------------------------+-------+-------------------------+-----------------------------------| +| current-time | 11 | time boundary | keep mocked (variadic) | +|-------------------------+-------+-------------------------+-----------------------------------| +| save-buffer | 10 | file I/O boundary | keep, or real temp-file fixture | +|-------------------------+-------+-------------------------+-----------------------------------| +| write-region | 4 | file I/O boundary | keep, or real temp-file fixture | +|-------------------------+-------+-------------------------+-----------------------------------| +| message | 69 | output-silencing | keep mocked (variadic) | +|-------------------------+-------+-------------------------+-----------------------------------| +| completing-read | 25 | UI prompt | MIGRATE — extract pure internal | +|-------------------------+-------+-------------------------+-----------------------------------| +| read-string | 16 | UI prompt | MIGRATE — extract pure internal | +|-------------------------+-------+-------------------------+-----------------------------------| +| yes-or-no-p | 14 | UI prompt | MIGRATE — extract pure internal | +|-------------------------+-------+-------------------------+-----------------------------------| +| read-from-minibuffer | 6 | UI prompt | MIGRATE — extract pure internal | +|-------------------------+-------+-------------------------+-----------------------------------| + +The genuine migration target is the UI-prompt bucket: ~61 sites. Per +=elisp-testing.md='s Interactive-vs-Internal rule, the fix is to extract a pure +internal that takes the value as an argument and test that directly, leaving the +interactive wrapper a thin un-tested (or smoke-tested) shell. That removes the +prompt mock entirely — immune to all three failure modes — and improves the +production code's testability as a side effect. + +Boundary mocks (shell, file I/O, time, =executable-find=, =call-process=) stay +mocked: that is correct unit-test practice, and the variadic form already +handles native-comp. =message= is output-silencing, not logic — keep it. + +* Proposed approach + +Not a single sweep. The migration touches production code (each extraction is a +small design change), so it is incremental and reviewable, not mechanical. + +1. *Scoped exemplar pass first.* Pick one module with a few prompt-mocks, do the + extract-internal conversion there, set the pattern, and measure the per-case + effort. This calibrates the rest. +2. *Batch by module afterward.* Convert remaining UI-prompt sites a module at a + time, each its own commit, with the suite green between. +3. *Leave boundaries alone.* No conversion of shell / file / time / process + mocks. The meta-test keeps them arity-safe. + +* Open decisions (resolve in discussion) + +** TODO Confirm the scope: UI-prompt mocks only, boundaries stay +Is the migration scoped to the ~61 UI-prompt sites (completing-read, read-string, +yes-or-no-p, read-from-minibuffer), with all boundary mocks explicitly out of +scope? Or is there an appetite to also convert the file-I/O mocks +(=save-buffer=, =write-region=) to real temp-file fixtures where it reads +cleaner? + +** TODO Reframe the todo.org task title to match the real scope +The current title — "Migrate tests off mocking primitives" — reads as all 300+ +sites. If we agree on UI-prompt-only, retitle to something like "Extract pure +internals for UI-prompt-mocked tests" so a future session does not re-scope it +as a wholesale sweep. + +** TODO Pick the exemplar module for the first pass +Which module gets the calibrating conversion? A small one with 2-4 prompt-mocks +is ideal. Candidate selection needs a per-module breakdown of the ~61 sites +(not yet collected). + +** TODO Decide priority and timing +Currently =[#C]=, =:solo:=. The suite is not broken (stopgap holds), so this is +test-quality debt, not urgent. Confirm it stays low and gets done in batches +between other work, rather than as a dedicated push. + +* Non-goals + +- Re-deriving or re-documenting the native-comp trampoline mechanism (see the + companion doc). +- Converting boundary mocks (shell, file I/O, time, process, executable-find). +- Removing the variadic-mock convention or the meta-test gate — both stay; they + are the standing protection for every mock that legitimately remains. diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el index 261956f37..38ae0bae1 100644 --- a/modules/custom-buffer-file.el +++ b/modules/custom-buffer-file.el @@ -370,6 +370,262 @@ Sets up diff-mode for navigation." (diff-mode) (goto-char (point-min))))) +(defun cj/--diff-buffer-renderer (ws-only difft-available) + "Choose the diff renderer symbol from WS-ONLY and DIFFT-AVAILABLE. +`whitespace' for a whitespace-only diff (a plain unified diff with trailing +whitespace highlighted, because difftastic treats it as no change and renders it +blank); otherwise `difftastic' when available, else `regular'." + (cond (ws-only 'whitespace) + (difft-available 'difftastic) + (t 'regular))) + +(defun cj/--diff-whitespace-only-p (file-a file-b) + "Return non-nil if FILE-A and FILE-B differ ONLY in whitespace. +Route-1 detection via diff(1): true when a plain `diff' reports a difference but +`diff -w' (ignore all whitespace) reports none. Identical files differ in +nothing, so they are not whitespace-only." + (and (not (zerop (call-process "diff" nil nil nil "-q" file-a file-b))) + (zerop (call-process "diff" nil nil nil "-q" "-w" file-a file-b)))) + +(defun cj/--buffer-differs-prompt-string (name ws-only-p) + "Build the buffer-differs prompt question for buffer NAME. +When WS-ONLY-P is non-nil, fold a terse \"(whitespace only)\" parenthetical into +the question so the reader knows the difference is whitespace before choosing." + (format "%s changed on disk%s" + name (if ws-only-p " (whitespace only)" ""))) + +(defun cj/--buffer-differs-choices () + "Return the terse `read-multiple-choice' menu for the disk-changed save prompt. +Inline names are single words so the menu fits at a glance; the full meaning is +in each description (the ? help). s overwrites the file with the buffer; r +discards the buffer's edits and rereads from disk." + '((?s "save" "overwrite the file with this buffer") + (?d "diff" "show what changed, then ask again") + (?w "clean" "clean whitespace and save") + (?r "revert" "discard edits and reread from disk") + (?c "cancel" "leave the buffer as is"))) + +(defun cj/--buffer-changed-on-disk-p (buffer) + "Return non-nil if BUFFER is modified AND its file changed on disk since visited. +This is the disk-changed conflict: there are unsaved edits to lose AND the file +underneath has moved, so a plain save would silently overwrite the disk version." + (when (buffer-live-p buffer) + (with-current-buffer buffer + (and (buffer-modified-p) + buffer-file-name + (file-exists-p buffer-file-name) + (not (verify-visited-file-modtime buffer)))))) + +(defun cj/--buffer-differs-action (key) + "Map a disk-changed-prompt KEY to an action symbol, or nil when unmapped. +`save' overwrites the file, `clean-save' cleans whitespace then saves, `revert' +rereads from disk, `cancel' does nothing, and `diff' peeks (the caller re-prompts)." + (pcase key + (?s 'save) + (?w 'clean-save) + (?r 'revert) + (?d 'diff) + (?c 'cancel))) + +(defun cj/--buffer-differs-dispatch (buffer action) + "Carry out ACTION for BUFFER after a disk-changed prompt. +`save' overwrites the file with the buffer; `clean-save' strips trailing +whitespace first; `revert' discards the buffer's edits and rereads the disk; +`cancel' leaves the buffer untouched. Save updates the recorded modtime first so +the stock `save-buffer' does not re-ask its own \"changed on disk\" question." + (with-current-buffer buffer + (pcase action + ('save (set-visited-file-modtime) (save-buffer)) + ('clean-save (delete-trailing-whitespace) (set-visited-file-modtime) (save-buffer)) + ('revert (revert-buffer t t)) + ('cancel (message "Save cancelled; buffer left as is")) + (_ nil)))) + +(defun cj/--read-choice-with-diff (prompt choices show-diff-fn) + "Read a `read-multiple-choice' key for PROMPT and CHOICES; d toggles a diff. +SHOW-DIFF-FN displays the buffer/file diff and returns its buffer. The d key +shows the diff, or hides it when it is already shown (a toggle). Any other key +-- a terminating choice -- closes a still-open diff window before returning that +key, so the diff never lingers after the decision is made." + (let ((key nil) (diff-buf nil)) + (while (not key) + (let ((k (car (read-multiple-choice prompt choices)))) + (if (eq k ?d) + (let ((win (and (buffer-live-p diff-buf) (get-buffer-window diff-buf)))) + (if win + (progn (quit-window nil win) (setq diff-buf nil)) + (setq diff-buf (funcall show-diff-fn)))) + (setq key k)))) + (let ((win (and (buffer-live-p diff-buf) (get-buffer-window diff-buf)))) + (when win (quit-window nil win))) + key)) + +(defun cj/--buffer-differs-read-key (buffer ws-only) + "Read a disk-changed-prompt key for BUFFER via `read-multiple-choice'. +WS-ONLY non-nil folds a terse \"(whitespace only)\" note into the prompt. d +toggles the buffer/file diff; a terminating choice closes a still-open diff." + (cj/--read-choice-with-diff + (cj/--buffer-differs-prompt-string (buffer-name buffer) ws-only) + (cj/--buffer-differs-choices) + (lambda () (with-current-buffer buffer (cj/diff-buffer-with-file))))) + +(defun cj/save-buffer (&optional arg) + "Save the current buffer; show a legible menu when the file changed on disk. +A normal save falls straight through to `save-buffer' (ARG, the prefix argument, +is passed along so \\[universal-argument] \\[save-buffer] still marks for backup). +When the buffer has unsaved edits AND the file changed on disk since it was +visited, offer a terse labeled menu -- save / diff / clean / revert / cancel -- +instead of the stock yes/no \"Save anyway?\" prompt. Bound to \\`C-x C-s'." + (interactive "P") + (if (not (cj/--buffer-changed-on-disk-p (current-buffer))) + (save-buffer arg) + (let* ((buf (current-buffer)) + (ws-only (cj/--buffer-file-whitespace-only-p buf)) + (key (cj/--buffer-differs-read-key buf ws-only))) + (cj/--buffer-differs-dispatch buf (cj/--buffer-differs-action key))))) + +(defun cj/--save-some-buffers-action (key) + "Map a save-loop KEY to (THIS-ACTION . LOOP-EFFECT), or nil when unmapped. +THIS-ACTION is `save', `clean-save', `skip', or `diff'. LOOP-EFFECT is +`continue' (keep prompting), `save-rest' (save this and all remaining without +asking), `stop' (act on this, skip the rest), or `reprompt' (peek, then ask the +same buffer again)." + (pcase key + (?y '(save . continue)) + (?n '(skip . continue)) + (?w '(clean-save . continue)) + (?! '(save . save-rest)) + (?. '(save . stop)) + (?q '(skip . stop)) + (?d '(diff . reprompt)))) + +(defun cj/--save-some-buffers-choices () + "Return the terse `read-multiple-choice' choices for the save loop. +Single-word inline names keep the menu to the minimum space; the full meaning is +in each description (the ? help)." + '((?y "save" "save this buffer") + (?n "skip" "do not save this buffer") + (?w "clean" "clean whitespace and save this buffer") + (?d "diff" "show what changed, then ask again") + (?! "all" "save this and all remaining buffers") + (?. "only" "save this buffer, then skip the rest") + (?q "none" "stop; save no more buffers"))) + +(defun cj/--buffer-file-whitespace-only-p (buffer) + "Return non-nil if BUFFER's text differs from its visited file ONLY in whitespace. +Writes the buffer to a temp file and reuses `cj/--diff-whitespace-only-p'. Nil +when BUFFER visits no file or the file is gone." + (when (buffer-live-p buffer) + (with-current-buffer buffer + (let ((file (buffer-file-name))) + (when (and file (file-exists-p file)) + (let ((temp (make-temp-file "cbf-ws-buf-" nil + (or (file-name-extension file t) ""))) + (content (buffer-string))) + (unwind-protect + (progn (with-temp-file temp (insert content)) + (cj/--diff-whitespace-only-p file temp)) + (when (file-exists-p temp) (delete-file temp))))))))) + +(defun cj/--save-some-buffers-plan (buffers key-fn) + "Resolve each buffer in BUFFERS to a per-buffer action using KEY-FN. +KEY-FN is called with a buffer and returns a `read-multiple-choice' key; the diff +re-prompt is the caller's concern, so KEY-FN never returns ?d. Returns a list of +\(BUFFER . ACTION) where ACTION is `save', `clean-save', or `skip', honoring +`save-rest' (! saves this and all remaining) and `stop' (./q act on this, then +skip the rest). KEY-FN is not consulted once a buffer triggers save-rest or stop." + (let ((plan nil) (mode 'ask)) + (dolist (buf buffers (nreverse plan)) + (pcase mode + ('save-all (push (cons buf 'save) plan)) + ('done (push (cons buf 'skip) plan)) + ('ask + (pcase (cj/--save-some-buffers-action (funcall key-fn buf)) + (`(,act . save-rest) (push (cons buf act) plan) (setq mode 'save-all)) + (`(,act . stop) (push (cons buf act) plan) (setq mode 'done)) + (`(,act . ,_) (push (cons buf act) plan)) + (_ (push (cons buf 'skip) plan)))))))) + +(declare-function files--buffers-needing-to-be-saved "files" (pred)) + +(defun cj/--save-some-buffers-read-key (buffer ws-only) + "Read a save-loop key for BUFFER via `read-multiple-choice'. +WS-ONLY non-nil folds a terse \"(whitespace only)\" note into the prompt. d +toggles the buffer/file diff; a terminating choice closes a still-open diff." + (cj/--read-choice-with-diff + (format "Save %s%s" + (if (buffer-file-name buffer) + (file-name-nondirectory (buffer-file-name buffer)) + (buffer-name buffer)) + (if ws-only " (whitespace only)" "")) + (cj/--save-some-buffers-choices) + (lambda () (with-current-buffer buffer (cj/diff-buffer-with-file))))) + +(defun cj/--save-some-buffers-execute (plan) + "Carry out PLAN, a list of (BUFFER . ACTION); return the number saved. +ACTION `clean-save' deletes trailing whitespace before saving; `save' saves as-is; +`skip' leaves the buffer alone." + (let ((n 0)) + (dolist (entry plan n) + (let ((buffer (car entry))) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (pcase (cdr entry) + ('clean-save (delete-trailing-whitespace) (save-buffer) (setq n (1+ n))) + ('save (save-buffer) (setq n (1+ n))) + (_ nil)))))))) + +(defun cj/save-some-buffers (&optional arg pred) + "Save modified buffers with a legible, labeled prompt per buffer. +A `read-multiple-choice' replacement for `save-some-buffers': the options are +shown on screen rather than recalled as keys, with an added clean-whitespace-and- +save action and a per-buffer \"(whitespace only)\" note. ARG and PRED match +`save-some-buffers' -- ARG non-nil saves all without asking; PRED selects which +buffers are considered. Installed over `save-some-buffers' by advice, so \\[save-some-buffers] +and the save-on-exit prompt both use it." + (interactive "P") + (unless pred + (setq pred + (if (and (symbolp save-some-buffers-default-predicate) + (get save-some-buffers-default-predicate + 'save-some-buffers-function)) + (funcall save-some-buffers-default-predicate) + save-some-buffers-default-predicate))) + (let (queried autosaved-buffers files-done inhibit-message) + (save-window-excursion + ;; Save buffers flagged for unconditional save first (mirrors the original). + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (and buffer-save-without-query (buffer-modified-p)) + (push (buffer-name) autosaved-buffers) + (save-buffer)))) + (let* ((candidates (files--buffers-needing-to-be-saved pred)) + (plan (if arg + (mapcar (lambda (b) (cons b 'save)) candidates) + (when candidates (setq queried t)) + (cj/--save-some-buffers-plan + candidates + (lambda (b) + (cj/--save-some-buffers-read-key + b (cj/--buffer-file-whitespace-only-p b))))))) + (setq files-done (cj/--save-some-buffers-execute plan))) + ;; Let other things (abbrevs, etc.) save at this point. + (dolist (func save-some-buffers-functions) + (setq inhibit-message (or (funcall func nil arg) inhibit-message))) + (or queried (> files-done 0) inhibit-message + (cond + ((null autosaved-buffers) + (when (called-interactively-p 'any) + (message "(No files need saving)"))) + ((= (length autosaved-buffers) 1) + (message "(Saved %s)" (car autosaved-buffers))) + (t (message "(Saved %d files: %s)" (length autosaved-buffers) + (mapconcat #'identity autosaved-buffers ", ")))))) + files-done)) + +(advice-add 'save-some-buffers :override #'cj/save-some-buffers) +(keymap-global-set "C-x C-s" #'cj/save-buffer) + (defun cj/diff-buffer-with-file () "Compare the current modified buffer with the saved version. Uses difftastic if available for syntax-aware diffing, otherwise @@ -389,17 +645,27 @@ Signals an error if the buffer is not visiting a file." (insert buffer-content)) ;; Check if there are any differences first (if (zerop (call-process "diff" nil nil nil "-q" file temp-file)) - (message "No differences between buffer and file") - ;; Run diff/difftastic and display in buffer - (let* ((using-difftastic (cj/executable-exists-p "difft")) - (buffer-name (if using-difftastic + (progn (message "No differences between buffer and file") nil) + ;; Pick a renderer: difftastic for content diffs, but a plain unified + ;; diff with trailing whitespace highlighted for whitespace-only ones + ;; (difftastic treats trailing whitespace as no change and hides it). + (let* ((renderer (cj/--diff-buffer-renderer + (cj/--diff-whitespace-only-p file temp-file) + (cj/executable-exists-p "difft"))) + (buffer-name (if (eq renderer 'difftastic) "*Diff (difftastic)*" "*Diff (unified)*")) (diff-buffer (get-buffer-create buffer-name))) - (if using-difftastic + (if (eq renderer 'difftastic) (cj/--diff-with-difftastic file temp-file diff-buffer) - (cj/--diff-with-regular-diff file temp-file diff-buffer)) - (display-buffer diff-buffer)))) + (cj/--diff-with-regular-diff file temp-file diff-buffer) + (when (eq renderer 'whitespace) + (with-current-buffer diff-buffer + (setq-local show-trailing-whitespace t)))) + (display-buffer diff-buffer) + ;; Return the diff buffer so callers (the save prompts) can toggle + ;; and auto-close its window. + diff-buffer))) ;; Clean up temp file (when (file-exists-p temp-file) (delete-file temp-file))))) diff --git a/scripts/theme-studio/WIP.json b/scripts/theme-studio/WIP.json index f30294dbc..5a54729bb 100644 --- a/scripts/theme-studio/WIP.json +++ b/scripts/theme-studio/WIP.json @@ -437,7 +437,7 @@ "sky" ], [ - "#5f8bf9", + "#5178db", "link", "link" ] @@ -1046,7 +1046,7 @@ "height": null }, "link": { - "fg": "#5f8bf9", + "fg": "#5178db", "bg": null, "distant-fg": null, "family": null, @@ -1571,7 +1571,16 @@ "pkg:nerd-icons:nerd-icons-red", "pkg:nerd-icons:nerd-icons-red-alt", "pkg:nerd-icons:nerd-icons-silver", - "pkg:nerd-icons:nerd-icons-yellow" + "pkg:nerd-icons:nerd-icons-yellow", + "pkg:nov-reading:cj/nov-reading-light", + "pkg:nov-reading:cj/nov-reading-dark", + "pkg:nov-reading:cj/nov-reading-sepia", + "pkg:nov-reading:cj/nov-reading-sepia-heading", + "pkg:nov-reading:cj/nov-reading-sepia-link", + "pkg:nov-reading:cj/nov-reading-dark-heading", + "pkg:nov-reading:cj/nov-reading-dark-link", + "pkg:nov-reading:cj/nov-reading-light-heading", + "pkg:nov-reading:cj/nov-reading-light-link" ], "packages": { "org-mode": { @@ -3564,18 +3573,6 @@ "inherit": null, "source": "user" }, - "eat-term-color-22": { - "fg": "#002f00", - "bg": null, - "inherit": null, - "source": "user" - }, - "eat-term-color-52": { - "fg": "#2f0000", - "bg": null, - "inherit": null, - "source": "user" - }, "eat-term-color-bright-black": { "fg": "#8e919a", "bg": null, @@ -3681,6 +3678,18 @@ "bg": null, "inherit": null, "source": "user" + }, + "eat-term-color-22": { + "fg": "#002f00", + "bg": null, + "inherit": null, + "source": "user" + }, + "eat-term-color-52": { + "fg": "#2f0000", + "bg": null, + "inherit": null, + "source": "user" } }, "auto-dim-other-buffers": { @@ -4503,6 +4512,75 @@ "source": "user" } }, + "nov-reading": { + "cj/nov-reading-sepia": { + "fg": "#ab8d2e", + "bg": null, + "inherit": null, + "source": "user" + }, + "cj/nov-reading-dark": { + "fg": "#7c838a", + "bg": null, + "inherit": null, + "source": "user" + }, + "cj/nov-reading-light": { + "fg": "#100f0f", + "bg": "#7c838a", + "inherit": null, + "source": "user" + }, + "cj/nov-reading-sepia-heading": { + "fg": "#54677d", + "bg": null, + "inherit": "cj/nov-reading-sepia", + "height": 1.2, + "source": "user" + }, + "cj/nov-reading-sepia-link": { + "fg": "#5178db", + "bg": null, + "underline": { + "color": null, + "style": "line" + }, + "inherit": null, + "source": "user" + }, + "cj/nov-reading-dark-heading": { + "fg": null, + "bg": null, + "inherit": null, + "source": "default" + }, + "cj/nov-reading-dark-link": { + "fg": "#5178db", + "bg": null, + "underline": { + "color": null, + "style": "line" + }, + "inherit": "cj/nov-reading-dark", + "source": "user" + }, + "cj/nov-reading-light-heading": { + "fg": null, + "bg": null, + "inherit": null, + "source": "default" + }, + "cj/nov-reading-light-link": { + "fg": "#5178db", + "bg": null, + "underline": { + "color": null, + "style": "line" + }, + "inherit": null, + "source": "user" + } + }, "erc": { "erc-header-line": { "fg": "#e4eaf8", @@ -5815,7 +5893,7 @@ "source": "user" }, "shr-link": { - "fg": "#5f8bf9", + "fg": "#5178db", "bg": null, "underline": { "style": "line", @@ -8678,6 +8756,32 @@ "source": "default" } }, + "wttrin": { + "wttrin-instructions": { + "fg": null, + "bg": null, + "inherit": null, + "source": "default" + }, + "wttrin-key": { + "fg": null, + "bg": null, + "inherit": null, + "source": "default" + }, + "wttrin-mode-line-stale": { + "fg": null, + "bg": null, + "inherit": null, + "source": "default" + }, + "wttrin-staleness-header": { + "fg": null, + "bg": null, + "inherit": null, + "source": "default" + } + }, "yasnippet": { "yas--field-debug-face": { "fg": null, diff --git a/tests/test-custom-buffer-file--buffer-differs-prompt.el b/tests/test-custom-buffer-file--buffer-differs-prompt.el new file mode 100644 index 000000000..109ca121f --- /dev/null +++ b/tests/test-custom-buffer-file--buffer-differs-prompt.el @@ -0,0 +1,197 @@ +;;; test-custom-buffer-file--buffer-differs-prompt.el --- disk-changed save prompt pieces -*- lexical-binding: t; -*- + +;;; Commentary: +;; Pure-logic tests for the disk-changed save prompt (the C-x C-s case where the +;; buffer is modified AND the file changed on disk): the question string (with a +;; terse whitespace-only parenthetical), the labeled-but-terse choice list, and +;; the key->action mapping. The interactive read loop, the diff display, and the +;; save/revert dispatch are exercised live, not here. + +;;; Code: + +(require 'ert) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'custom-buffer-file) + +(declare-function cj/--buffer-differs-prompt-string "custom-buffer-file" (name ws-only-p)) +(declare-function cj/--buffer-differs-choices "custom-buffer-file" ()) +(declare-function cj/--buffer-differs-action "custom-buffer-file" (key)) + +;;; ------------------- cj/--buffer-differs-prompt-string ---------------------- + +(ert-deftest test-cbf-buffer-differs-prompt-plain () + "Normal: without whitespace-only, the prompt names the buffer, no parenthetical." + (let ((s (cj/--buffer-differs-prompt-string "todo.org" nil))) + (should (string-match-p "todo\\.org" s)) + (should-not (string-match-p "whitespace" s)))) + +(ert-deftest test-cbf-buffer-differs-prompt-whitespace-only () + "Normal: whitespace-only folds in a terse \"(whitespace only)\" parenthetical." + (let ((s (cj/--buffer-differs-prompt-string "todo.org" t))) + (should (string-match-p "todo\\.org" s)) + (should (string-match-p "(whitespace only)" s)))) + +;;; ---------------------- cj/--buffer-differs-choices ------------------------- + +(ert-deftest test-cbf-buffer-differs-choices-keys () + "Normal: the menu offers save, diff, clean, revert, and cancel." + (let ((c (cj/--buffer-differs-choices))) + (dolist (key '(?s ?d ?w ?r ?c)) + (should (assq key c))))) + +(ert-deftest test-cbf-buffer-differs-choices-terse-names () + "Boundary: inline names stay terse (one word) so the menu fits at a glance." + (dolist (entry (cj/--buffer-differs-choices)) + (let ((name (nth 1 entry))) + (should (stringp name)) + (should-not (string-match-p " " name))))) + +(ert-deftest test-cbf-buffer-differs-choices-clean-help-mentions-whitespace () + "Normal: the clean option's description (the ? help) names whitespace." + (let ((entry (assq ?w (cj/--buffer-differs-choices)))) + (should (string-match-p "whitespace" (or (nth 2 entry) ""))))) + +(ert-deftest test-cbf-buffer-differs-choices-revert-help-mentions-disk () + "Normal: the revert option's description makes clear it rereads from disk." + (let ((entry (assq ?r (cj/--buffer-differs-choices)))) + (should (string-match-p "disk" (or (nth 2 entry) ""))))) + +;;; ---------------------- cj/--buffer-differs-action -------------------------- + +(ert-deftest test-cbf-buffer-differs-action-save () + "Normal: s overwrites the file with the buffer." + (should (eq (cj/--buffer-differs-action ?s) 'save))) + +(ert-deftest test-cbf-buffer-differs-action-clean () + "Normal: w cleans whitespace, then saves." + (should (eq (cj/--buffer-differs-action ?w) 'clean-save))) + +(ert-deftest test-cbf-buffer-differs-action-revert () + "Normal: r discards edits and rereads from disk." + (should (eq (cj/--buffer-differs-action ?r) 'revert))) + +(ert-deftest test-cbf-buffer-differs-action-diff () + "Normal: d peeks at the diff (re-prompt is the caller's concern)." + (should (eq (cj/--buffer-differs-action ?d) 'diff))) + +(ert-deftest test-cbf-buffer-differs-action-cancel () + "Boundary: c cancels, leaving the buffer untouched." + (should (eq (cj/--buffer-differs-action ?c) 'cancel))) + +(ert-deftest test-cbf-buffer-differs-action-unknown () + "Error: an unmapped key returns nil." + (should-not (cj/--buffer-differs-action ?z))) + +;;; ------------------- cj/--buffer-changed-on-disk-p -------------------------- +;; Real visited-file buffers; modtime state is driven (set-visited-file-modtime), +;; not mocked. The trigger is the disk-changed conflict: modified AND the file +;; changed on disk since visited. + +(declare-function cj/--buffer-changed-on-disk-p "custom-buffer-file" (buffer)) + +(defun test-cbf-cod--with-visited (edit-fn body-fn) + "Visit a temp file, run EDIT-FN in its buffer, call BODY-FN with the buffer." + (let ((f (make-temp-file "cbf-cod-" nil ".txt"))) + (unwind-protect + (progn + (with-temp-file f (insert "original\n")) + (let ((buf (find-file-noselect f))) + (unwind-protect + (with-current-buffer buf (funcall edit-fn) (funcall body-fn buf)) + (with-current-buffer buf (set-buffer-modified-p nil)) + (kill-buffer buf)))) + (when (file-exists-p f) (delete-file f))))) + +(ert-deftest test-cbf-changed-on-disk-detects () + "Normal: modified buffer whose recorded modtime no longer matches is changed-on-disk." + (test-cbf-cod--with-visited + (lambda () (goto-char (point-max)) (insert "edit\n") (set-visited-file-modtime '(0 0))) + (lambda (buf) (should (cj/--buffer-changed-on-disk-p buf))))) + +(ert-deftest test-cbf-changed-on-disk-clean-modtime () + "Boundary: modified buffer whose modtime still matches is not changed-on-disk." + (test-cbf-cod--with-visited + (lambda () (goto-char (point-max)) (insert "edit\n")) + (lambda (buf) (should-not (cj/--buffer-changed-on-disk-p buf))))) + +(ert-deftest test-cbf-changed-on-disk-unmodified () + "Boundary: an unmodified buffer is never changed-on-disk (nothing of mine to lose)." + (test-cbf-cod--with-visited + (lambda () (set-visited-file-modtime '(0 0))) + (lambda (buf) (should-not (cj/--buffer-changed-on-disk-p buf))))) + +;;; -------------- cj/--buffer-differs-dispatch (data direction) --------------- +;; The destructive directions, driven against real files: `save' overwrites the +;; disk with the buffer (buffer wins); `revert' discards the buffer's edits and +;; rereads the disk (disk wins); `clean-save' strips trailing whitespace first. + +(declare-function cj/--buffer-differs-dispatch "custom-buffer-file" (buffer action)) +(declare-function cj/save-buffer "custom-buffer-file" ()) + +(defun test-cbf-disp--with-conflict (buffer-insert disk-content body-fn) + "Visit a temp file, BUFFER-INSERT into the buffer, overwrite the file with +DISK-CONTENT underneath, then call BODY-FN with the buffer and file path." + (let ((f (make-temp-file "cbf-disp-" nil ".txt"))) + (unwind-protect + (progn + (with-temp-file f (insert "original\n")) + (let ((buf (find-file-noselect f))) + (unwind-protect + (with-current-buffer buf + (goto-char (point-max)) (insert buffer-insert) + (with-temp-file f (insert disk-content)) + (funcall body-fn buf f)) + (with-current-buffer buf (set-buffer-modified-p nil)) + (kill-buffer buf)))) + (when (file-exists-p f) (delete-file f))))) + +(defun test-cbf-disp--disk (f) + "Return the on-disk contents of F." + (with-temp-buffer (insert-file-contents f) (buffer-string))) + +(ert-deftest test-cbf-buffer-differs-dispatch-save-overwrites-disk () + "Normal: save writes the buffer over the disk version (buffer wins)." + (test-cbf-disp--with-conflict + "my edit\n" "disk changed underneath\n" + (lambda (buf f) + (cj/--buffer-differs-dispatch buf 'save) + (should-not (buffer-modified-p buf)) + (should (string= (test-cbf-disp--disk f) "original\nmy edit\n"))))) + +(ert-deftest test-cbf-buffer-differs-dispatch-revert-discards-edits () + "Normal: revert discards the buffer's edits and rereads the disk (disk wins)." + (test-cbf-disp--with-conflict + "my edit\n" "disk version\n" + (lambda (buf f) + (cj/--buffer-differs-dispatch buf 'revert) + (should-not (buffer-modified-p buf)) + (should (string= (with-current-buffer buf (buffer-string)) "disk version\n"))))) + +(ert-deftest test-cbf-buffer-differs-dispatch-clean-save-strips-whitespace () + "Normal: clean-save strips trailing whitespace, then overwrites the disk." + (test-cbf-disp--with-conflict + "edit \n" "disk changed\n" + (lambda (buf f) + (cj/--buffer-differs-dispatch buf 'clean-save) + (should-not (buffer-modified-p buf)) + (should (string= (test-cbf-disp--disk f) "original\nedit\n"))))) + +(ert-deftest test-cbf-save-buffer-fast-path-no-conflict () + "Boundary: with no disk conflict, cj/save-buffer just saves (no prompt path)." + (let ((f (make-temp-file "cbf-fast-" nil ".txt"))) + (unwind-protect + (progn + (with-temp-file f (insert "base\n")) + (let ((buf (find-file-noselect f))) + (unwind-protect + (with-current-buffer buf + (goto-char (point-max)) (insert "added\n") + (cj/save-buffer) ; modtime matches -> fast path + (should-not (buffer-modified-p)) + (should (string= (test-cbf-disp--disk f) "base\nadded\n"))) + (with-current-buffer buf (set-buffer-modified-p nil)) + (kill-buffer buf)))) + (when (file-exists-p f) (delete-file f))))) + +(provide 'test-custom-buffer-file--buffer-differs-prompt) +;;; test-custom-buffer-file--buffer-differs-prompt.el ends here diff --git a/tests/test-custom-buffer-file--diff-whitespace-only.el b/tests/test-custom-buffer-file--diff-whitespace-only.el new file mode 100644 index 000000000..e792637b5 --- /dev/null +++ b/tests/test-custom-buffer-file--diff-whitespace-only.el @@ -0,0 +1,146 @@ +;;; test-custom-buffer-file--diff-whitespace-only.el --- whitespace-only diff detection -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for cj/--diff-whitespace-only-p, the route-1 detector behind the +;; buffer-differs save prompt: two files differ ONLY in whitespace when a plain +;; diff finds changes but `diff -w' (ignore all whitespace) finds none. Uses +;; real temp files and the real diff(1) binary (a system boundary we keep), so +;; nothing is mocked. + +;;; Code: + +(require 'ert) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'custom-buffer-file) + +(declare-function cj/--diff-whitespace-only-p "custom-buffer-file" (file-a file-b)) + +(defun test-cbf-ws--two-files (content-a content-b fn) + "Write CONTENT-A and CONTENT-B to temp files, call FN with their paths, clean up." + (let ((a (make-temp-file "cbf-ws-a-")) + (b (make-temp-file "cbf-ws-b-"))) + (unwind-protect + (progn + (with-temp-file a (insert content-a)) + (with-temp-file b (insert content-b)) + (funcall fn a b)) + (delete-file a) + (delete-file b)))) + +(ert-deftest test-cbf-diff-whitespace-only-trailing () + "Normal: files differing only by trailing whitespace are whitespace-only." + (test-cbf-ws--two-files + "alpha\nbeta\n" "alpha \nbeta\n" + (lambda (a b) (should (cj/--diff-whitespace-only-p a b))))) + +(ert-deftest test-cbf-diff-whitespace-only-indentation () + "Normal: files differing only by leading indentation are whitespace-only." + (test-cbf-ws--two-files + "(foo)\n(bar)\n" "(foo)\n (bar)\n" + (lambda (a b) (should (cj/--diff-whitespace-only-p a b))))) + +(ert-deftest test-cbf-diff-whitespace-only-real-content () + "Normal: files differing in actual content are NOT whitespace-only." + (test-cbf-ws--two-files + "alpha\nbeta\n" "alpha\nGAMMA\n" + (lambda (a b) (should-not (cj/--diff-whitespace-only-p a b))))) + +(ert-deftest test-cbf-diff-whitespace-only-identical () + "Boundary: identical files do not differ at all, so not whitespace-only." + (test-cbf-ws--two-files + "alpha\nbeta\n" "alpha\nbeta\n" + (lambda (a b) (should-not (cj/--diff-whitespace-only-p a b))))) + +(ert-deftest test-cbf-diff-whitespace-only-mixed () + "Boundary: whitespace change plus a real content change is NOT whitespace-only." + (test-cbf-ws--two-files + "alpha\nbeta\n" "alpha \nGAMMA\n" + (lambda (a b) (should-not (cj/--diff-whitespace-only-p a b))))) + +;;; -------------------- cj/--diff-buffer-renderer ----------------------------- +;; Which renderer the diff command uses: whitespace-only diffs go to a plain +;; unified diff (trailing whitespace highlighted, so it is actually visible) +;; because difftastic treats trailing-whitespace as no change and renders it +;; blank. Real content diffs use difftastic when available, else plain diff. + +(declare-function cj/--diff-buffer-renderer "custom-buffer-file" (ws-only difft-available)) + +(ert-deftest test-cbf-diff-renderer-whitespace-over-difftastic () + "Normal: a whitespace-only diff uses the whitespace renderer even when difft is present." + (should (eq (cj/--diff-buffer-renderer t t) 'whitespace))) + +(ert-deftest test-cbf-diff-renderer-whitespace-no-difft () + "Boundary: whitespace-only still uses the whitespace renderer without difft." + (should (eq (cj/--diff-buffer-renderer t nil) 'whitespace))) + +(ert-deftest test-cbf-diff-renderer-content-uses-difftastic () + "Normal: a content diff uses difftastic when it is available." + (should (eq (cj/--diff-buffer-renderer nil t) 'difftastic))) + +(ert-deftest test-cbf-diff-renderer-content-no-difft-regular () + "Boundary: a content diff falls back to the regular renderer without difft." + (should (eq (cj/--diff-buffer-renderer nil nil) 'regular))) + +;;; --------------- cj/--buffer-file-whitespace-only-p (buffer) ---------------- +;; Buffer-vs-its-file variant: writes the buffer to a temp file and reuses the +;; detector against the buffer's visited file. Uses a real visited-file buffer. + +(declare-function cj/--buffer-file-whitespace-only-p "custom-buffer-file" (buffer)) + +(defun test-cbf-ws--with-visited (disk-content edit-fn body-fn) + "Visit a temp file holding DISK-CONTENT, apply EDIT-FN in it, call BODY-FN with the buffer." + (let ((f (make-temp-file "cbf-bws-" nil ".txt"))) + (unwind-protect + (progn + (with-temp-file f (insert disk-content)) + (let ((buf (find-file-noselect f))) + (unwind-protect + (with-current-buffer buf + (funcall edit-fn) + (funcall body-fn buf)) + (with-current-buffer buf (set-buffer-modified-p nil)) + (kill-buffer buf)))) + (when (file-exists-p f) (delete-file f))))) + +(ert-deftest test-cbf-buffer-file-ws-only-trailing () + "Normal: an unsaved trailing-whitespace edit is whitespace-only vs the file." + (test-cbf-ws--with-visited + "alpha\nbeta\n" + (lambda () (goto-char (point-min)) (end-of-line) (insert " ")) + (lambda (buf) (should (cj/--buffer-file-whitespace-only-p buf))))) + +(ert-deftest test-cbf-buffer-file-ws-only-content () + "Normal: an unsaved content edit is NOT whitespace-only vs the file." + (test-cbf-ws--with-visited + "alpha\nbeta\n" + (lambda () (goto-char (point-max)) (insert "gamma\n")) + (lambda (buf) (should-not (cj/--buffer-file-whitespace-only-p buf))))) + +;;; --------- cj/diff-buffer-with-file return value (for the toggle) ----------- + +(ert-deftest test-cbf-diff-returns-buffer-when-differs () + "Normal: cj/diff-buffer-with-file returns the live diff buffer when the buffer differs." + (test-cbf-ws--with-visited + "x\ny\n" + (lambda () (goto-char (point-max)) (insert "ADDED\n")) + (lambda (buf) + (let ((db (with-current-buffer buf (cj/diff-buffer-with-file)))) + (should (bufferp db)) + (should (buffer-live-p db)))))) + +(ert-deftest test-cbf-diff-returns-nil-when-identical () + "Boundary: with no differences, cj/diff-buffer-with-file returns nil." + (test-cbf-ws--with-visited + "x\ny\n" + (lambda () nil) + (lambda (buf) + (should-not (with-current-buffer buf (cj/diff-buffer-with-file)))))) + +(ert-deftest test-cbf-buffer-file-ws-only-non-file () + "Boundary: a buffer not visiting a file is not whitespace-only." + (with-temp-buffer + (insert "scratch") + (should-not (cj/--buffer-file-whitespace-only-p (current-buffer))))) + +(provide 'test-custom-buffer-file--diff-whitespace-only) +;;; test-custom-buffer-file--diff-whitespace-only.el ends here diff --git a/tests/test-custom-buffer-file--save-some-buffers.el b/tests/test-custom-buffer-file--save-some-buffers.el new file mode 100644 index 000000000..d4ecd318d --- /dev/null +++ b/tests/test-custom-buffer-file--save-some-buffers.el @@ -0,0 +1,123 @@ +;;; test-custom-buffer-file--save-some-buffers.el --- save-loop prompt pieces -*- lexical-binding: t; -*- + +;;; Commentary: +;; Pure-logic tests for the read-multiple-choice save loop that replaces +;; save-some-buffers' terse map-y-or-n-p prompt: the key->action mapping (what +;; to do with this buffer, and how the choice steers the rest of the loop) and +;; the labeled choice list. The interactive loop, the file saves, and the +;; override wiring are exercised live, not here. + +;;; Code: + +(require 'ert) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'custom-buffer-file) + +(declare-function cj/--save-some-buffers-action "custom-buffer-file" (key)) +(declare-function cj/--save-some-buffers-choices "custom-buffer-file" ()) + +;;; --------------------- cj/--save-some-buffers-action ------------------------ +;; Each result is (THIS-ACTION . LOOP-EFFECT): +;; THIS-ACTION ∈ save | clean-save | skip | diff +;; LOOP-EFFECT ∈ continue | save-rest | stop | reprompt + +(ert-deftest test-cbf-ssb-action-save () + "Normal: y saves this buffer and continues prompting." + (should (equal (cj/--save-some-buffers-action ?y) '(save . continue)))) + +(ert-deftest test-cbf-ssb-action-skip () + "Normal: n skips this buffer and continues." + (should (equal (cj/--save-some-buffers-action ?n) '(skip . continue)))) + +(ert-deftest test-cbf-ssb-action-clean-save () + "Normal: w cleans whitespace, saves this buffer, and continues." + (should (equal (cj/--save-some-buffers-action ?w) '(clean-save . continue)))) + +(ert-deftest test-cbf-ssb-action-save-rest () + "Boundary: ! saves this buffer and all remaining without asking." + (should (equal (cj/--save-some-buffers-action ?!) '(save . save-rest)))) + +(ert-deftest test-cbf-ssb-action-save-this-stop () + "Boundary: . saves this buffer and skips the rest." + (should (equal (cj/--save-some-buffers-action ?.) '(save . stop)))) + +(ert-deftest test-cbf-ssb-action-quit () + "Boundary: q saves no more buffers (skips this and the rest)." + (should (equal (cj/--save-some-buffers-action ?q) '(skip . stop)))) + +(ert-deftest test-cbf-ssb-action-diff () + "Normal: d views the diff and re-prompts rather than resolving." + (should (equal (cj/--save-some-buffers-action ?d) '(diff . reprompt)))) + +(ert-deftest test-cbf-ssb-action-unknown () + "Error: an unmapped key returns nil." + (should-not (cj/--save-some-buffers-action ?z))) + +;;; --------------------- cj/--save-some-buffers-choices ----------------------- + +(ert-deftest test-cbf-ssb-choices-cover-all-keys () + "Normal: the choice list offers every save-loop key, each labeled." + (let ((choices (cj/--save-some-buffers-choices))) + (dolist (key '(?y ?n ?w ?d ?! ?. ?q)) + (let ((entry (assq key choices))) + (should entry) + ;; entry is (KEY NAME &optional DESC); NAME must be a non-empty label. + (should (stringp (nth 1 entry))) + (should (> (length (nth 1 entry)) 0)))))) + +(ert-deftest test-cbf-ssb-choices-terse-names () + "Boundary: inline names are single words so the menu takes minimum space." + (dolist (entry (cj/--save-some-buffers-choices)) + (let ((name (nth 1 entry))) + (should (stringp name)) + (should-not (string-match-p " " name))))) + +(ert-deftest test-cbf-ssb-choices-clean-mentions-whitespace () + "Normal: the clean-and-save choice is labeled with whitespace." + (let ((entry (assq ?w (cj/--save-some-buffers-choices)))) + (should (string-match-p "whitespace" + (mapconcat #'identity (cdr entry) " "))))) + +;;; ---------------------- cj/--save-some-buffers-plan ------------------------- +;; The pure planner: given the candidate BUFFERS and a KEY-FN that yields a +;; (non-diff) key per buffer, resolve each to `save' / `clean-save' / `skip', +;; honoring ! (save the rest) and . / q (stop after this). Buffers are opaque +;; here (symbols stand in), so the planner is testable without real buffers. + +(declare-function cj/--save-some-buffers-plan "custom-buffer-file" (buffers key-fn)) + +(ert-deftest test-cbf-ssb-plan-mixed () + "Normal: y / n / w resolve per-buffer to save / skip / clean-save." + (let* ((keys '((a . ?y) (b . ?n) (c . ?w))) + (plan (cj/--save-some-buffers-plan + '(a b c) (lambda (buf) (cdr (assq buf keys)))))) + (should (equal plan '((a . save) (b . skip) (c . clean-save)))))) + +(ert-deftest test-cbf-ssb-plan-save-rest () + "Boundary: ! saves this and all remaining without consulting KEY-FN again." + (let* ((asked nil) + (plan (cj/--save-some-buffers-plan + '(a b c) + (lambda (buf) (push buf asked) (if (eq buf 'a) ?! ?n))))) + (should (equal plan '((a . save) (b . save) (c . save)))) + ;; key-fn consulted only for the first buffer; the rest ride save-all. + (should (equal asked '(a))))) + +(ert-deftest test-cbf-ssb-plan-save-this-stop () + "Boundary: . saves this buffer and skips the rest." + (let ((plan (cj/--save-some-buffers-plan + '(a b c) (lambda (buf) (if (eq buf 'a) ?. ?y))))) + (should (equal plan '((a . save) (b . skip) (c . skip)))))) + +(ert-deftest test-cbf-ssb-plan-quit-skips-all () + "Boundary: q skips this buffer and all remaining." + (let ((plan (cj/--save-some-buffers-plan + '(a b c) (lambda (_) ?q)))) + (should (equal plan '((a . skip) (b . skip) (c . skip)))))) + +(ert-deftest test-cbf-ssb-plan-empty () + "Boundary: no candidate buffers yields an empty plan." + (should-not (cj/--save-some-buffers-plan '() (lambda (_) ?y)))) + +(provide 'test-custom-buffer-file--save-some-buffers) +;;; test-custom-buffer-file--save-some-buffers.el ends here diff --git a/themes/WIP-theme.el b/themes/WIP-theme.el index 843537d20..5bb25be8b 100644 --- a/themes/WIP-theme.el +++ b/themes/WIP-theme.el @@ -51,7 +51,7 @@ '(isearch-fail ((t (:foreground "#cb6b4d" :weight bold)))) '(show-paren-match ((t (:foreground "#100f0f" :background "#74932f")))) '(show-paren-mismatch ((t (:foreground "#edeff1" :background "#cb6b4d")))) - '(link ((t (:foreground "#5f8bf9" :underline t)))) + '(link ((t (:foreground "#5178db" :underline t)))) '(error ((t (:foreground "#cb6b4d" :weight bold)))) '(warning ((t (:foreground "#ab8d2e" :weight bold)))) '(success ((t (:foreground "#74932f" :weight bold)))) @@ -265,8 +265,6 @@ '(eat-term-color-magenta ((t (:foreground "#8255b5")))) '(eat-term-color-cyan ((t (:foreground "#88b2c3")))) '(eat-term-color-white ((t (:foreground "#bfc4d0")))) - '(eat-term-color-22 ((t (:foreground "#002f00")))) - '(eat-term-color-52 ((t (:foreground "#2f0000")))) '(eat-term-color-bright-black ((t (:foreground "#8e919a" :weight bold)))) '(eat-term-color-bright-red ((t (:foreground "#cb6b4d" :weight bold)))) '(eat-term-color-bright-green ((t (:foreground "#74932f" :weight bold)))) @@ -281,6 +279,8 @@ '(eat-shell-prompt-annotation-success ((t (:foreground "#74932f")))) '(eat-shell-prompt-annotation-running ((t (:foreground "#dab53d")))) '(eat-shell-prompt-annotation-failure ((t (:foreground "#a85b42")))) + '(eat-term-color-22 ((t (:foreground "#002f00")))) + '(eat-term-color-52 ((t (:foreground "#2f0000")))) '(auto-dim-other-buffers ((t (:foreground "#777980")))) '(auto-dim-other-buffers-hide ((t (:foreground "#0a0c0d")))) '(dashboard-banner-logo-title ((t (:inherit default :foreground "#dab53d" :background "#100f0f" :weight bold :slant italic :height 1.25)))) @@ -406,6 +406,13 @@ '(calibredb-mouse-face ((t (:background "#363638")))) '(calibredb-title-detailed-view-face ((t (:foreground "#dab53d" :weight bold)))) '(calibredb-edit-annotation-header-title-face ((t (:foreground "#bfc4d0" :weight bold)))) + '(cj/nov-reading-sepia ((t (:foreground "#ab8d2e")))) + '(cj/nov-reading-dark ((t (:foreground "#7c838a")))) + '(cj/nov-reading-light ((t (:foreground "#100f0f" :background "#7c838a")))) + '(cj/nov-reading-sepia-heading ((t (:inherit cj/nov-reading-sepia :foreground "#54677d" :height 1.2)))) + '(cj/nov-reading-sepia-link ((t (:foreground "#5178db" :underline t)))) + '(cj/nov-reading-dark-link ((t (:inherit cj/nov-reading-dark :foreground "#5178db" :underline t)))) + '(cj/nov-reading-light-link ((t (:foreground "#5178db" :underline t)))) '(erc-header-line ((t (:foreground "#e4eaf8" :background "#2f343a" :weight bold)))) '(erc-timestamp-face ((t (:foreground "#5e6770")))) '(erc-notice-face ((t (:foreground "#838d97")))) @@ -607,7 +614,7 @@ '(shr-h5 ((t (:foreground "#777980" :weight bold)))) '(shr-h6 ((t (:foreground "#606267" :weight bold)))) '(shr-text ((t (:foreground "#a9b2bb")))) - '(shr-link ((t (:foreground "#5f8bf9" :underline t)))) + '(shr-link ((t (:foreground "#5178db" :underline t)))) '(shr-selected-link ((t (:foreground "#777980" :weight bold :underline t)))) '(shr-code ((t (:foreground "#cb6b4d")))) '(shr-mark ((t (:foreground "#100f0f" :background "#dab53d")))) |
