branch: elpa/casual
commit e507877fef1f85c0906ebbcd97fd414c66106baf
Merge: f33c344d0f 7a16655df2
Author: Charles Choi <[email protected]>
Commit: GitHub <[email protected]>
Merge pull request #314 from kickingvegas/173-build-casual-ediff
Add Casual Ediff
---
docs/ediff.org | 113 ++++++++
docs/images/casual-ediff-basic-screenshot.png | Bin 0 -> 283120 bytes
docs/images/casual-ediff-merge-conflict.png | Bin 0 -> 313757 bytes
docs/images/casual-ediff-screenshot.png | Bin 0 -> 362160 bytes
lisp/Makefile | 5 +
lisp/Makefile-agenda.make | 1 +
lisp/Makefile-bibtex.make | 1 +
lisp/Makefile-bookmarks.make | 1 +
lisp/Makefile-calc.make | 1 +
lisp/Makefile-calendar.make | 1 +
lisp/Makefile-compile.make | 1 +
lisp/{Makefile-eshell.make => Makefile-ediff.make} | 9 +-
lisp/Makefile-editkit.make | 1 +
lisp/Makefile-elisp.make | 1 +
lisp/Makefile-eshell.make | 1 +
lisp/Makefile-help.make | 1 +
lisp/Makefile-ibuffer.make | 1 +
lisp/Makefile-image.make | 1 +
lisp/Makefile-isearch.make | 1 +
lisp/Makefile-make-mode.make | 1 +
lisp/Makefile-man.make | 1 +
lisp/Makefile-re-builder.make | 1 +
lisp/Makefile-timezone.make | 1 +
lisp/casual-ediff-settings.el | 100 +++++++
lisp/casual-ediff-utils.el | 242 +++++++++++++++++
lisp/casual-ediff.el | 295 +++++++++++++++++++++
tests/Makefile | 5 +
tests/casual-ediff-test-utils.el | 39 +++
tests/test-casual-ediff-settings.el | 60 +++++
tests/test-casual-ediff-utils.el | 98 +++++++
tests/test-casual-ediff.el | 56 ++++
tests/test-casual-eshell-settings.el | 1 +
tests/test-casual-eshell-utils.el | 4 -
tests/test-casual-timezone-utils.el | 9 +-
34 files changed, 1044 insertions(+), 9 deletions(-)
diff --git a/docs/ediff.org b/docs/ediff.org
new file mode 100644
index 0000000000..19b9ae9c24
--- /dev/null
+++ b/docs/ediff.org
@@ -0,0 +1,113 @@
+* Ediff
+#+CINDEX: Ediff
+#+VINDEX: casual-ediff-tmenu
+
+Casual Ediff is a user interface for Ediff ([[info:ediff#Top][Ediff]]), a
visual interface for the Unix diff and patch utilities. Casual Ediff strives to
improve the usability of Ediff by simplifying the following workflows:
+
+- Comparing a modified and uncommitted version-controlled file with its most
recent commit.
+- Resolving a merge conflicted file.
+
+Notable features of Casual Ediff include:
+
+- Context aware menu items.
+- Menu design tuned for side-by-side comparison.
+- For a merge conflict, the ability to resolve using both the conflicting diff
versions.
+
+Shown below is screenshot of the Casual Ediff in action for a
version-controlled file.
+
+[[file:images/casual-ediff-screenshot.png]]
+
+
+** Ediff Install
+:PROPERTIES:
+:CUSTOM_ID: ediff-install
+:END:
+
+#+CINDEX: Ediff Install
+
+In your initialization file, bind the Transient ~casual-ediff-tmenu~ to your
key binding of preference.
+
+#+begin_src elisp :lexical no
+ (casual-ediff-install) ; run this to enable Casual Ediff
+ (add-hook 'ediff-keymap-setup-hook
+ (lambda ()
+ (keymap-set ediff-mode-map "C-o" #'casual-ediff-tmenu)))
+#+end_src
+
+If the current buffer is loaded with a version-controlled file, then the
difference between that buffer's content with its most recent commit can be
seen with the command ~casual-ediff-revision~. It is often convenient to bind
this command. Shown below is an example of such a binding.
+
+#+begin_src elisp :lexical no
+ (keymap-global-set "<f15>" #'casual-ediff-revision)
+#+end_src
+
+Users who wish to call ~casual-ediff-revision~ via mouse can use the command
~casual-ediff-revision-from-menu~. An example of its use is shown in the source
example below (note this is a code fragment).
+
+#+begin_src elisp :lexical no
+ (easy-menu-add-item
+ menu nil
+ ["Ediff revision…"
+ casual-ediff-revision-from-menu
+ :visible (and (bound-and-true-p buffer-file-name)
+ (vc-registered (buffer-file-name)))
+ :help "Ediff this file with revision"])
+#+end_src
+
+
+#+TEXINFO: @subsubheading Ediff Variables
+
+Casual Ediff recommends the following variables be set as follows:
+
+| Variable | Value |
+|-----------------------------+---------------------------|
+| ediff-keep-variants | nil |
+| ediff-window-setup-function | ediff-setup-windows-plain |
+| ediff-split-window-function | split-window-horizontally |
+
+** Ediff Usage
+#+CINDEX: Ediff Usage
+
+[[file:images/casual-ediff-basic-screenshot.png]]
+
+Casual Ediff (~casual-ediff-tmenu~) is invoked from the Ediff control window,
typically using the binding {{{kbd(C-o)}}} (or your binding of preference).
+
+The menu comprises of sections laid out horizontally that correspond to source
of the diffs. The number of sections and their contents are context-dependent
on the type of content to be compared.
+
+The following sections are offered in the menu:
+
+- A :: Context-dependent commands for Ediff buffer A.
+- B :: Context-dependent commands for Ediff buffer B.
+- C :: Context-dependent commands for Ediff buffer C, if available.
+- Diff :: Ediff commands not related to a specific buffer.
+
+Navigation between diffs are bound to the {{{kbd(p)}}} and {{{kbd(n)}}} keys
for previous and next diff respectively.
+
+*** Ediff Basic Operation
+
+[[file:images/casual-ediff-basic-screenshot.png]]
+
+If two writeable files are compared, Casual Ediff provides the ability to
write diffs from either side A or B to each other. Unwanted changes to a side
can be restored.
+
+*** Comparing a Version-Controlled File
+
+[[file:images/casual-ediff-screenshot.png]]
+
+Casual Ediff provides a dedicated command to compare a version-controlled file
with its latest commit: ~casual-ediff-revision~. This command short-circuits
the prompts asked for by the command ~ediff-revision~ which asks for the
source file and the revision to compare it to. Call ~casual-ediff-revision~
from the window of a version-controlled file, typically by key binding of
preference.
+
+The A side will hold the last committed revision of a file. The B side will
hold current source file.
+
+Users desiring a mouse-driven approach to calling ~casual-ediff-revision~
should use the command ~casual-ediff-revision-from-menu~ invoked from either
the top-level or context menu.
+
+Note that both ~casual-ediff-revision~ and ~casual-ediff-revision-from-menu~
will checkpoint the window configuration before invoking the Ediff session and
restore it after the session is complete.
+
+*** Resolving a Merge Conflict
+
+[[file:images/casual-ediff-merge-conflict.png]]
+
+Magit offers a means of invoking Ediff to resolve a merge conflict via the
{{{kbd(e)}}} binding from the ~magit-status~ ([[info:magit#Status
Buffer][(magit) Status Buffer]]) window.
+
+Calling ~casual-ediff-tmenu~ will show the menu shown above. Note that in the
C section, there are commands to merge both the variant A and variant B diffs
using the bindings {{{kbd(mab)}}} or {{{kbd(mba)}}}, depending on the desired
order the diffs are to be merged.
+
+
+*** Ediff Unicode Symbol Support
+By enabling “{{{kbd(u)}}} Use Unicode Symbols” from the Settings menu, Casual
Ediff will use Unicode symbols as appropriate in its menus.
+
diff --git a/docs/images/casual-ediff-basic-screenshot.png
b/docs/images/casual-ediff-basic-screenshot.png
new file mode 100644
index 0000000000..1d70b4834d
Binary files /dev/null and b/docs/images/casual-ediff-basic-screenshot.png
differ
diff --git a/docs/images/casual-ediff-merge-conflict.png
b/docs/images/casual-ediff-merge-conflict.png
new file mode 100644
index 0000000000..d0220f71ed
Binary files /dev/null and b/docs/images/casual-ediff-merge-conflict.png differ
diff --git a/docs/images/casual-ediff-screenshot.png
b/docs/images/casual-ediff-screenshot.png
new file mode 100644
index 0000000000..45902acc31
Binary files /dev/null and b/docs/images/casual-ediff-screenshot.png differ
diff --git a/lisp/Makefile b/lisp/Makefile
index b26f8b2433..b65da829cc 100644
--- a/lisp/Makefile
+++ b/lisp/Makefile
@@ -26,6 +26,7 @@ calc-tests \
calendar-tests \
compile-tests \
dired-tests \
+ediff-tests \
editkit-tests \
elisp-tests \
eshell-tests \
@@ -70,6 +71,10 @@ compile-tests:
dired-tests:
$(MAKE) -C $(SRC_DIR) -f Makefile-dired.make tests
+.PHONY: ediff-tests
+ediff-tests:
+ $(MAKE) -C $(SRC_DIR) -f Makefile-ediff.make tests
+
.PHONY: editkit-tests
editkit-tests:
$(MAKE) -C $(SRC_DIR) -f Makefile-editkit.make tests
diff --git a/lisp/Makefile-agenda.make b/lisp/Makefile-agenda.make
index ad3896dd6f..8f990993d0 100644
--- a/lisp/Makefile-agenda.make
+++ b/lisp/Makefile-agenda.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-bibtex.make b/lisp/Makefile-bibtex.make
index ffc38630ca..4960bfb80a 100644
--- a/lisp/Makefile-bibtex.make
+++ b/lisp/Makefile-bibtex.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-bookmarks.make b/lisp/Makefile-bookmarks.make
index 5783f1d17c..e1716f617f 100644
--- a/lisp/Makefile-bookmarks.make
+++ b/lisp/Makefile-bookmarks.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-calc.make b/lisp/Makefile-calc.make
index 2021e916e4..8b9bb41b8e 100644
--- a/lisp/Makefile-calc.make
+++ b/lisp/Makefile-calc.make
@@ -53,6 +53,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transpose-frame-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(EMACS_ELPA_DIR)/magit-current \
-L $(EMACS_ELPA_DIR)/magit-section-current \
-L $(EMACS_ELPA_DIR)/dash-current \
diff --git a/lisp/Makefile-calendar.make b/lisp/Makefile-calendar.make
index 01caf35f33..1d80048aa2 100644
--- a/lisp/Makefile-calendar.make
+++ b/lisp/Makefile-calendar.make
@@ -27,6 +27,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transpose-frame-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(EMACS_ELPA_DIR)/magit-current \
-L $(EMACS_ELPA_DIR)/magit-section-current \
-L $(EMACS_ELPA_DIR)/dash-current \
diff --git a/lisp/Makefile-compile.make b/lisp/Makefile-compile.make
index ab09c7f592..6cf74554a0 100644
--- a/lisp/Makefile-compile.make
+++ b/lisp/Makefile-compile.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-eshell.make b/lisp/Makefile-ediff.make
similarity index 86%
copy from lisp/Makefile-eshell.make
copy to lisp/Makefile-ediff.make
index 349f653096..faa6772349 100644
--- a/lisp/Makefile-eshell.make
+++ b/lisp/Makefile-ediff.make
@@ -16,15 +16,16 @@
include Makefile--defines.make
-PACKAGE_NAME=casual-eshell
-ELISP_INCLUDES=casual-eshell-utils.el \
-casual-eshell-settings.el
+PACKAGE_NAME=casual-ediff
+ELISP_INCLUDES=casual-ediff-utils.el \
+casual-ediff-settings.el
ELISP_PACKAGES=
-ELISP_TEST_INCLUDES=casual-eshell-test-utils.el
+ELISP_TEST_INCLUDES=casual-ediff-test-utils.el
PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(EMACS_ELPA_DIR)/magit-current \
-L $(EMACS_ELPA_DIR)/magit-section-current \
-L $(EMACS_ELPA_DIR)/dash-current \
diff --git a/lisp/Makefile-editkit.make b/lisp/Makefile-editkit.make
index d75e1e9805..ab3350e96a 100644
--- a/lisp/Makefile-editkit.make
+++ b/lisp/Makefile-editkit.make
@@ -26,6 +26,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transpose-frame-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(EMACS_ELPA_DIR)/magit-current \
-L $(EMACS_ELPA_DIR)/magit-section-current \
-L $(EMACS_ELPA_DIR)/dash-current \
diff --git a/lisp/Makefile-elisp.make b/lisp/Makefile-elisp.make
index f9d2fb8466..45052935a4 100644
--- a/lisp/Makefile-elisp.make
+++ b/lisp/Makefile-elisp.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-eshell.make b/lisp/Makefile-eshell.make
index 349f653096..d3e178ea2f 100644
--- a/lisp/Makefile-eshell.make
+++ b/lisp/Makefile-eshell.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(EMACS_ELPA_DIR)/magit-current \
-L $(EMACS_ELPA_DIR)/magit-section-current \
-L $(EMACS_ELPA_DIR)/dash-current \
diff --git a/lisp/Makefile-help.make b/lisp/Makefile-help.make
index ae3368698c..8de0cca5ea 100644
--- a/lisp/Makefile-help.make
+++ b/lisp/Makefile-help.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-ibuffer.make b/lisp/Makefile-ibuffer.make
index 2521701ea9..d524770e6f 100644
--- a/lisp/Makefile-ibuffer.make
+++ b/lisp/Makefile-ibuffer.make
@@ -24,6 +24,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-image.make b/lisp/Makefile-image.make
index 7cb0c1f018..1f000821e9 100644
--- a/lisp/Makefile-image.make
+++ b/lisp/Makefile-image.make
@@ -24,6 +24,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-isearch.make b/lisp/Makefile-isearch.make
index bc761cbeb9..01ece3c994 100644
--- a/lisp/Makefile-isearch.make
+++ b/lisp/Makefile-isearch.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
.PHONY: tests compile regression
diff --git a/lisp/Makefile-make-mode.make b/lisp/Makefile-make-mode.make
index f567e868f0..26d3697949 100644
--- a/lisp/Makefile-make-mode.make
+++ b/lisp/Makefile-make-mode.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-man.make b/lisp/Makefile-man.make
index 76dd12ead4..5bb4f4a1d6 100644
--- a/lisp/Makefile-man.make
+++ b/lisp/Makefile-man.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-re-builder.make b/lisp/Makefile-re-builder.make
index 52ddc528df..ff2000bcee 100644
--- a/lisp/Makefile-re-builder.make
+++ b/lisp/Makefile-re-builder.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/Makefile-timezone.make b/lisp/Makefile-timezone.make
index b293c6824e..d8b2fb1c04 100644
--- a/lisp/Makefile-timezone.make
+++ b/lisp/Makefile-timezone.make
@@ -25,6 +25,7 @@ PACKAGE_PATHS= \
-L $(EMACS_ELPA_DIR)/compat-current \
-L $(EMACS_ELPA_DIR)/seq-current \
-L $(EMACS_ELPA_DIR)/transient-current \
+-L $(EMACS_ELPA_DIR)/cond-let-current \
-L $(CASUAL_LIB_LISP_DIR)
include Makefile--rules.make
diff --git a/lisp/casual-ediff-settings.el b/lisp/casual-ediff-settings.el
new file mode 100644
index 0000000000..f017da6633
--- /dev/null
+++ b/lisp/casual-ediff-settings.el
@@ -0,0 +1,100 @@
+;;; casual-ediff-settings.el --- Casual Ediff Settings -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+
+;;; Code:
+(require 'ediff)
+(require 'casual-lib)
+
+(transient-define-prefix casual-ediff-settings-tmenu ()
+ "Casual Ediff settings menu."
+ ["Casual Ediff: Settings"
+ [("k" "Keep Variants"
+ casual-ediff-customize-ediff-keep-variants
+ :description (lambda () (casual-lib-checkbox-label ediff-keep-variants
+ "Keep Variants")))
+
+ ("w" "Window Setup Function"
+ casual-ediff-customize-ediff-window-setup-function)
+
+ ("s" "Split Window Function"
+ casual-ediff-customize-ediff-split-window-function)]
+
+ [("G" "Ediff Group" casual-ediff-customize-group)
+ (casual-lib-customize-unicode)
+ (casual-lib-customize-hide-navigation)]]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("a" "About" casual-ediff-about :transient nil)
+ (casual-lib-quit-all)])
+
+(defun casual-ediff-customize-group ()
+ "Customize Ediff group."
+ (interactive)
+ (customize-group "ediff"))
+
+(defun casual-ediff-customize-ediff-keep-variants ()
+ "Customize `ediff-keep-variants'."
+ (interactive)
+ (customize-variable 'ediff-keep-variants))
+
+(defun casual-ediff-customize-ediff-window-setup-function ()
+ "Customize `ediff-window-setup-function'."
+ (interactive)
+ (customize-variable 'ediff-window-setup-function))
+
+(defun casual-ediff-customize-ediff-split-window-function ()
+ "Customize `ediff-split-window-function'."
+ (interactive)
+ (customize-variable 'ediff-split-window-function))
+
+(defun casual-ediff-about-ediff ()
+ "Casual Ediff is a Transient menu for Ediff.
+
+Learn more about using Casual Ediff at our discussion group on GitHub.
+Any questions or comments about it should be made there.
+URL `https://github.com/kickingvegas/casual/discussions'
+
+If you find a bug or have an enhancement request, please file an issue.
+Our best effort will be made to answer it.
+URL `https://github.com/kickingvegas/casual/issues'
+
+If you enjoy using Casual Ediff, consider making a modest financial
+contribution to help support its development and maintenance.
+URL `https://www.buymeacoffee.com/kickingvegas'
+
+Casual Ediff was conceived and crafted by Charles Choi in San Francisco,
+California.
+
+Thank you for using Casual Ediff.
+
+Always choose love."
+ (ignore))
+
+(defun casual-ediff-about ()
+ "About information for Casual Ediff."
+ (interactive)
+ (describe-function #'casual-ediff-about-ediff))
+
+(provide 'casual-ediff-settings)
+;;; casual-ediff-settings.el ends here
diff --git a/lisp/casual-ediff-utils.el b/lisp/casual-ediff-utils.el
new file mode 100644
index 0000000000..543a3712e4
--- /dev/null
+++ b/lisp/casual-ediff-utils.el
@@ -0,0 +1,242 @@
+;;; casual-ediff-utils.el --- Casual Eshell Utils -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(require 'ediff)
+(require 'casual-lib)
+
+;; these defvars are here to let cc-ediff-mode.el compile clean
+(defvar ediff-buffer-A)
+(defvar ediff-buffer-B)
+(defvar ediff-buffer-C)
+(defvar ediff-merge-job)
+(defvar ediff-ancestor-buffer)
+
+;; CC: I set my Ediff variables in `custom-set-variables'
+;; Use your own preference.
+;; '(ediff-keep-variants nil)
+;; '(ediff-split-window-function 'split-window-horizontally)
+;; '(ediff-window-setup-function 'ediff-setup-windows-plain)
+
+(defconst casual-ediff-unicode-db
+ '((:previous . '("↑" "Previous"))
+ (:next . '("↓" "Next"))
+ (:scroll-to-right . '("↤" "Scroll to right"))
+ (:scroll-to-left . '("↦" "Scroll to left"))
+ (:refresh . '("⟲" "Refresh")))
+
+ "Unicode symbol DB to use for Ediff Transient menus.")
+
+(defun casual-ediff-unicode-get (key)
+ "Lookup Unicode symbol for KEY in DB.
+
+- KEY symbol used to lookup Unicode symbol in DB.
+
+If the value of customizable variable `casual-lib-use-unicode'
+is non-nil, then the Unicode symbol is returned, otherwise a
+plain ASCII-range string."
+ (casual-lib-unicode-db-get key casual-ediff-unicode-db))
+
+
+;; -------------------------------------------------------------------
+;;; Format
+
+(defun casual-ediff--buffer-description (ebuf slot &optional extend)
+ "Format name of buffer EBUF in SLOT with option to EXTEND."
+ (let ((bufname (buffer-name ebuf))
+ (fwidth (frame-width)))
+ (casual-ediff--display-filename bufname slot extend fwidth)))
+
+(defun casual-ediff--display-filename (filename slot extend fwidth)
+ "Reformat FILENAME for Ediff display using SLOT, EXTEND, and FWIDTH."
+ (if (and (string-match "~\\([[:xdigit:]]*\\)~$" filename) (< fwidth 95))
+ (let* ((commit-hash (match-string 1 filename))
+ (short-hash (truncate-string-to-width commit-hash 7 0 nil t))
+ (filename (string-replace commit-hash short-hash filename)))
+ (format "%s: %s" slot filename))
+ (if extend
+ (let* ((bwidth (- (/ fwidth 2) 6))
+ (ext-fmt (concat "%s: " (format "%%-%ss" bwidth))))
+ (format ext-fmt slot filename))
+ (format "%s: %s" slot filename))))
+
+
+;; -------------------------------------------------------------------
+;;; Ediff Functions
+(defun casual-ediff-info ()
+ "Open Info for Ediff."
+ (interactive) (info "(ediff) Top"))
+
+(defvar casual-ediff--revision-session-p nil
+ "If t then `casual-ediff--internal-last-revision' has been called.
+This state variable is used to insert added behavior to the advised
+function `ediff-janitor'.")
+
+(defvar casual-ediff--installed-p nil
+ "If t then Casual Ediff is initialized.")
+
+(defun casual-ediff-revision-from-menu (e)
+ "Invoke `casual-ediff-revision' on E with variable `buffer-file-name'."
+ (interactive "e")
+ (ignore e)
+ (casual-ediff-revision))
+
+(defun casual-ediff-revision ()
+ "Run Ediff comparing current file with last committed version.
+
+This function handles the interactive concerns found in
+`ediff-revision'. This function will also test if a diff should apply to
+the current buffer."
+ (interactive)
+ (when (and (bound-and-true-p buffer-file-name)
+ (vc-registered (buffer-file-name)))
+ (if (and (buffer-modified-p)
+ (y-or-n-p (format "Buffer %s is modified. Save buffer? "
+ (buffer-name))))
+ (save-buffer (current-buffer)))
+ (message buffer-file-name)
+ (casual-ediff--internal-last-revision))
+
+ (cond ((not (bound-and-true-p buffer-file-name))
+ (message (concat (buffer-name) " is not a file that can be diffed.")))
+ ((not (vc-registered buffer-file-name))
+ (message (concat buffer-file-name " is not under version
control.")))))
+
+(defun casual-ediff--internal-last-revision ()
+ "Implementation of Ediff comparing current file with last committed version.
+
+This function handles the actual diff behavior called by `ediff-revision'."
+ (let ((rev1 "")
+ (rev2 ""))
+ (setq casual-ediff--revision-session-p t)
+ (ediff-load-version-control)
+ (funcall
+ (intern (format "ediff-%S-internal" ediff-version-control-package))
+ rev1 rev2 nil)))
+
+(defun casual-ediff-janitor (ask keep-variants)
+ "Kill buffers A, B, and, possibly, C, if these buffers aren't modified.
+In merge jobs, buffer C is not deleted here, but rather according to
+`ediff-quit-merge-hook'.
+ASK non-nil means ask the user whether to keep each unmodified buffer, unless
+KEEP-VARIANTS is non-nil, in which case buffers are never killed.
+A side effect of cleaning up may be that you should be careful when comparing
+the same buffer in two separate Ediff sessions: quitting one of them might
+delete this buffer in another session as well.
+
+!!!: This method overrides the original Ediff function."
+ (let ((ask (if (and (boundp 'casual-ediff--revision-session-p)
+ casual-ediff--revision-session-p)
+ nil
+ ask)))
+ (ediff-dispose-of-variant-according-to-user
+ ediff-buffer-A 'A ask keep-variants)
+ ;; !!!: Test global state variable `casual-ediff--revision-session-p' to
+ ;; determine if the modified repo file should be kept.
+ ;; Guarding in place to hopefully avoid side-effects when `ediff-janitor'
is
+ ;; called from other Ediff functions. Informal testing has not revealed any
+ ;; side-effects but YOLO.
+ (if (and (boundp 'casual-ediff--revision-session-p)
+ casual-ediff--revision-session-p)
+ (ediff-dispose-of-variant-according-to-user
+ ;; CC Note: keep-variants argument is hard-coded to t to keep
+ ;; buffer holding modified repo file around.
+ ediff-buffer-B 'B t t)
+ (ediff-dispose-of-variant-according-to-user
+ ediff-buffer-B 'B ask keep-variants))
+ (if ediff-merge-job ; don't del buf C if merging--del ancestor buf instead
+ (ediff-dispose-of-variant-according-to-user
+ ediff-ancestor-buffer 'Ancestor ask keep-variants)
+ (ediff-dispose-of-variant-according-to-user
+ ediff-buffer-C 'C ask keep-variants))
+ ;; CC Note: Reset global state variable `casual-ediff--revision-session-p'.
+ (if (and (boundp 'casual-ediff--revision-session-p)
+ casual-ediff--revision-session-p)
+ (setq casual-ediff--revision-session-p nil))))
+
+
+(defun casual-ediff--stash-window-configuration-for-ediff ()
+ "Store window configuration to register 🧊.
+
+Use of emoji is to avoid potential use of keyboard character to reference
+the register."
+ (window-configuration-to-register ?🧊))
+
+(defun casual-ediff--restore-window-configuration-for-ediff ()
+ "Restore window configuration from register 🧊.
+
+Use of emoji is to avoid potential use of keyboard character to reference
+the register."
+ (jump-to-register ?🧊))
+
+(defun casual-ediff--restore-and-save-diff (key)
+ "Restore diff and save Ediff buffer referenced by KEY."
+ (ediff-restore-diff nil key)
+ (casual-ediff--save-buffer key))
+
+(defun casual-ediff--save-buffer (key)
+ "Save Ediff buffer referenced by KEY."
+ (setq last-command-event key)
+ (ediff-save-buffer nil))
+
+(defun casual-ediff--buffer-read-only-p (buf)
+ "Predicate to test if BUF is read-only."
+ (with-current-buffer buf
+ (if buffer-read-only t nil)))
+
+(defun casual-ediff--split-window-vertically-description ()
+ "Provide string label for state of `split-window-vertically'."
+ (if (eq ediff-split-window-function 'split-window-vertically)
+ "Side by side"
+ "On top"))
+
+;; The implementations of `casual-ediff-copy-AB-to-C' and
+;; `casual-ediff-copy-BA-to-C' are adapted from code written by killdash9 from
+;; the following Stack Overflow post.
+;;
https://stackoverflow.com/questions/9656311/conflict-resolution-with-emacs-ediff-how-can-i-take-the-changes-of-both-version/29757750#29757750
+
+(defun casual-ediff-copy-AB-to-C ()
+ "Resolve merge conflict by inserting difference from buffer A then buffer B."
+ (interactive)
+ (ediff-copy-diff
+ ediff-current-difference nil 'C nil
+ (concat
+ (ediff-get-region-contents
+ ediff-current-difference 'A ediff-control-buffer)
+ (ediff-get-region-contents
+ ediff-current-difference 'B ediff-control-buffer))))
+
+(defun casual-ediff-copy-BA-to-C ()
+ "Resolve merge conflict by inserting difference from buffer B then buffer A."
+ (interactive)
+ (ediff-copy-diff
+ ediff-current-difference nil 'C nil
+ (concat
+ (ediff-get-region-contents
+ ediff-current-difference 'B ediff-control-buffer)
+ (ediff-get-region-contents
+ ediff-current-difference 'A ediff-control-buffer))))
+
+(provide 'casual-ediff-utils)
+;;; casual-ediff-utils.el ends here
diff --git a/lisp/casual-ediff.el b/lisp/casual-ediff.el
new file mode 100644
index 0000000000..2dd6ff965d
--- /dev/null
+++ b/lisp/casual-ediff.el
@@ -0,0 +1,295 @@
+;;; casual-ediff.el --- Transient UI for Eshell -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides a Transient-based user interface for Ediff.
+
+;; INSTALLATION
+
+;; To install Casual Ediff, the function `casual-ediff-install' should be
called
+;; from your Emacs initialization file. You will also need to bind
+;; `casual-ediff-tmenu' to your key binding of preference.
+
+;; (require 'casual-ediff) ; optional if using autoloaded menu
+;; (casual-ediff-install) ; run this to enable Casual Ediff
+;; (add-hook 'ediff-keymap-setup-hook
+;; (lambda ()
+;; (keymap-set ediff-mode-map "C-o" #'casual-ediff-tmenu)))
+
+;; Notes
+;; - `casual-ediff-install' will:
+;; * override advise the function `ediff-janitor'.
+;; * in-memory set the variable `ediff-window-setup-function' to plain.
+;; - The way Ediff handles keymaps necessitates the configuration of
+;; `ediff-keymap-setup-hook' as shown above.
+
+;;; Code:
+(require 'casual-ediff-settings)
+(require 'casual-ediff-utils)
+
+;;;###autoload (autoload 'casual-ediff-tmenu "casual-ediff" nil t)
+(transient-define-prefix casual-ediff-tmenu ()
+ :refresh-suffixes t
+ [["A"
+ :pad-keys t
+ :description (lambda () (casual-ediff--buffer-description
+ ediff-buffer-A
+ "A"
+ (not ediff-buffer-C)))
+ ("ab" "A→B" ediff-copy-A-to-B
+ :transient t
+ :if (lambda () (and
+ (not ediff-buffer-C)
+ ediff-buffer-B
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-B)))))
+
+ ("ac" "A→C" ediff-copy-A-to-C
+ :transient t
+ :if (lambda () (and
+ ediff-buffer-C
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-C)))))
+
+ ("ra" "Restore A"
+ (lambda ()
+ "Restore and save prior state of buffer A."
+ (interactive)
+ (casual-ediff--restore-and-save-diff ?a))
+ :transient t
+ :if (lambda () (and
+ (or ediff-buffer-B ediff-buffer-C)
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-A)))))
+
+ ("wa" "Save A"
+ (lambda ()
+ "Save buffer A."
+ (interactive)
+ (casual-ediff--save-buffer ?a))
+ :transient t
+ :if (lambda () (and
+ (or ediff-buffer-B ediff-buffer-C)
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-A))))
+ :inapt-if-not (lambda () (buffer-modified-p ediff-buffer-A)))]
+
+ ;; !!!: A diff B
+ ["Diff"
+ :pad-keys t
+ :if (lambda () (not ediff-buffer-C))
+ ("p" "↑" ediff-previous-difference
+ :description (lambda () (casual-ediff-unicode-get :previous))
+ :transient t)
+ ("n" "↓" ediff-next-difference
+ :description (lambda () (casual-ediff-unicode-get :next))
+ :transient t)
+
+ ("<" "↤" ediff-scroll-horizontally
+ :description (lambda () (casual-ediff-unicode-get :scroll-to-right))
+ :transient t)
+ (">" "↦" ediff-scroll-horizontally
+ :description (lambda () (casual-ediff-unicode-get :scroll-to-left))
+ :transient t)
+
+ ("!" "⟲" ediff-update-diffs
+ :description (lambda () (casual-ediff-unicode-get :refresh))
+ :transient t)
+
+ ("|" "H/V" ediff-toggle-split
+ :transient t
+ :description casual-ediff--split-window-vertically-description)
+ ("#" "Skip 𝑤𝑠" ediff-toggle-skip-similar
+ :transient t
+ :description (lambda ()
+ (casual-lib-checkbox-label ediff-ignore-similar-regions
+ "Skip Space")))]
+
+ ["B"
+ :pad-keys t
+ :description (lambda () (casual-ediff--buffer-description ediff-buffer-B
"B"))
+ ("ba" "A←B" ediff-copy-B-to-A
+ :transient t
+ :if (lambda () (and
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-A)))))
+
+ ("bc" "B→C" ediff-copy-B-to-C
+ :transient t
+ :if (lambda () (and
+ ediff-buffer-C
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-C)))))
+
+ ("rb" "Restore B"
+ (lambda ()
+ "Restore and save prior state of buffer B."
+ (interactive)
+ (casual-ediff--restore-and-save-diff ?b))
+ :transient t
+ :if (lambda () (and
+ ediff-buffer-B
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-B))
+ (not (string= (file-name-extension (buffer-name
ediff-buffer-B))
+ "~{index}")))))
+
+ ("wb" "Save B"
+ (lambda ()
+ "Save buffer B."
+ (interactive)
+ (casual-ediff--save-buffer ?b))
+ :transient t
+ :if (lambda () (and
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-B))
+ (not (string= (file-name-extension (buffer-name
ediff-buffer-B))
+ "~{index}"))))
+ :inapt-if-not (lambda () (buffer-modified-p ediff-buffer-B)))]
+
+ ;; !!!: A B diff C
+ ["Diff"
+ :pad-keys t
+ :if (lambda () (and ediff-buffer-C t))
+ ("p" "↑" ediff-previous-difference
+ :description (lambda () (casual-ediff-unicode-get :previous))
+ :transient t)
+ ("n" "↓" ediff-next-difference
+ :description (lambda () (casual-ediff-unicode-get :next))
+ :transient t)
+
+ (">" "↤" ediff-scroll-horizontally
+ :description (lambda () (casual-ediff-unicode-get :scroll-to-right))
+ :transient t)
+ ("<" "↦" ediff-scroll-horizontally
+ :description (lambda () (casual-ediff-unicode-get :scroll-to-left))
+ :transient t)
+
+ ("!" "⟲" ediff-update-diffs
+ :description (lambda () (casual-ediff-unicode-get :refresh))
+ :transient t)
+ ("|" "H/V" ediff-toggle-split
+ :description casual-ediff--split-window-vertically-description
+ :transient t)
+ ("#" "Skip 𝑤𝑠" ediff-toggle-skip-similar
+ :transient t
+ :description (lambda ()
+ (casual-lib-checkbox-label ediff-ignore-similar-regions
+ "Skip Space")))]
+
+ ["C"
+ :pad-keys t
+ :if (lambda () (if ediff-buffer-C t nil))
+ :description (lambda () (casual-ediff--buffer-description ediff-buffer-C
"C"))
+ ("cb" "B←C" ediff-copy-C-to-B
+ :transient t
+ :if (lambda () (and
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-B))
+ (not (string= (file-name-extension (buffer-name
ediff-buffer-B))
+ "~{index}")))))
+
+ ("ca" "A←C" ediff-copy-C-to-A
+ :transient t
+ :if (lambda () (and
+ (not (casual-ediff--buffer-read-only-p ediff-buffer-A)))))
+
+ ("mab" "Merge A,B to C" casual-ediff-copy-AB-to-C
+ :transient t
+ :if (lambda () (string-equal (buffer-name ediff-buffer-C)
"*ediff-merge*")))
+
+ ("mba" "Merge B,A to C" casual-ediff-copy-BA-to-C
+ :transient t
+ :if (lambda () (string-equal (buffer-name ediff-buffer-C)
"*ediff-merge*")))
+
+ ("rc" "Restore C"
+ (lambda ()
+ "Restore and save prior state of buffer C."
+ (interactive)
+ (casual-ediff--restore-and-save-diff ?c))
+ :transient t
+ :if (lambda ()
+ (if (not (casual-ediff--buffer-read-only-p ediff-buffer-C))
+ (if (not (string= (buffer-name ediff-buffer-C) "*ediff-merge*"))
+ t
+ nil)
+ nil))
+ :inapt-if-not (lambda () (buffer-modified-p ediff-buffer-C)))
+
+ ("rm" "Restore Prior to Last Merge"
+ (lambda ()
+ "Call `ediff-restore-diff-in-merge-buffer' using current diff.
+
+Note that this command will restore only the state of the *ediff-merge*
+buffer prior to the previous merge. To avoid any changes to the
+conflicted file, exit Ediff and when prompted to save the merge file,
+reply with no."
+ (interactive)
+ (ediff-restore-diff-in-merge-buffer nil))
+ :transient t
+ :if (lambda ()
+ (if (not (casual-ediff--buffer-read-only-p ediff-buffer-C))
+ (if (string= (buffer-name ediff-buffer-C) "*ediff-merge*")
+ t
+ nil)
+ nil)))]]
+
+ [:class transient-row
+ (casual-lib-quit-one)
+ ("i" "Status" ediff-status-info)
+ ("I" "ⓘ" ediff-documentation)
+ ("," "Settings" casual-ediff-settings-tmenu)
+ ("q" "Quit Ediff" ediff-quit)])
+
+;;;###autoload (autoload 'casual-ediff-install "casual-ediff" nil t)
+(defun casual-ediff-install ()
+ "Install Casual Ediff."
+ (interactive)
+ (setq casual-ediff--installed-p t)
+
+ ;; CC: I set my Ediff variables in `custom-set-variables'
+ ;; Use your own preference.
+ ;; '(ediff-keep-variants nil)
+ ;; '(ediff-split-window-function 'split-window-horizontally)
+ ;; '(ediff-window-setup-function 'ediff-setup-windows-plain)
+
+ (unless (eq ediff-window-setup-function #'ediff-setup-windows-plain)
+ (message
+ "Overriding ediff-window-setup-function to ediff-setup-windows-plain. \
+Consider customizing to always set this variable to plain.")
+ (setq ediff-window-setup-function #'ediff-setup-windows-plain))
+
+ (add-hook
+ 'ediff-before-setup-hook
+ #'casual-ediff--stash-window-configuration-for-ediff)
+ (add-hook
+ 'ediff-after-quit-hook-internal
+ #'casual-ediff--restore-window-configuration-for-ediff)
+ (advice-add 'ediff-janitor :override #'casual-ediff-janitor))
+
+;;;###autoload (autoload 'casual-ediff-uninstall "casual-ediff" nil t)
+(defun casual-ediff-uninstall ()
+ "Uninstall Casual Ediff."
+ (interactive)
+ (advice-remove 'ediff-janitor #'casual-ediff-janitor)
+
+ (remove-hook
+ 'ediff-before-setup-hook
+ #'casual-ediff--stash-window-configuration-for-ediff)
+ (remove-hook
+ 'ediff-after-quit-hook-internal
+ #'casual-ediff--restore-window-configuration-for-ediff)
+
+ (setq casual-ediff--installed-p nil))
+
+(provide 'casual-ediff)
+;;; casual-ediff.el ends here
diff --git a/tests/Makefile b/tests/Makefile
index fa1ef9c71a..82b98bed4e 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -25,6 +25,7 @@ calc-tests \
calendar-tests \
compile-tests \
dired-tests \
+ediff-tests \
editkit-tests \
elisp-tests \
eshell-tests \
@@ -47,6 +48,7 @@ calc-tests \
calendar-tests \
compile-tests \
dired-tests \
+ediff-tests \
editkit-tests \
elisp-tests \
eshell-tests \
@@ -83,6 +85,9 @@ compile-tests:
dired-tests:
$(MAKE) -C $(SRC_DIR) $@
+ediff-tests:
+ $(MAKE) -C $(SRC_DIR) $@
+
editkit-tests:
$(MAKE) -C $(SRC_DIR) $@
diff --git a/tests/casual-ediff-test-utils.el b/tests/casual-ediff-test-utils.el
new file mode 100644
index 0000000000..e7ec8e9a9f
--- /dev/null
+++ b/tests/casual-ediff-test-utils.el
@@ -0,0 +1,39 @@
+;;; casual-ediff-test-utils.el --- Casual Test Utils -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'casual-lib)
+(require 'kmacro)
+
+(defun casualt-ediff-setup ()
+ "Casual Ediff setup."
+ )
+
+(defun casualt-ediff-breakdown ()
+ "Casual man breakdown."
+ )
+
+(provide 'casual-ediff-test-utils)
+;;; casual-ediff-test-utils.el ends here
diff --git a/tests/test-casual-ediff-settings.el
b/tests/test-casual-ediff-settings.el
new file mode 100644
index 0000000000..6c9f3dee6f
--- /dev/null
+++ b/tests/test-casual-ediff-settings.el
@@ -0,0 +1,60 @@
+;;; test-casual-ediff-settings.el --- Casual Elisp Settings Tests -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(require 'ert)
+(require 'casual-ediff-test-utils)
+(require 'casual-lib-test-utils)
+(require 'casual-ediff-settings)
+
+(ert-deftest test-casual-ediff-settings-tmenu ()
+ (let ()
+ (cl-letf ((casualt-mock #'casual-ediff-customize-group)
+ (casualt-mock #'casual-ediff-customize-ediff-keep-variants)
+ (casualt-mock
#'casual-ediff-customize-ediff-split-window-function)
+ (casualt-mock
#'casual-ediff-customize-ediff-window-setup-function)
+ (casualt-mock #'casual-lib-customize-casual-lib-hide-navigation)
+ (casualt-mock #'casual-lib-customize-casual-lib-use-unicode)
+ (casualt-mock #'casual-ediff-about))
+
+ (let ((test-vectors
+ '(
+ (:binding "k" :command
casual-ediff-customize-ediff-keep-variants)
+ (:binding "w" :command
casual-ediff-customize-ediff-window-setup-function)
+ (:binding "s" :command
casual-ediff-customize-ediff-split-window-function)
+ (:binding "G" :command casual-ediff-customize-group)
+ (:binding "u" :command
casual-lib-customize-casual-lib-use-unicode)
+ (:binding "n" :command
casual-lib-customize-casual-lib-hide-navigation)
+ (:binding "a" :command casual-ediff-about))))
+
+ (casualt-suffix-testcase-runner test-vectors
+ #'casual-ediff-settings-tmenu
+ '(lambda () (random 5000)))))))
+
+(ert-deftest test-casual-ediff-about ()
+ (should (stringp (casual-ediff-about))))
+
+(provide 'test-casual-ediff-settings)
+;;; test-casual-ediff-setttings.el ends here
diff --git a/tests/test-casual-ediff-utils.el b/tests/test-casual-ediff-utils.el
new file mode 100644
index 0000000000..32570958d9
--- /dev/null
+++ b/tests/test-casual-ediff-utils.el
@@ -0,0 +1,98 @@
+;;; test-casual-ediff-utils.el --- Casual Make Utils Tests -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'casual-ediff-test-utils)
+(require 'casual-ediff-utils)
+
+(ert-deftest test-casual-ediff-unicode-get ()
+ (let ((casual-lib-use-unicode nil))
+ (should (string-equal (casual-ediff-unicode-get :previous) "Previous"))
+ (should (string-equal (casual-ediff-unicode-get :next) "Next"))
+ (should (string-equal (casual-ediff-unicode-get :scroll-to-right) "Scroll
to right"))
+ (should (string-equal (casual-ediff-unicode-get :scroll-to-left) "Scroll
to left"))
+ (should (string-equal (casual-ediff-unicode-get :refresh) "Refresh")))
+
+ (let ((casual-lib-use-unicode t))
+ (should (string-equal (casual-ediff-unicode-get :previous) "↑"))
+ (should (string-equal (casual-ediff-unicode-get :next) "↓"))
+ (should (string-equal (casual-ediff-unicode-get :scroll-to-right) "↤"))
+ (should (string-equal (casual-ediff-unicode-get :scroll-to-left) "↦"))
+ (should (string-equal (casual-ediff-unicode-get :refresh) "⟲"))))
+
+
+(ert-deftest test-casual-ediff--display-filename ()
+ (let* ((control "A: fred.foo")
+ (filename "fred.foo")
+ (slot "A")
+ (extend nil)
+ (fwidth 120)
+ (result (casual-ediff--display-filename filename slot extend fwidth)))
+ (should (string-equal control result)))
+
+ (let* ((control "B: fred.foo")
+ (filename "fred.foo")
+ (slot "B")
+ (extend nil)
+ (fwidth 120)
+ (result (casual-ediff--display-filename filename slot extend fwidth)))
+ (should (string-equal control result)))
+
+ (let* ((control "B: fred.foo ")
+ (filename "fred.foo")
+ (slot "B")
+ (extend t)
+ (fwidth 120)
+ (result (casual-ediff--display-filename filename slot extend fwidth)))
+ (should (string-equal control result)))
+
+ (let* ((control "B: fred.foo.~ABCDEF…~")
+ (filename "fred.foo.~ABCDEF0123456~")
+ (slot "B")
+ (extend t)
+ (fwidth 80)
+ (result (casual-ediff--display-filename filename slot extend fwidth)))
+ (should (string-equal control result)))
+
+ (let* ((control
+ "B: fred.foo.~ABCDEF0123456~
")
+ (filename "fred.foo.~ABCDEF0123456~")
+ (slot "B")
+ (extend t)
+ (fwidth 180)
+ (result (casual-ediff--display-filename filename slot extend fwidth)))
+ (should (string-equal control result)))
+
+ (let* ((control
+ "B: fred.foo.~ABCDEF0123456~")
+ (filename "fred.foo.~ABCDEF0123456~")
+ (slot "B")
+ (extend nil)
+ (fwidth 180)
+ (result (casual-ediff--display-filename filename slot extend fwidth)))
+ (should (string-equal control result))))
+
+(provide 'test-casual-ediff-utils)
+;;; test-casual-ediff-utils.el ends here
diff --git a/tests/test-casual-ediff.el b/tests/test-casual-ediff.el
new file mode 100644
index 0000000000..dd5c1e1af8
--- /dev/null
+++ b/tests/test-casual-ediff.el
@@ -0,0 +1,56 @@
+;;; test-casual-ediff.el --- Casual Make Tests -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Charles Y. Choi
+
+;; Author: Charles Choi <[email protected]>
+;; Keywords: tools
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(require 'ert)
+(require 'casual-ediff-test-utils)
+(require 'casual-lib-test-utils)
+(require 'casual-ediff)
+
+;; Gah, testing casual-ediff-tmenu might be too hard.
+;; Resorting to manual testing.
+
+;; (ert-deftest test-casual-ediff-tmenu ()
+;; (let ((tmpfile "casual-ediff-tmenu.txt"))
+;; (casualt-ediff-setup)
+;; (cl-letf (;;((symbol-function #') (lambda () t))
+;; (casualt-mock #'ediff-previous-difference)
+;; (casualt-mock #'ediff-next-difference)
+;; ;;(casualt-mock #')
+;; )
+
+;; (let ((test-vectors
+;; '((:binding "p" :command ediff-previous-difference)
+;; (:binding "n" :command ediff-next-difference)
+;; ;;(:binding "" :command ediff-main-)
+;; )))
+
+;; (casualt-suffix-testcase-runner test-vectors
+;; #'casual-ediff-tmenu
+;; '(lambda () (random 5000)))))
+;; (casualt-ediff-breakdown)))
+
+(provide 'test-casual-ediff)
+;;; test-casual-ediff.el ends here
diff --git a/tests/test-casual-eshell-settings.el
b/tests/test-casual-eshell-settings.el
index 267873ee41..33e9240e78 100644
--- a/tests/test-casual-eshell-settings.el
+++ b/tests/test-casual-eshell-settings.el
@@ -26,6 +26,7 @@
(require 'ert)
(require 'casual-eshell-test-utils)
+(require 'casual-lib-test-utils)
(require 'casual-eshell-settings)
(ert-deftest test-casual-eshell-settings-tmenu ()
diff --git a/tests/test-casual-eshell-utils.el
b/tests/test-casual-eshell-utils.el
index f54d492bdb..b1b2e5a75d 100644
--- a/tests/test-casual-eshell-utils.el
+++ b/tests/test-casual-eshell-utils.el
@@ -46,9 +46,5 @@
(should (string-equal (casual-eshell-unicode-get :clear) "⌫"))
))
-
-
-
-
(provide 'test-casual-eshell-utils)
;;; test-casual-eshell-utils.el ends here
diff --git a/tests/test-casual-timezone-utils.el
b/tests/test-casual-timezone-utils.el
index ee06be4998..5420381fb3 100644
--- a/tests/test-casual-timezone-utils.el
+++ b/tests/test-casual-timezone-utils.el
@@ -146,8 +146,15 @@
(should (string-equal control result))))
+(defun in-daylight-saving-time-p ()
+ "Return t if the current local time is in Daylight Saving Time (DST), nil
otherwise."
+ (let ((dst (nth 8 (decode-time (current-time)))))
+ (and dst (not (eq dst 0)))))
+
(ert-deftest test-casual-timezone-remote-time-to-local ()
- (let* ((read-date "2025-05-23 12:00")
+ (let* ((read-date (if (in-daylight-saving-time-p)
+ "2025-05-23 11:00"
+ "2025-05-23 12:00"))
(remote-tz "Europe/Berlin")
(control "2025-05-23 03:00:00 PDT")
(result (casual-timezone-remote-time-to-local read-date remote-tz)))