all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: joaotavora@gmail.com (João Távora)
To: emacs-devel@gnu.org
Subject: [patch] make electric-pair-mode smarter/more useful
Date: Fri, 06 Dec 2013 23:31:05 +0000	[thread overview]
Message-ID: <87haalh806.fsf@gmail.com> (raw)

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	other threads:[~2013-12-06 23:31 UTC|newest]

Thread overview: 36+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2013-12-06 23:31 João Távora [this message]
2013-12-07  2:09 ` [patch] make electric-pair-mode smarter/more useful Leo Liu
2013-12-07  2:36 ` Dmitry Gutov
2013-12-07 21:01   ` João Távora
2013-12-07 23:16     ` Stefan Monnier
2013-12-12  3:05       ` João Távora
2013-12-12  4:29         ` Dmitry Gutov
2013-12-12 11:26           ` João Távora
2013-12-12 16:30           ` Stefan Monnier
2013-12-12 17:06             ` João Távora
2013-12-12 20:12               ` Stefan Monnier
2013-12-13  2:55               ` Dmitry Gutov
2013-12-14 15:18                 ` Stefan Monnier
2013-12-14 16:56                   ` Dmitry Gutov
2013-12-15  1:39                     ` Stefan Monnier
2013-12-16  0:35                       ` João Távora
2013-12-16  3:34                         ` Stefan Monnier
2013-12-16 19:26                           ` João Távora
2013-12-17  1:54                             ` Stefan Monnier
2013-12-18  2:43                               ` João Távora
2013-12-18 15:32                                 ` João Távora
2013-12-23 14:41                                   ` João Távora
2013-12-24 14:29                                     ` Bozhidar Batsov
2013-12-07 23:07 ` Stefan Monnier
2013-12-12  3:01   ` João Távora
2013-12-12 18:08     ` Stefan Monnier
2013-12-13  1:02       ` João Távora
2013-12-13  2:32         ` Stefan Monnier
2013-12-15 22:10           ` João Távora
2013-12-16  3:22             ` Stefan Monnier
2013-12-16 14:21               ` João Távora
2013-12-16 15:30                 ` Stefan Monnier
2013-12-16 18:40                   ` Stefan Monnier
2013-12-16 19:06                     ` João Távora
2013-12-17  1:42                       ` Stefan Monnier
     [not found]                   ` <CALDnm52AoShN891-L9=Cbng98UtYPEntzO+n_XDMmEL+UV0r-A@mail.gmail.com>
2013-12-16 19:02                     ` Fwd: " João Távora

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=87haalh806.fsf@gmail.com \
    --to=joaotavora@gmail.com \
    --cc=emacs-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.