emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[patch] make electric-pair-mode smarter/more useful


From: João Távora
Subject: [patch] make electric-pair-mode smarter/more useful
Date: Fri, 06 Dec 2013 23:31:05 +0000
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/24.3.50 (gnu/linux)

Hi list,

In a recent cleanup I did of my autopair.el library [1], I decided to
try and add one of its core features to emacs's built-in
`electric-pair-mode' and make it the default behaviour.

The new functionality criteriously deciding when to automatically insert
a closer or when to skip the newly inserted character.

For those of you that might use autopair.el, this may be familiar. For
others, the best way to understand what it does is to try it out. For
others still, here's a quick summary ('|' marks point)

- typing (((( makes            ((((|))))
- typing )))) afterwards makes (((())))|
- if the buffer has too many closers an opener before them will *not*
  autopair
- if the buffer has too many openers a closer after them will *not*
  autoskip
- in a mixed parenthesis situation with []'s and ()'s it tries to do
  sensible things

The resulting behaviour can be described as something similar to
paredit.el's, but much less stringent, less surprising and works across
all modes and syntaxes. In my opinion, it's also a much better default
than `electric-pair-default-inhibit'.

The feature was surprisingly easy to implement using the existing
`electric-pair-inhibit-predicate' customization variable and only
slightly changing the semantics of the existing
`electric-pair-skip-self` variable.

I renamed the existing and default predicate
`electric-pair-default-inhibit` to `electric-pair-conservative-inhibit`.

There are also more features in autopair.el that could be worth adding
into electric.el, like autobackspacing two adjacent parens, chomping
whitespace forward on skip, smarter auto-wrapping of region, etc...

So, have a look at the patch. Its only slightly tested, but seems to be
OK.

João

[1] http://github.com/capitaomorte/autopair

diff --git a/lisp/electric.el b/lisp/electric.el
index 91b99b4..16cc028 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -331,28 +331,37 @@ insert a character from `electric-indent-chars'."
   :version "24.1"
   :type '(repeat (cons character character)))

