From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!not-for-mail From: joaotavora@gmail.com (=?utf-8?B?Sm/Do28gVMOhdm9yYQ==?=) Newsgroups: gmane.emacs.devel Subject: [patch] make electric-pair-mode smarter/more useful Date: Fri, 06 Dec 2013 23:31:05 +0000 Message-ID: <87haalh806.fsf@gmail.com> NNTP-Posting-Host: plane.gmane.org Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Trace: ger.gmane.org 1386373215 29095 80.91.229.3 (6 Dec 2013 23:40:15 GMT) X-Complaints-To: usenet@ger.gmane.org NNTP-Posting-Date: Fri, 6 Dec 2013 23:40:15 +0000 (UTC) To: emacs-devel@gnu.org Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Sat Dec 07 00:40:20 2013 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([208.118.235.17]) by plane.gmane.org with esmtp (Exim 4.69) (envelope-from ) id 1Vp50N-00017c-Rr for ged-emacs-devel@m.gmane.org; Sat, 07 Dec 2013 00:40:20 +0100 Original-Received: from localhost ([::1]:33889 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1Vp50N-0002UE-HQ for ged-emacs-devel@m.gmane.org; Fri, 06 Dec 2013 18:40:19 -0500 Original-Received: from eggs.gnu.org ([2001:4830:134:3::10]:45400) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1Vp50F-0002U2-DD for emacs-devel@gnu.org; Fri, 06 Dec 2013 18:40:17 -0500 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1Vp508-0006hO-Jb for emacs-devel@gnu.org; Fri, 06 Dec 2013 18:40:11 -0500 Original-Received: from plane.gmane.org ([80.91.229.3]:58623) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1Vp508-0006cw-A1 for emacs-devel@gnu.org; Fri, 06 Dec 2013 18:40:04 -0500 Original-Received: from list by plane.gmane.org with local (Exim 4.69) (envelope-from ) id 1Vp507-0000y9-0C for emacs-devel@gnu.org; Sat, 07 Dec 2013 00:40:03 +0100 Original-Received: from 66.207.108.93.rev.vodafone.pt ([93.108.207.66]) by main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for ; Sat, 07 Dec 2013 00:40:02 +0100 Original-Received: from joaotavora by 66.207.108.93.rev.vodafone.pt with local (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for ; Sat, 07 Dec 2013 00:40:02 +0100 X-Injected-Via-Gmane: http://gmane.org/ Original-Lines: 223 Original-X-Complaints-To: usenet@ger.gmane.org X-Gmane-NNTP-Posting-Host: 66.207.108.93.rev.vodafone.pt User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/24.3.50 (gnu/linux) Cancel-Lock: sha1:q7Irjb3HJAy4NY64n1ZwFddb9nc= X-detected-operating-system: by eggs.gnu.org: Genre and OS details not recognized. X-Received-From: 80.91.229.3 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Original-Sender: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Xref: news.gmane.org gmane.emacs.devel:166181 Archived-At: 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