aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-13 01:31:32 -0500
committerCraig Jennings <c@cjennings.net>2026-06-13 01:31:32 -0500
commitd9c90e83b6ae6525fa733116edbe7634f143fd92 (patch)
tree5032c9e90721f6714fcc5a1179043f9070e8af6c
parenta631938c95ee7974a1d5489ad3aba713c4f0a0d5 (diff)
downloaddotemacs-d9c90e83b6ae6525fa733116edbe7634f143fd92.tar.gz
dotemacs-d9c90e83b6ae6525fa733116edbe7634f143fd92.zip
fix(snippets): stop electric-pair from stranding ">" after "<"-key snippets
Most of the yasnippet keys start with "<" (<cj, <for, <main, ...), mirroring org-tempo. electric-pair-mode pairs "<" into "<>" wherever the mode's syntax table gives "<" paren syntax, which org and the pairing-enabled prog modes both do. So typing "<cj" lands as "<cj>", and expanding the "<cj" key strands the ">" after the snippet. The cj-comment block came out with a "#+end_src>" close fence, which breaks the cj-scan fence parser and made every respond-to-cj-comments pass hand-parse around it. I set electric-pair-inhibit-predicate globally to inhibit the open angle bracket and defer to the default for every other character. That keeps the "<"-prefixed snippet convention intact across all 14 of them, at the cost of "<>" auto-pairing where it might otherwise be wanted (C++ templates). The snippet convention is universal, so it wins. Surfaced from the smoke project, which kept hitting the malformed fence in its todo.org.
-rw-r--r--modules/prog-general.el16
-rw-r--r--tests/test-prog-general--electric-pair-angle.el54
2 files changed, 70 insertions, 0 deletions
diff --git a/modules/prog-general.el b/modules/prog-general.el
index a4be7205..8b4dedda 100644
--- a/modules/prog-general.el
+++ b/modules/prog-general.el
@@ -298,6 +298,22 @@ This is what makes universal snippets like =<cj= work in any buffer."
(yas-reload-all)
(yas-global-mode 1))
+;; Most of the snippet keys start with "<" (=<cj=, =<for=, =<main=…), mirroring
+;; org-tempo. But `electric-pair-mode' pairs "<" into "<>" wherever the mode's
+;; syntax table gives "<" paren syntax (org, and the prog modes that enable
+;; pairing), so typing "<cj" lands as "<cj>"; expanding the "<cj" key then
+;; strands the ">" after the snippet — the cj-comment fence comes out as
+;; "#+end_src>", which breaks the cj-scan fence parser. Inhibit pairing for the
+;; open angle bracket globally; defer to the default for every other character.
+(defun cj/--electric-pair-inhibit-angle (char)
+ "Return non-nil to stop `electric-pair-mode' from pairing the angle CHAR.
+Inhibit the open angle bracket so \"<\"-prefixed yasnippet keys expand cleanly;
+defer to `electric-pair-default-inhibit' for any other CHAR."
+ (or (eq char ?<)
+ (electric-pair-default-inhibit char)))
+
+(setq electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle)
+
;; --------------------- Display Color On Color Declaration --------------------
;; display the actual color as highlight to color hex code
diff --git a/tests/test-prog-general--electric-pair-angle.el b/tests/test-prog-general--electric-pair-angle.el
new file mode 100644
index 00000000..cb33725a
--- /dev/null
+++ b/tests/test-prog-general--electric-pair-angle.el
@@ -0,0 +1,54 @@
+;;; test-prog-general--electric-pair-angle.el --- Angle-bracket pairing inhibit -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/--electric-pair-inhibit-angle, which stops electric-pair from
+;; pairing "<" into "<>". Craig's yasnippet keys start with "<" (e.g. <cj);
+;; auto-pairing the "<" strands a ">" after the expanded snippet, which broke
+;; the cj-comment close fence into "#+end_src>".
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'elec-pair)
+(require 'org)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'prog-general)
+
+;;; cj/--electric-pair-inhibit-angle
+
+(ert-deftest test-prog-general-electric-pair-inhibit-angle-open ()
+ "Normal: the open angle bracket is inhibited."
+ (should (cj/--electric-pair-inhibit-angle ?<)))
+
+(ert-deftest test-prog-general-electric-pair-inhibit-angle-delegates ()
+ "Boundary: any other character defers to electric-pair-default-inhibit."
+ (cl-letf (((symbol-function 'electric-pair-default-inhibit)
+ (lambda (_c) 'delegated)))
+ (should (eq (cj/--electric-pair-inhibit-angle ?a) 'delegated))
+ (should (eq (cj/--electric-pair-inhibit-angle ?\() 'delegated))))
+
+(ert-deftest test-prog-general-electric-pair-predicate-installed ()
+ "Normal: prog-general installs the predicate as the global value."
+ (should (eq electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle)))
+
+;;; Integration — the actual pairing behavior
+
+(ert-deftest test-integration-prog-general-angle-not-paired-in-org ()
+ "Integration: in an org buffer (where < has paren syntax), typing < with the
+inhibit predicate active inserts just <, not <>.
+
+Components integrated:
+- cj/--electric-pair-inhibit-angle (real)
+- electric-pair-local-mode / self-insert-command (real)
+- org-mode syntax table (real — gives < paren syntax)"
+ (with-temp-buffer
+ (org-mode)
+ (electric-pair-local-mode 1)
+ (setq-local electric-pair-inhibit-predicate #'cj/--electric-pair-inhibit-angle)
+ (let ((last-command-event ?<))
+ (call-interactively #'self-insert-command))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max)) "<"))))
+
+(provide 'test-prog-general--electric-pair-angle)
+;;; test-prog-general--electric-pair-angle.el ends here