-(defcustom electric-pair-skip-self t
+(defcustom electric-pair-skip-self #'electric-pair-skip-if-helps-balance
   "If non-nil, skip char instead of inserting a second closing paren.
+
+Can also be a function of one argument (the closer char just
+inserted). If the function returns non-nil
+
 When inserting a closing paren character right before the same character,
 just skip that character instead, so that hitting ( followed by ) results
 in \"()\" rather than \"())\".
 This can be convenient for people who find it easier to hit ) than C-f."
   :version "24.1"
-  :type 'boolean)
+  :type '(choice
+          (const :tag "Always skip" t)
+          (const :tag "Never skip" nil)
+          (const :tag "Help balance" electric-pair-skip-if-helps-balance)
+          function))

 (defcustom electric-pair-inhibit-predicate
-  #'electric-pair-default-inhibit
+  #'electric-pair-inhibit-if-helps-balance
   "Predicate to prevent insertion of a matching pair.
 The function is called with a single char (the opening char just inserted).
 If it returns non-nil, then `electric-pair-mode' will not insert a matching
 closer."
   :version "24.4"
   :type '(choice
-          (const :tag "Default" electric-pair-default-inhibit)
+          (const :tag "Conservative" electric-pair-conservative-inhibit)
+          (const :tag "Help balance" electric-pair-inhibit-if-helps-balance)
           (const :tag "Always pair" ignore)
           function))

-(defun electric-pair-default-inhibit (char)
+(defun electric-pair-conservative-inhibit (char)
   (or
    ;; I find it more often preferable not to pair when the
    ;; same char is next.
@@ -378,6 +387,118 @@ closer."
        (electric-pair-mode nil))
     (self-insert-command 1)))

+(defun electric-pair--pair-of (char)
+  "Return pair of CHAR if it has parenthesis or delimiter syntax."
+  (and (memq (char-syntax char) '(?\( ?\) ?\" ?\$))
+       (cdr (aref (syntax-table) char))))
+
+(defun electric-pair--find-pair (direction)
+  "Compute (MATCHED THERE HERE) for the pair of the delimiter at point.
+
+With positive DIRECTION consider the delimiter after point and
+travel forward, otherwise consider the delimiter is just before
+point and travel backward.
+
+MATCHED indicates if the found pair a perfect matcher, THERE and
+HERE are buffer positions."
+  (let ((here (point)))
+    (condition-case move-err
+        (save-excursion
+          (forward-sexp (if (> direction 0) 1 -1))
+          (list (if (> direction 0)
+                    (eq (char-after here)
+                        (electric-pair--pair-of (char-before (point))))
+                  (eq (char-before here)
+                      (electric-pair--pair-of (char-after (point)))))
+                (point) here))
+      (scan-error
+       (list nil (nth 2 move-err) here)))))
+
+(defun electric-pair--up-list (&optional n)
+  "Try to up-list forward as much as N lists.
+
+With negative N, up-list backward. N default to `point-max'.
+
+Return a cons of two descritions (MATCHED START END) for the
+innermost and outermost lists that enclose point. The outermost
+list enclosing point is either the first top-level or mismatched
+list found by uplisting."
+  (save-excursion
+    (let ((n (or n (point-max)))
+          (i 0)
+          innermost outermost)
+      (while (and (< i n)
+                  (not outermost))
+        (condition-case forward-err
+            (progn
+              (scan-sexps (point) (if (> n 0)
+                                      (point-max)
+                                    (- (point-max))))
+              (unless innermost
+                (setq innermost (list t)))
+              (setq outermost (list t)))
+          (scan-error
+           (goto-char
+            (if (> n 0)
+                ;; HACK: the reason for this `max' is that some
+                ;; modes like ruby-mode sometimes mis-report the
+                ;; scan error when `forward-sexp'eeing too-much, its
+                ;; (nth 3) should at least one greater than its (nth
+                ;; 2). We really need to move out of the sexp so
+                ;; detect this and add 1. If this were fixed we
+                ;; could move to (nth 3 forward-err) in all
+                ;; situations.
+                ;;
+                (max (1+ (nth 2 forward-err))
+                     (nth 3 forward-err))
+              (nth 3 forward-err)))
+           (let ((pair-data (electric-pair--find-pair (- n))))
+             (unless innermost
+               (setq innermost pair-data))
+             (unless (nth 0 pair-data)
+               (setq outermost pair-data))))))
+      (cons innermost outermost))))
+
+(defun electric-pair-inhibit-if-helps-balance (char)
+  "Return non-nil if auto-pairing of CHAR would hurt parentheses' balance.
+
+Works by first removing the character from the buffer, then doing
+some list calculations then restoring the situation as if nothing
+happened."
+  (unwind-protect
+      (progn
+        (delete-char -1)
+        (let* ((pair-data (electric-pair--up-list (point-max)))
+               (innermost (car pair-data))
+               (outermost (cdr pair-data))
+               (pair (and (nth 2 outermost)
+                          (char-before (nth 2 outermost)))))
+          (cond ((nth 0 outermost)
+                 nil)
+                ((not (nth 0 innermost))
+                 (eq pair (electric-pair--pair-of char))))))
+    (insert-char char)))
+
+(defun electric-pair-skip-if-helps-balance (char)
+  "Return non-nil if skipping CHAR would benefit parentheses' balance.
+
+Works by first removing the character from the buffer, then doing
+some list calculations then restoring the situation as if nothing
+happened."
+  (unwind-protect
+      (progn
+        (delete-char -1)
+        (let* ((pair-data (electric-pair--up-list (- (point-max))))
+               (innermost (car pair-data))
+               (outermost (cdr pair-data))
+               (pair (and (nth 2 outermost)
+                          (char-after (nth 2 outermost)))))
+          (cond ((nth 0 outermost)
+                 (nth 0 innermost))
+                ((not (nth 0 innermost))
+                 (not (eq pair (electric-pair--pair-of char)))))))
+    (insert-char char)))
+
 (defun electric-pair-post-self-insert-function ()
   (let* ((pos (and electric-pair-mode (electric--after-char-pos)))
         (syntax (and pos (electric-pair-syntax last-command-event)))
@@ -412,7 +533,9 @@ closer."
       nil)
      ;; Skip self.
      ((and (memq syntax '(?\) ?\" ?\$))
-           electric-pair-skip-self
+           (if (functionp electric-pair-skip-self)
+               (funcall electric-pair-skip-self last-command-event)
+             electric-pair-skip-self)
            (eq (char-after pos) last-command-event))
       ;; This is too late: rather than insert&delete we'd want to only skip (or
       ;; insert in overwrite mode).  The difference is in what goes in the




reply via email to

[Prev in Thread] Current Thread [Next in Thread]