Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 107 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,107 @@
.PHONY: test
test:
emacs -batch -l ert -l undo-propose.el -l undo-propose-test.el -f ert-run-tests-batch-and-exit
.PHONY: help compile test checkdoc lint ci clean

.DEFAULT_GOAL := help

# Package information
PACKAGE := undo-propose
VERSION := $(shell perl -ne 'if (/^;;\s*Version:\s*(\S+)/) {print $$1; last}' $(PACKAGE).el)
TEST_COUNT := $(shell grep -c 'ert-deftest' tests/$(PACKAGE)-tests.el 2>/dev/null || echo 0)

# Emacs command
EMACS ?= emacs
BATCH := $(EMACS) --batch

# All Elisp source files
EL_FILES := $(PACKAGE).el $(wildcard tests/*.el)

help:
@echo "$(PACKAGE) v$(VERSION) - Makefile targets"
@echo ""
@echo "Development:"
@echo " make compile Byte-compile the package"
@echo " make clean Remove generated files"
@echo ""
@echo "Quality checks:"
@echo " make test Run ERT unit tests ($(TEST_COUNT) tests)"
@echo " make checkdoc Check documentation strings"
@echo " make lint Run package-lint (requires network)"
@echo ""
@echo "CI:"
@echo " make ci Run all"
@echo ""

$(PACKAGE).elc: $(PACKAGE).el
@echo "Byte-compiling $(PACKAGE)..."
@$(BATCH) --eval "(setq byte-compile-error-on-warn t)" \
-f batch-byte-compile $(PACKAGE).el
@echo "✓ Compilation successful"

compile: $(PACKAGE).elc

test: $(PACKAGE).elc
@if [ -f tests/$(PACKAGE)-tests.el ]; then \
$(BATCH) -l ert -l $(PACKAGE).el \
-l tests/$(PACKAGE)-tests.el \
-f ert-run-tests-batch-and-exit; \
else \
echo "No tests found."; \
fi

checkdoc: $(PACKAGE).elc
@echo "Running checkdoc on $(PACKAGE).el..."
@$(BATCH) --eval "\
(progn \
(require 'checkdoc) \
(let ((checkdoc-diagnostic-buffer \"*chk*\") \
(issues 0) (output \"\")) \
(checkdoc-file \"$(PACKAGE).el\") \
(when (get-buffer \"*chk*\") \
(with-current-buffer \"*chk*\" \
(unless (zerop (buffer-size)) \
(setq issues (count-lines (point-min) (point-max))) \
(setq output (buffer-string))))) \
(when (get-buffer \"*Warnings*\") \
(with-current-buffer \"*Warnings*\" \
(unless (zerop (buffer-size)) \
(setq issues (+ issues (count-lines (point-min) (point-max)))) \
(setq output (concat output (buffer-string)))))) \
(if (> issues 0) \
(progn \
(message \"checkdoc: %d issue(s) found in $(PACKAGE).el:\" issues) \
(message \"%s\" output) \
(kill-emacs 1)) \
(message \"checkdoc: $(PACKAGE).el OK (no issues)\"))))"

lint: $(PACKAGE).elc
@echo "Running package-lint checks..."
@$(BATCH) \
--eval "(require 'package)" \
--eval "(push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives)" \
--eval "(package-initialize)" \
--eval "(condition-case err \
(unless (package-installed-p 'package-lint) \
(package-refresh-contents) \
(package-install 'package-lint)) \
(error \
(message \"ERROR: Failed to install package-lint: %s\" (error-message-string err)) \
(message \"Hint: check your network connection and MELPA availability.\") \
(kill-emacs 1)))" \
--eval "(require 'package-lint)" \
-f package-lint-batch-and-exit $(PACKAGE).el
@echo "✓ package-lint passed"


ci: clean compile checkdoc lint test
@echo ""
@echo "✓ All CI checks passed!"
@echo " - Byte compilation: OK"
@echo " - Documentation (checkdoc): OK"
@echo " - Lint (package-lint): OK"
@echo " - Tests: $(TEST_COUNT)/$(TEST_COUNT) passing"
@echo ""
@echo "Package $(PACKAGE) v$(VERSION) - ready for release"

clean:
@echo "Cleaning generated files..."
@rm -f *.elc *~
@echo "✓ Clean complete"
14 changes: 14 additions & 0 deletions Readme.org → README.org
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ individual undo events (squashing them into a single edit event in the
undo history). To cancel, type ~C-c C-k~. You can also ediff the
proposed chain of undo's by typing ~C-c C-d~.

*** Read-only buffers

The ~undo-propose~ also works on read-only buffers (e.g., =magit-diff=,
=help-mode=). This provides a handy way to create a temporary
reference copy of any buffer. The snapshot can be kept open as a
reference while continuing to work in other buffers.

** Configuration
*** Adding commands (e.g. redo)

Expand Down Expand Up @@ -135,6 +142,13 @@ bugs that corrupt the undo history.
- [[https://www.reddit.com/r/emacs/comments/6yzwic/how_emacs_undo_works/][reddit.com/r/emacs: How Emacs undo works]]

** Changes
*** 5.0.0

Requires Emacs 29.1+ and adds a new dependency on =transient= (0.3.0+).

A transient help menu is now available in the undo-propose buffer via
~?~ or ~h~, listing all available commands.

*** 4.0.0

Switched to using the ~display-buffer~ framework to configure window
Expand Down
211 changes: 211 additions & 0 deletions tests/undo-propose-tests.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
;;; undo-propose-tests.el --- Tests for undo-propose -*- lexical-binding: t -*-

(require 'ert)
(require 'undo-propose)

;;; Code:

;;; Helpers

(defmacro with-undoable-temp-buffer (&rest body)
"Like `with-temp-buffer', but doesn't disable `undo'.
Executes BODY in the temporary buffer."
`(let ((temp-buffer (generate-new-buffer
;; this must NOT start with a space, otherwise
;; undo won't work. See `get-buffer-create'
"*temp*")))
(unwind-protect
(with-current-buffer temp-buffer
,@body)
(kill-buffer temp-buffer))))

(defmacro with-undo-propose-test (initial &rest body)
"Set up a temp buffer with INITIAL text, open undo-propose, run BODY.
BODY runs inside the undo-propose buffer. The original buffer is
bound to `orig-buf'."
(declare (indent 1))
`(with-undoable-temp-buffer
(insert ,initial)
(undo-boundary)
(let ((orig-buf (current-buffer)))
(undo-propose)
,@body)))

;;; commit

(ert-deftest undo-propose-commit-copies-buffer ()
"Committing copies the proposed buffer content back to the parent."
(with-undo-propose-test "hello"
(let ((inhibit-read-only t))
(erase-buffer)
(insert "world"))
(undo-propose-commit)
(should (equal (buffer-string) "world"))))

(ert-deftest undo-propose-commit-copies-undo-ring ()
"Committing copies the undo-ring back so changes can be undone."
(with-undo-propose-test "first"
(call-interactively (command-remapping 'undo))
(undo-propose-commit)
;; after commit, parent should be undoable back to empty
(undo)
(should (equal (buffer-string) ""))))

(ert-deftest undo-propose-commit-restores-point ()
"Committing restores point to the position in the propose buffer."
(with-undo-propose-test "hello world"
(goto-char 6)
(undo-propose-commit)
(should (= (point) 6))))

(ert-deftest undo-propose-commit-kills-propose-buffer ()
"Committing kills the temporary undo-propose buffer."
(with-undo-propose-test "hello"
(let ((propose-buf (current-buffer)))
(undo-propose-commit)
(should-not (buffer-live-p propose-buf)))))

;;; squash-commit

(ert-deftest undo-propose-squash-commit-copies-buffer ()
"Squash-committing copies buffer content to the parent."
(with-undo-propose-test "hello"
(call-interactively (command-remapping 'undo))
(undo-propose-squash-commit)
(should (equal (buffer-string) ""))))

(ert-deftest undo-propose-squash-commit-does-not-copy-undo-ring ()
"Squash-committing does NOT copy the propose buffer's undo-ring to the parent."
(with-undo-propose-test "first"
(call-interactively (command-remapping 'undo))
;; capture propose buffer's undo-list after undoing
(let ((propose-undo-list buffer-undo-list))
(undo-propose-squash-commit)
;; parent undo-list must differ from propose buffer's undo-list
(with-current-buffer orig-buf
(should-not (equal buffer-undo-list propose-undo-list))))))

(ert-deftest undo-propose-squash-commit-noop-when-unchanged ()
"Squash-committing without undoing leaves the parent buffer unchanged."
(with-undo-propose-test "hello"
(let ((undo-list-before buffer-undo-list))
(undo-propose-squash-commit)
(should (equal (buffer-string) "hello"))
(should (equal buffer-undo-list undo-list-before)))))

;;; cancel

(ert-deftest undo-propose-cancel-leaves-parent-unchanged ()
"Cancelling does not modify the parent buffer content."
(with-undo-propose-test "original"
(let ((inhibit-read-only t))
(erase-buffer)
(insert "discarded"))
(undo-propose-cancel)
(with-current-buffer orig-buf
(should (equal (buffer-string) "original")))))

(ert-deftest undo-propose-cancel-kills-propose-buffer ()
"Cancelling kills the temporary undo-propose buffer."
(with-undo-propose-test "hello"
(let ((propose-buf (current-buffer)))
(undo-propose-cancel)
(should-not (buffer-live-p propose-buf)))))

;;; nested call

(ert-deftest undo-propose-nested-call-runs-undo ()
"Calling undo-propose inside an undo-propose buffer runs undo instead."
(with-undo-propose-test "hello"
(let ((inhibit-read-only t))
(insert " world")
(undo-boundary))
;; second call should undo the " world" insertion
(undo-propose)
(should (equal (buffer-string) "hello"))
(undo-propose-cancel)))

;;; hooks

(ert-deftest undo-propose-entry-hook-runs-on-entry ()
"`undo-propose-entry-hook' fires when entering the propose buffer."
(with-undoable-temp-buffer
(let ((fired nil))
(add-hook 'undo-propose-entry-hook (lambda () (setq fired t)))
(unwind-protect
(progn
(undo-propose)
(should fired)
(undo-propose-cancel))
(remove-hook 'undo-propose-entry-hook (lambda () (setq fired t)))))))

(ert-deftest undo-propose-done-hook-runs-on-commit ()
"`undo-propose-done-hook' fires after committing."
(with-undo-propose-test "hello"
(let ((fired nil))
(add-hook 'undo-propose-done-hook (lambda () (setq fired t)))
(unwind-protect
(progn
(undo-propose-commit)
(should fired))
(remove-hook 'undo-propose-done-hook (lambda () (setq fired t)))))))

(ert-deftest undo-propose-done-hook-runs-on-cancel ()
"`undo-propose-done-hook' fires even when cancelling."
(with-undo-propose-test "hello"
(let ((fired nil))
(add-hook 'undo-propose-done-hook (lambda () (setq fired t)))
(unwind-protect
(progn
(undo-propose-cancel)
(should fired))
(remove-hook 'undo-propose-done-hook (lambda () (setq fired t)))))))

;;; read-only source (reference snapshot)

(ert-deftest undo-propose-readonly-source-sets-flag ()
"Opening undo-propose on a buffer with no undo history sets the snapshot flag."
(let ((buf (generate-new-buffer "*ro-test*")))
(unwind-protect
(with-current-buffer buf
(setq buffer-undo-list t)
(undo-propose)
(should undo-propose-read-only-source)
(undo-propose-cancel))
(when (buffer-live-p buf) (kill-buffer buf)))))

(ert-deftest undo-propose-readonly-source-allows-undo-in-propose ()
"In snapshot mode the propose buffer has its own undo history."
(let ((buf (generate-new-buffer "*ro-test*")))
(unwind-protect
(with-current-buffer buf
(setq buffer-undo-list t)
(undo-propose)
(let ((inhibit-read-only t))
(insert "typed in propose")
(undo-boundary))
(call-interactively (command-remapping 'undo))
(should (equal (buffer-string) ""))
(undo-propose-cancel))
(when (buffer-live-p buf) (kill-buffer buf)))))

;;; org-clock marker integration (existing test, kept for regression)

(ert-deftest undo-propose-test-org-clock ()
(with-undoable-temp-buffer
(org-mode)
(insert "* test\n")
(undo-boundary)
(org-clock-in)
(undo-boundary)
(goto-char (point-max))
(insert "\nfoobar")
(undo-boundary)
(undo-propose)
(call-interactively (command-remapping 'undo))
(undo-propose-commit)
(org-clock-out)))

(provide 'undo-propose-tests)

;;; undo-propose-tests.el ends here
31 changes: 0 additions & 31 deletions undo-propose-test.el

This file was deleted.

Loading