* [patch] make electric-pair-mode smarter/more useful
@ 2013-12-06 23:31 João Távora
2013-12-07 2:09 ` Leo Liu
` (2 more replies)
0 siblings, 3 replies; 36+ messages in thread
From: João Távora @ 2013-12-06 23:31 UTC (permalink / raw
To: emacs-devel
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
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-06 23:31 [patch] make electric-pair-mode smarter/more useful João Távora
@ 2013-12-07 2:09 ` Leo Liu
2013-12-07 2:36 ` Dmitry Gutov
2013-12-07 23:07 ` Stefan Monnier
2 siblings, 0 replies; 36+ messages in thread
From: Leo Liu @ 2013-12-07 2:09 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
On 2013-12-07 07:31 +0800, João Távora wrote:
> 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.
Thank you for taking the time to port new features to emacs's code base.
I hope more people do the same ;)
Leo
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-06 23:31 [patch] make electric-pair-mode smarter/more useful João Távora
2013-12-07 2:09 ` Leo Liu
@ 2013-12-07 2:36 ` Dmitry Gutov
2013-12-07 21:01 ` João Távora
2013-12-07 23:07 ` Stefan Monnier
2 siblings, 1 reply; 36+ messages in thread
From: Dmitry Gutov @ 2013-12-07 2:36 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
joaotavora@gmail.com (João Távora) writes:
> 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.
As a long-time autopair user, I heartily approve. Thanks!
> 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...
Autobackspacing two adjacent parens sounds good.
I'm also partial to the `autopair-newline' feature. It would probably
serve best as an extension of `electric-layout-mode'.
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-07 2:36 ` Dmitry Gutov
@ 2013-12-07 21:01 ` João Távora
2013-12-07 23:16 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-07 21:01 UTC (permalink / raw
To: Dmitry Gutov; +Cc: emacs-devel
Dmitry Gutov <dgutov@yandex.ru> writes:
> Autobackspacing two adjacent parens sounds good.
How should one approach this in electric.el? Should a hook be added to
`backward-delete-char-untabify`? Should this be included in the
`electric-layout-mode' somehow?
> I'm also partial to the `autopair-newline' feature. It would probably
> serve best as an extension of `electric-layout-mode'.
Yes, I agree that `electric-layout-mode' seems the place for this, but
how to write these rules in the existing `electric-layout-rules' var? It
seems a little too rigid, I couldn't make much sense of it.
(add-to-list 'electric-layout-rules
`(,(aref "\n" 0) . electric-layout-in-between-parenthesis))
(defun electric-layout-in-between-parenthesis ()
(save-excursion
(skip-chars-backward " \t\n")
(backward-char)
(when (looking-at "([ \t\n]*)")
'after)))
But it didn't really work... inserting "\n" between "()" leaves point
after the two newlines, not inside as intended. It would need to return
'after-then-back or something. Also, an undo after this doesn't restore
the "()" state... In general, looking at the code and reading the
comments, this might need to be enhanced considerably.
Also, regarding the autowrapping feature already found in electric.el's
electric-pair-mode
- I found (and reported) a bug when using cua-mode with it
- both openers and closers should cause a wrap (currently only
openers do). A closer should move point to after the wrapping.
- additionally one should be able to customize if points ends up
inside or outside the wrapped region.
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-06 23:31 [patch] make electric-pair-mode smarter/more useful João Távora
2013-12-07 2:09 ` Leo Liu
2013-12-07 2:36 ` Dmitry Gutov
@ 2013-12-07 23:07 ` Stefan Monnier
2013-12-12 3:01 ` João Távora
2 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-07 23:07 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
> 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.
Thank you, very appreciated.
> 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.
Overall, the integration looks very good, indeed.
> +(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))))
Hmmm... the existing code already has such a functionality. Can you try
and use your new function in that code, or somehow merge the two?
> +(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."
Could you try and use up-list, instead?
Also, you can find the START of all enclosing lists in (nth
9 (syntax-ppss)), which seems like it might be helpful here.
It would be good to try and avoid moving all the way to the START or END
of the outermost list, since that may require scanning the whole buffer,
which in pathological cases will make the feature slow. Maybe using
syntax-ppss could help us avoid those pathological cases (since
syntax-ppss uses a cache to avoid re-scanning).
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-07 23:16 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel, Dmitry Gutov
>> Autobackspacing two adjacent parens sounds good.
> How should one approach this in electric.el? Should a hook be added to
> `backward-delete-char-untabify`? Should this be included in the
> `electric-layout-mode' somehow?
We could remap backward-delete-char-untabify in electric-pair-mode-map.
>> I'm also partial to the `autopair-newline' feature. It would probably
>> serve best as an extension of `electric-layout-mode'.
> Yes, I agree that `electric-layout-mode' seems the place for this, but
> how to write these rules in the existing `electric-layout-rules' var?
Indeed, it doesn't really fit in there. You could probably hack it in
brute-force style by adding the newline directly from
electric-layout-in-between-parenthesis (and then return nil rather than
`after').
> - both openers and closers should cause a wrap (currently only
> openers do). A closer should move point to after the wrapping.
Patch welcome.
> - additionally one should be able to customize if points ends up
> inside or outside the wrapped region.
I don't see a strong need for such customization, but I wouldn't object.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-07 23:07 ` Stefan Monnier
@ 2013-12-12 3:01 ` João Távora
2013-12-12 18:08 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-12 3:01 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel
Stefan Monnier <monnier@iro.umontreal.ca> writes:
> Overall, the integration looks very good, indeed.
>
Thanks. So I went ahead and considerably the implementation along the
lines suggested. I'm quite happy with it having implemented most of
autopair's core features in a way that I still think is tightly
integrating with the existing electric.el backend.
I'd be happy to get comments and perform any adjustments.
Skip to to patch at the end of the mail or read some key points:
- pairing skipping of parens/quotes is balanced in both code, strings
and comments.
- electric backspacing was implemented per your remapping suggestion.
- autowrapping was enhanced to also happen on closers.
- there are just over 500 tests in
tests/automated/electric-pair-tests.el. Obviously I wrote some 30 or
40 and the rest are variations of in different major modes (elisp, c++
and ruby-mode), in strings and comments. Some js-mode tests are also
there for its use of `electric-layout-rules'.
- `electric-pair-pairs` now defaults to nil.
See its updated docstring for the reasoning and the new (akwardly
named) variables `electric-pair-non-code-pairs' and
`electric-pair-non-code-syntax-table' for slightly better, more
flexible alternatives to it, in my opinion.
Maybe it's built as overkill, but I could not find a simpler way to
get autopair.el's backtick-and-quote pairing for elisp comments and
strings...other than customizing electric-inhibit/skip callbacks in my
.emacs, of course.
But I do feel this also belongs electric.el, and was a common request
in autopair.el, which has even more flexibility for this (it has
:code, :string, :comments and :everywhere modifiers).
- `electric-pair--pair-of' and `electric-pair-syntax' have merged, as
you suggested, into `electric-pair-syntax-info'. This is also where
the new variables above come into play.
- there is a new `electric--sort-post-self-insertion-hook' function
called whenever a hook is added to `post-self-insert-hook'. As you had
already noted in the "FIXME: ugly!" note (now removed), the `append'
arg to `add-hook' doesn't cut it.
I read the other FIXME notes but didn't understand them so well. Can
you provide examples of the conflicts between `electric-indent-mode'
and other `electric-modes' that you mention there?
- I simplififed the previous `electric-pair--up-list' function and
renamed it `electric-pair--balance-info'. But I still couldn't make it
use `up-list' or get rid of some `forward-sexp'. `up-list' can
probably be done with enough care, but I think replacing
`forward-sexp' with `syntax-ppss' is only possible in the "pair", not
the "skip" case. And only when outside comments or strings.
I do agree that too many `forward-sexp' can be hazardous (I had some
bad reports in autopair.el), but i recall that it was only when the
number of calls is proportional to buffer size.
In this implementation, I think the the number of `forward-sexp''s is
at most proportional to the nesting depth, which is less
dangerous.
But anyway if you or someone more knowlegeable in emacs's syntax
parsing engine can figure out a way to make this and still pass all
the tests, all the better.
- some helper functions might be reinventing the wheel, such as
`electric-pair--looking-at-mismatched-string-p' and
`electric-pair--inside-comment-string'.
- I'm also trying my luck and changed lisp-mode.el defaults to
accomodate some of my preferred defaults in variables
`electric-pair-skip-whitespace' and `electric-pair-non-code-pairs').
>> +(defun electric-pair--pair-of (char)
> Hmmm... the existing code already has such a functionality. Can you try
> and use your new function in that code, or somehow merge the two?
Done, see above.
>> +(defun electric-pair--up-list (&optional n)
> Could you try and use up-list, instead?
I tried, I failed. See above.
> Also, you can find the START of all enclosing lists in (nth
> 9 (syntax-ppss)), which seems like it might be helpful here.
I tried, I removed one forward-sexp in the "pair" case. See inline
comments.
diff --git a/lisp/electric.el b/lisp/electric.el
index 91b99b4..b227e3d 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -187,6 +187,27 @@ Returns nil when we can't find this char."
(eq (char-before) last-command-event)))))
pos)))
+(defun electric--sort-post-self-insertion-hook ()
+ "Ensure order of electric functions in `post-self-insertion-hook'.
+
+Hooks in this variable interact in non-trivial ways, so a
+relative order must be maintained within it."
+ (let ((relative-order '(electric-pair-post-self-insert-function
+ electric-layout-post-self-insert-function
+ electric-indent-post-self-insert-function
+ blink-paren-post-self-insert-function)))
+ (setq post-self-insert-hook
+ (sort post-self-insert-hook
+ #'(lambda (fn1 fn2)
+ (let ((fn1-tail (memq fn1 relative-order))
+ (fn2-tail (memq fn2 relative-order)))
+ (cond ((and fn1-tail fn2-tail)
+ (> (length fn1-tail)
+ (length fn2-tail)))
+ (fn1-tail t)
+ (fn2-tail nil)
+ (t nil))))))))
+
;;; Electric indentation.
;; Autoloading variables is generally undesirable, but major modes
@@ -295,20 +316,9 @@ insert a character from `electric-indent-chars'."
#'electric-indent-post-self-insert-function))
(when (eq (lookup-key global-map [?\C-j]) 'newline-and-indent)
(define-key global-map [?\C-j] 'electric-indent-just-newline))
- ;; post-self-insert-hooks interact in non-trivial ways.
- ;; It turns out that electric-indent-mode generally works better if run
- ;; late, but still before blink-paren.
(add-hook 'post-self-insert-hook
- #'electric-indent-post-self-insert-function
- 'append)
- ;; FIXME: Ugly!
- (let ((bp (memq #'blink-paren-post-self-insert-function
- (default-value 'post-self-insert-hook))))
- (when (memq #'electric-indent-post-self-insert-function bp)
- (setcar bp #'electric-indent-post-self-insert-function)
- (setcdr bp (cons #'blink-paren-post-self-insert-function
- (delq #'electric-indent-post-self-insert-function
- (cdr bp))))))))
+ #'electric-indent-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)))
;;;###autoload
(define-minor-mode electric-indent-local-mode
@@ -326,33 +336,121 @@ insert a character from `electric-indent-chars'."
;;; Electric pairing.
(defcustom electric-pair-pairs
- '((?\" . ?\"))
- "Alist of pairs that should be used regardless of major mode."
+ '()
+ "Alist of pairs that should be used regardless of major mode.
+
+Pairs of delimiters in this list cannot be balanced automatically, so
+before adding to this variable, consider modifying your mode's syntax
+table.
+
+See also the variable `electric-pair-non-code-pairs'."
:version "24.1"
:type '(repeat (cons character character)))
-(defcustom electric-pair-skip-self t
+(defcustom electric-pair-non-code-pairs
+ '()
+ "Alist of pairs that should be used only when in strings or comments.
+
+Pairs of delimiters in this list cannot be balanced automatically, so
+before adding to this variable, consider modifying the \(buffer-local)
+value of the variable `electric-pair-non-code-syntax-table'."
+ :version "24.4"
+ :type '(repeat (cons character character)))
+
+(make-variable-buffer-local 'electric-pair-non-code-pairs)
+
+(defcustom electric-pair-skip-self #'electric-pair-skip-if-helps-balance
"If non-nil, skip char instead of inserting a second closing paren.
+
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."
+
+This can be convenient for people who find it easier to hit ) than C-f.
+
+Can also be a function of one argument (the closer char just
+inserted), in which case that function's return value is
+considered instead."
: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)
+(defcustom electric-pair-delete-adjacent-pairs t
+ "If non-nil, backspacing an open paren also deletes ajacent closer.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace t
+ "If non-nil skip whitespace when skipping over closing parens.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes, jump over whitespace" t)
+ (const :tag "Yes, and delete whitespace" 'chomp)
+ (const :tag "No, no whitespace skipping" nil)
+ function))
+
+(defvar electric-pair-non-code-syntax-table prog-mode-syntax-table
+ "Syntax table used when pairing inside comments and strings.
+
+`electric-pair-mode' considers this syntax table only when point in inside
+quotes or comments, and only after examining `electric-pair-pairs'.")
+
+(defun electric-pair-backward-delete-char (n &optional killflag untabify)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char' or, if
+UNTABIFY is non-nil, `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (let* ((prev (char-before))
+ (next (char-after))
+ (syntax-info (electric-pair-syntax-info prev))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (when (and (if (functionp electric-pair-delete-adjacent-pairs)
+ (funcall electric-pair-delete-adjacent-pairs)
+ electric-pair-delete-adjacent-pairs)
+ next
+ (memq syntax '(?\( ?\" ?\$))
+ (eq pair next))
+ (delete-char 1 killflag))
+ (if untabify
+ (backward-delete-char-untabify n killflag)
+ (backward-delete-char n killflag))))
+
+(defun electric-pair-backward-delete-char-untabify (n &optional killflag)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (electric-pair-backward-delete-char n killflag t))
+
+(defun electric-pair-conservative-inhibit (char)
(or
;; I find it more often preferable not to pair when the
;; same char is next.
@@ -363,14 +461,40 @@ closer."
;; I also find it often preferable not to pair next to a word.
(eq (char-syntax (following-char)) ?w)))
-(defun electric-pair-syntax (command-event)
- (let ((x (assq command-event electric-pair-pairs)))
+(defun electric-pair-syntax-info (command-event)
+ "Calculate a list (SYNTAX PAIR WHERE).
+
+SYNTAX is COMMAND-EVENT's syntax character, PAIR is its pair and WHERE is
+either a syntax table or `t', meaning \"everywhere\""
+ (let* ((pre-comment-or-string-p (save-excursion
+ (nth 8 (syntax-ppss (1- (point))))))
+ (post-comment-or-string-p (nth 8 (syntax-ppss)))
+ (comment-or-string-p
+ (and post-comment-or-string-p
+ pre-comment-or-string-p))
+ (direct (or (assq command-event electric-pair-pairs)
+ (and comment-or-string-p
+ (assq command-event electric-pair-non-code-pairs))))
+ (reverse (or (rassq command-event electric-pair-pairs)
+ (and comment-or-string-p
+ (rassq command-event electric-pair-non-code-pairs))))
+ (table (if comment-or-string-p
+ electric-pair-non-code-syntax-table
+ (syntax-table))))
(cond
- (x (if (eq (car x) (cdr x)) ?\" ?\())
- ((rassq command-event electric-pair-pairs) ?\))
- ((nth 8 (syntax-ppss))
- (with-syntax-table text-mode-syntax-table (char-syntax command-event)))
- (t (char-syntax command-event)))))
+ (direct (if (eq (car direct) (cdr direct))
+ (list ?\" command-event t)
+ (list ?\( (cdr direct) t)))
+ (reverse (list ?\) (car reverse) t))
+ (t
+ (with-syntax-table table
+ (list (char-syntax command-event)
+ (or (matching-paren command-event)
+ command-event)
+ table))))))
+
+(defun electric-pair--pair-of (char)
+ (cadr (electric-pair-syntax-info char)))
(defun electric-pair--insert (char)
(let ((last-command-event char)
@@ -378,56 +502,235 @@ closer."
(electric-pair-mode nil))
(self-insert-command 1)))
+(defun electric-pair--matched-p (here direction)
+ "Tell if the delimiter at point HERE is perfectly matched.
+
+With positive DIRECTION consider the delimiter after HERE and
+search forward, otherwise consider the delimiter is just before
+HERE and search backward."
+ ;; FIXME: Ideally no `forward-sexp'eeing should take place here, but
+ ;; we can only avoid it to find out if a sexp before point is
+ ;; matched. It won't work for one after point or one inside
+ ;; comments.
+ ;;
+ ;; We could also use `show-paren-data-function' here, it seems to
+ ;; always provide reliable results.
+ ;;
+ (cond ((> direction 0)
+ (condition-case move-err
+ (save-excursion
+ (forward-sexp 1)
+ (eq (char-after here)
+ (electric-pair--pair-of (char-before (point)))))
+ (scan-error nil)))
+ ((nth 8 (syntax-ppss))
+ (condition-case move-err
+ (save-excursion
+ (forward-sexp -1)
+ (eq (char-before here)
+ (electric-pair--pair-of (char-after (point)))))
+ (scan-error nil)))
+ (t
+ ;; we can use some `syntax-ppss' in this case,
+ ;; no need to `forward-sexp' back
+ (save-excursion
+ (goto-char (1- (point)))
+ (let ((start (car (nth 9 (syntax-ppss)))))
+ (eq (char-before here)
+ (electric-pair--pair-of (char-after start))))))))
+
+(defun electric-pair--balance-info (n)
+ "Examine lists forward or backward according to N's sign.
+
+Return a cons of two descritions (MATCHED . PAIR) 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.
+
+If the outermost list is matched, don't rely on its PAIR. If
+point is not enclosed by any lists, return ((T) (T))."
+ (save-excursion
+ (let (innermost outermost)
+ (while (not outermost)
+ (condition-case err
+ (progn
+ (scan-sexps (point) (if (> n 0)
+ (point-max)
+ (- (point-max))))
+ (setq outermost (list t))
+ (unless innermost
+ (setq innermost (list t))))
+ (scan-error
+ (goto-char (nth 3 err))
+ (let ((matched (electric-pair--matched-p (nth 3 err) (- n)))
+ (actual-pair (if (> n 0)
+ (char-before (point))
+ (char-after (point)))))
+ (unless innermost
+ (setq innermost (cons matched actual-pair)))
+ (unless matched
+ (setq outermost (cons matched actual-pair)))))))
+ (cons innermost outermost))))
+
+(defun electric-pair--looking-at-mismatched-string-p ()
+ "Say if the nearest string started after point is mismatched."
+ (save-excursion
+ (skip-syntax-forward "^?\"")
+ (while (not (zerop (% (save-excursion (skip-syntax-backward "\\")) 2)))
+ (unless (eobp)
+ (forward-char 1)
+ (skip-syntax-forward "^?\"")))
+ (and (not (eobp))
+ (condition-case err
+ (progn (forward-sexp) nil)
+ (scan-error
+ t)))))
+
+(defun electric-pair--inside-comment-string ()
+ "When inside a comment, say if point is inside a string."
+ ;; FIXME: ugly/naive
+ (save-excursion
+ (save-restriction
+ (narrow-to-region (nth 8 (syntax-ppss)) (point))
+ (let ((non-escaped-quotes 0))
+ (while (not (bobp))
+ (skip-syntax-backward "^?\"")
+ (unless (bobp)
+ (backward-char))
+ (when (and (not (bobp))
+ (zerop (% (save-excursion (skip-syntax-backward "\\")) 2)))
+ (setq non-escaped-quotes (1+ non-escaped-quotes))))
+ (not (zerop (% non-escaped-quotes 2)))))))
+
+(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, finally restoring the situation as if nothing
+happened."
+ (let* ((syntax-info (electric-pair-syntax-info char))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq ?\( syntax)
+ (let* ((pair-data (electric-pair--balance-info 1))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (cond ((car outermost)
+ nil)
+ ((not (car innermost))
+ (eq (cdr outermost) pair))
+ (t
+ t))))
+ ((eq syntax ?\")
+ (let ((string-start (nth 3 (syntax-ppss))))
+ (or (eq string-start t)
+ (eq string-start char)
+ (electric-pair--looking-at-mismatched-string-p))))))
+ (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, finally restoring the situation as if nothing
+happened."
+ (let* ((syntax-info (electric-pair-syntax-info char))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq syntax ?\))
+ (let* ((pair-data (electric-pair--balance-info -1))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (and
+ (cond ((car outermost)
+ (car innermost))
+ ((car innermost)
+ (not (eq (cdr outermost) pair)))))))
+ ((eq syntax ?\")
+ (let ((string-start (nth 3 (syntax-ppss))))
+ (or (eq string-start t)
+ (eq string-start char)
+ (not (electric-pair--looking-at-mismatched-string-p))
+ (and (nth 8 (syntax-ppss))
+ (electric-pair--inside-comment-string)))))))
+ (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)))
- (closer (if (eq syntax ?\()
- (cdr (or (assq last-command-event electric-pair-pairs)
- (aref (syntax-table) last-command-event)))
- last-command-event)))
+ (syntax-info (and pos (electric-pair-syntax-info last-command-event)))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info))
+ (table (or (caddr syntax-info) (syntax-table))))
(cond
((null pos) nil)
;; Wrap a pair around the active region.
- ((and (memq syntax '(?\( ?\" ?\$)) (use-region-p))
+ ;;
+ ((and (memq syntax '(?\( ?\) ?\" ?\$)) (use-region-p))
;; FIXME: To do this right, we'd need a post-self-insert-function
;; so we could add-function around it and insert the closer after
;; all the rest of the hook has run.
- (if (>= (mark) (point))
- (goto-char (mark))
- ;; We already inserted the open-paren but at the end of the
- ;; region, so we have to remove it and start over.
- (delete-region (1- pos) (point))
- (save-excursion
- (goto-char (mark))
- (electric-pair--insert last-command-event)))
- ;; Since we're right after the closer now, we could tell the rest of
- ;; post-self-insert-hook that we inserted `closer', but then we'd get
- ;; blink-paren to kick in, which is annoying.
- ;;(setq last-command-event closer)
- (insert closer))
+ (if (or (eq syntax ?\")
+ (and (eq syntax ?\))
+ (>= (point) (mark)))
+ (and (not (eq syntax ?\)))
+ (>= (mark) (point))))
+ (save-excursion
+ (goto-char (mark))
+ (electric-pair--insert pair))
+ (delete-region pos (1- pos))
+ (electric-pair--insert pair)
+ (goto-char (mark))
+ (electric-pair--insert last-command-event)))
;; Backslash-escaped: no pairing, no skipping.
((save-excursion
(goto-char (1- pos))
(not (zerop (% (skip-syntax-backward "\\") 2))))
nil)
+ ;; Insert matching pair.
+ ((and (memq syntax `(?\( ?\" ?\$))
+ (not overwrite-mode)
+ (or (eq table t)
+ (with-syntax-table table
+ (not (funcall electric-pair-inhibit-predicate last-command-event)))))
+ (save-excursion (electric-pair--insert pair)))
;; Skip self.
((and (memq syntax '(?\) ?\" ?\$))
- 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
- ;; undo-log and in the intermediate state which might be visible to other
- ;; post-self-insert-hook. We'll just have to live with it for now.
- (delete-char 1))
- ;; Insert matching pair.
- ((not (or (not (memq syntax `(?\( ?\" ?\$)))
- overwrite-mode
- (funcall electric-pair-inhibit-predicate last-command-event)))
- (save-excursion (electric-pair--insert closer))))))
+ (or (eq table t)
+ (if (functionp electric-pair-skip-self)
+ (with-syntax-table table
+ (funcall electric-pair-skip-self last-command-event))
+ electric-pair-skip-self)))
+ (let ((original-point (point))
+ (skip-info (if (functionp electric-pair-skip-whitespace)
+ (funcall electric-pair-skip-whitespace)
+ electric-pair-skip-whitespace)))
+ (when skip-info (skip-chars-forward "\t\s\n"))
+ (if (eq (char-after (if skip-info
+ (point)
+ 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 undo-log and in the
+ ;; intermediate state which might be visible to other
+ ;; post-self-insert-hook. We'll just have to live with it
+ ;; for now.
+ (if (eq skip-info 'chomp)
+ (delete-region original-point (1+ (point)))
+ (delete-region (1- pos) pos)
+ (forward-char))
+ (goto-char original-point)))))))
(defun electric-pair-will-use-region ()
(and (use-region-p)
- (memq (electric-pair-syntax last-command-event) '(?\( ?\" ?\$))))
+ (memq (car (electric-pair-syntax-info last-command-event))
+ '(?\( ?\) ?\" ?\$))))
;;;###autoload
(define-minor-mode electric-pair-mode
@@ -442,10 +745,19 @@ closing parenthesis. \(Likewise for brackets, etc.)
See options `electric-pair-pairs' and `electric-pair-skip-self'."
:global t :group 'electricity
+ :keymap (let ((map (make-sparse-keymap)))
+ (define-key map [remap backward-delete-char-untabify]
+ 'electric-pair-backward-delete-char-untabify)
+ (define-key map [remap backward-delete-char]
+ 'electric-pair-backward-delete-char)
+ (define-key map [remap delete-backward-char]
+ 'electric-pair-backward-delete-char)
+ map)
(if electric-pair-mode
(progn
(add-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)
(add-hook 'self-insert-uses-region-functions
#'electric-pair-will-use-region))
(remove-hook 'post-self-insert-hook
@@ -494,11 +806,13 @@ positive, and disable it otherwise. If called from Lisp, enable
the mode if ARG is omitted or nil.
The variable `electric-layout-rules' says when and how to insert newlines."
:global t :group 'electricity
- (if electric-layout-mode
- (add-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)
- (remove-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)))
+ (cond (electric-layout-mode
+ (add-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook))
+ (t
+ (remove-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function))))
(provide 'electric)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index f4e9b31..8bb74d3 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -472,7 +472,11 @@ font-lock keywords will not be case sensitive."
(font-lock-mark-block-function . mark-defun)
(font-lock-syntactic-face-function
. lisp-font-lock-syntactic-face-function)))
- (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
+ (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
+ ; electric
+ (when elisp
+ (setq-local electric-pair-non-code-pairs '((?\` . ?\'))))
+ (setq-local electric-pair-skip-whitespace 'chomp))
(defun lisp-outline-level ()
"Lisp mode `outline-level' function."
diff --git a/lisp/simple.el b/lisp/simple.el
index 260c170..c591cee 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -607,7 +607,7 @@ In some text modes, where TAB inserts a tab, this command indents to the
column specified by the function `current-left-margin'."
(interactive "*")
(delete-horizontal-space t)
- (newline)
+ (newline 1 (not (or executing-kbd-macro noninteractive)))
(indent-according-to-mode))
(defun reindent-then-newline-and-indent ()
diff --git a/test/automated/electric-tests.el b/test/automated/electric-tests.el
new file mode 100644
index 0000000..feaea0a
--- /dev/null
+++ b/test/automated/electric-tests.el
@@ -0,0 +1,436 @@
+;;; electric-tests.el --- tests for electric.el -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2013 João Távora
+
+;; Author: João Távora <joaotavora@gmail.com>
+;; Keywords:
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'ert-x)
+(require 'electric)
+(require 'cl-lib)
+
+(defun call-with-saved-electric-modes (fn)
+ (let ((saved-electric (if electric-pair-mode 1 -1))
+ (saved-layout (if electric-layout-mode 1 -1))
+ (saved-indent (if electric-indent-mode 1 -1)))
+ (electric-pair-mode -1)
+ (electric-layout-mode -1)
+ (electric-indent-mode -1)
+ (unwind-protect
+ (funcall fn)
+ (electric-pair-mode saved-electric)
+ (electric-indent-mode saved-indent)
+ (electric-layout-mode saved-layout))))
+
+(defmacro save-electric-modes (&rest body)
+ (declare (indent defun) (debug t))
+ `(call-with-saved-electric-modes #'(lambda () ,@body)))
+
+(defun electric-pair-test-for (fixture where char expected-string
+ expected-point mode bindings fixture-fn)
+ (with-temp-buffer
+ (funcall mode)
+ (insert fixture)
+ (save-electric-modes
+ (let ((last-command-event char))
+ (goto-char where)
+ (funcall fixture-fn)
+ (progv
+ (mapcar #'car bindings)
+ (mapcar #'cdr bindings)
+ (self-insert-command 1))))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected-string))
+ (should (equal (point)
+ expected-point))))
+
+(eval-when-compile
+ (defun electric-pair-define-test-form (name fixture
+ char
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn)
+ (let* ((expected-string-and-point
+ (if skip-pair-string
+ (with-temp-buffer
+ (progv
+ ;; FIXME: avoid `eval'
+ (mapcar #'car (eval bindings))
+ (mapcar #'cdr (eval bindings))
+ (funcall mode)
+ (insert fixture)
+ (goto-char (1+ pos))
+ (insert char)
+ (cond ((eq (aref skip-pair-string pos)
+ ?p)
+ (insert (electric-pair--pair-of char))
+ (backward-char 1))
+ ((eq (aref skip-pair-string pos)
+ ?s)
+ (delete-char -1)
+ (forward-char 1)))
+ (list
+ (buffer-substring-no-properties (point-min) (point-max))
+ (point))))
+ (list expected-string expected-point)))
+ (expected-string (car expected-string-and-point))
+ (expected-point (cadr expected-string-and-point))
+ (fixture (format "%s%s%s" prefix fixture suffix))
+ (expected-string (format "%s%s%s" prefix expected-string suffix))
+ (expected-point (+ (length prefix) expected-point))
+ (pos (+ (length prefix) pos)))
+ `(ert-deftest ,(intern (format "electric-pair-%s-at-point-%s-in-%s%s"
+ name
+ (1+ pos)
+ mode
+ extra-desc))
+ ()
+ ,(format "With \"%s\", try input %c at point %d. \
+Should %s \"%s\" and point at %d"
+ fixture
+ char
+ (1+ pos)
+ (if (string= fixture expected-string)
+ "stay"
+ "become")
+ (replace-regexp-in-string "\n" "\\\\n" expected-string)
+ expected-point)
+ (electric-pair-test-for ,fixture
+ ,(1+ pos)
+ ,char
+ ,expected-string
+ ,expected-point
+ ',mode
+ ,bindings
+ ,fixture-fn)))))
+
+(cl-defmacro define-electric-pair-test
+ (name fixture
+ input
+ &key
+ skip-pair-string
+ expected-string
+ expected-point
+ bindings
+ (modes '(quote (emacs-lisp-mode ruby-mode c++-mode)))
+ (test-in-comments t)
+ (test-in-strings t)
+ (test-in-code t)
+ (fixture-fn #'(lambda ()
+ (electric-pair-mode 1))))
+ `(progn
+ ,@(cl-loop
+ for mode in (eval modes) ;FIXME: avoid `eval'
+ append
+ (cl-loop
+ for (prefix suffix extra-desc) in
+ (append (if test-in-comments
+ `((,(with-temp-buffer
+ (funcall mode)
+ (insert "z")
+ (comment-region (point-min) (point-max))
+ (buffer-substring-no-properties (point-min)
+ (1- (point-max))))
+ ""
+ "-in-comments")))
+ (if test-in-strings
+ `(("\"" "\"" "-in-strings")))
+ (if test-in-code
+ `(("" "" ""))))
+ append
+ (cl-loop
+ for char across input
+ for pos from 0
+ unless (eq char ?-)
+ collect (electric-pair-define-test-form
+ name
+ fixture
+ (aref input pos)
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn))))))
+\f
+;;; Basic pairings and skippings
+;;;
+(define-electric-pair-test balanced-situation
+ " (()) " "(((((((" :skip-pair-string "ppppppp"
+ :modes '(ruby-mode))
+
+(define-electric-pair-test too-many-openings
+ " ((()) " "(((((((" :skip-pair-string "ppppppp")
+
+(define-electric-pair-test too-many-closings
+ " (())) " "(((((((" :skip-pair-string "------p")
+
+(define-electric-pair-test too-many-closings-2
+ "() ) " "---(---" :skip-pair-string "-------")
+
+(define-electric-pair-test balanced-autoskipping
+ " (()) " "---))--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test too-many-openings-autoskipping
+ " ((()) " "----))-" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-autoskipping
+ " (())) " "---)))-" :skip-pair-string "---sss-")
+
+\f
+;;; Mixed parens
+;;;
+(define-electric-pair-test mixed-paren-1
+ " ()] " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test mixed-paren-2
+ " (]) " "-(-----" :skip-pair-string "-------")
+
+(define-electric-pair-test find-matching-different-paren-type
+ " ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test find-matching-different-paren-type-inside-list
+ "( ()]) " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test ignore-different-unmatching-paren-type
+ "( ()]) " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test autopair-keep-least-amount-of-mixed-unbalance
+ "( ()] " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test dont-autopair-to-resolve-mixed-unbalance
+ "( ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test autopair-so-as-not-to-worsen-unbalance-situation
+ "( (]) " "-[-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test skip-over-partially-balanced
+ " [([]) " "-----)---" :skip-pair-string "-----s---")
+
+(define-electric-pair-test only-skip-over-at-least-partially-balanced-stuff
+ " [([()) " "-----))--" :skip-pair-string "-----s---")
+
+\f
+;;; Skipping over quotes
+;;;
+(define-electric-pair-test pair-some-quotes-skip-others
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil)
+
+(define-electric-pair-test skip-single-quotes-in-ruby-mode
+ " '' " "--'-" :skip-pair-string "--s-"
+ :modes '(ruby-mode)
+ :test-in-comments nil
+ :test-in-strings nil)
+
+(define-electric-pair-test leave-unbalanced-quotes-alone
+ " \"' " "-\"'-" :skip-pair-string "----"
+ :modes '(ruby-mode)
+ :test-in-strings nil)
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-2
+ " \"\\\"' " "-\"--'-" :skip-pair-string "------"
+ :modes '(ruby-mode)
+ :test-in-strings nil)
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-3
+ " foo\\''" "'------" :skip-pair-string "-------"
+ :modes '(ruby-mode)
+ :test-in-strings nil)
+
+\f
+;;; Skipping over whitespace
+;;;
+(define-electric-pair-test whitespace-jumping
+ " ( ) " "--))))---" :expected-string " ( ) " :expected-point 8
+ :bindings '((electric-pair-skip-whitespace . t)))
+
+(define-electric-pair-test whitespace-chomping
+ " ( ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+(define-electric-pair-test whitespace-chomping-2
+ " ( \n\t\t\n ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+\f
+;;; Pairing arbitrary characters
+;;;
+(define-electric-pair-test angle-brackets-everywhere
+ "<>" "<>" :skip-pair-string "ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(define-electric-pair-test angle-brackets-everywhere-2
+ "(<>" "-<>" :skip-pair-string "-ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(defvar electric-pair-test-angle-brackets-table
+ (let ((table (make-syntax-table prog-mode-syntax-table)))
+ (modify-syntax-entry ?\< "(>" table)
+ (modify-syntax-entry ?\> ")<`" table)
+ table))
+
+(define-electric-pair-test angle-brackets-pair
+ "<>" "<" :expected-string "<><>" :expected-point 2
+ :test-in-code nil
+ :bindings `((electric-pair-non-code-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test angle-brackets-skip
+ "<>" "->" :expected-string "<>" :expected-point 3
+ :test-in-code nil
+ :bindings `((electric-pair-non-code-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test pair-backtick-and-quote-in-comments
+ ";; " "---`" :expected-string ";; `'" :expected-point 5
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-non-code-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-comments
+ ";; `foo'" "-------'" :expected-string ";; `foo'" :expected-point 9
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-non-code-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test pair-backtick-and-quote-in-strings
+ "\"\"" "-`" :expected-string "\"`'\"" :expected-point 3
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-non-code-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings
+ "\"`'\"" "--'" :expected-string "\"`'\"" :expected-point 4
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-non-code-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings-2
+ " \"`'\"" "----'" :expected-string " \"`'\"" :expected-point 6
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-non-code-pairs . ((?\` . ?\')))))
+
+\f
+;;; `js-mode' has `electric-layout-rules' for '{ and '}
+;;;
+(define-electric-pair-test js-mode-braces
+ "" "{" :expected-string "{}" :expected-point 2
+ :modes '(js-mode)
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout
+ "" "{" :expected-string "{\n\n}" :expected-point 3
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-layout-mode 1)
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout-and-indent
+ "" "{" :expected-string "{\n \n}" :expected-point 7
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (electric-layout-mode 1)))
+
+\f
+;;; Backspacing
+;;; TODO: better tests
+;;;
+(ert-deftest electric-pair-backspace-1 ()
+ (save-electric-modes
+ (with-temp-buffer
+ (insert "()")
+ (goto-char 2)
+ (electric-pair-backward-delete-char 1)
+ (should (equal "" (buffer-string))))))
+
+\f
+;;; Autowrapping
+;;;
+(define-electric-pair-test autowrapping-1
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-2
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-3
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-4
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-5
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-6
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(provide 'electric-pair-tests)
+;;; electric-pair-tests.el ends here
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-07 23:16 ` Stefan Monnier
@ 2013-12-12 3:05 ` João Távora
2013-12-12 4:29 ` Dmitry Gutov
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-12 3:05 UTC (permalink / raw
To: Stefan Monnier; +Cc: Dmitry Gutov, emacs-devel
Stefan Monnier <monnier@iro.umontreal.ca> writes:
> We could remap backward-delete-char-untabify in
> electric-pair-mode-map.
Thanks, I did that and it seems to work OK.
>>> I'm also partial to the `autopair-newline' feature. It would probably
>>> serve best as an extension of `electric-layout-mode'.
>> Yes, I agree that `electric-layout-mode' seems the place for this, but
>> how to write these rules in the existing `electric-layout-rules' var?
>
> Indeed, it doesn't really fit in there. You could probably hack it in
> brute-force style by adding the newline directly from
> electric-layout-in-between-parenthesis (and then return nil rather than
> `after').
Didn't try it yet. Anyway, in latest emacs, js-mode layout rules already
have reasonable behaviour, opening newlines after when you "{" and
before them when you "}". That might be enough.
> Patch welcome.
Patch sent.
>> - additionally one should be able to customize if points ends up
>> inside or outside the wrapped region.
>
> I don't see a strong need for such customization, but I wouldn't
> object.
Yeah, its overkill. Someone once requested it for autopair. Maybe I can
add it..
João
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 2 replies; 36+ messages in thread
From: Dmitry Gutov @ 2013-12-12 4:29 UTC (permalink / raw
To: João Távora, Stefan Monnier; +Cc: emacs-devel
On 12.12.2013 05:05, João Távora wrote:
> Didn't try it yet. Anyway, in latest emacs, js-mode layout rules already
> have reasonable behaviour, opening newlines after when you "{" and
> before them when you "}". That might be enough.
I don't think this is sufficient: what if I want to type a one-line
function or object literal (say, an empty one)? Or an empty for, while
or catch body? I'd have to remove the added newline(s) and whitespace in
each such instance.
I'd much prefer a workflow where electric-layout-mode doesn't do
anything until I press Return, and then does the autopair-newline thing
if point is directly between two parens.
>> Patch welcome.
>
> Patch sent.
Thank you!
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-12 4:29 ` Dmitry Gutov
@ 2013-12-12 11:26 ` João Távora
2013-12-12 16:30 ` Stefan Monnier
1 sibling, 0 replies; 36+ messages in thread
From: João Távora @ 2013-12-12 11:26 UTC (permalink / raw
To: Dmitry Gutov; +Cc: Stefan Monnier, emacs-devel
Dmitry Gutov <dgutov@yandex.ru> writes:
>> Didn't try it yet. Anyway, in latest emacs, js-mode layout rules
>> already have reasonable behaviour,
> I don't think this is sufficient: what if I want to type a one-line
> function or object literal (say, an empty one)? Or an empty for, while
> or catch body? I'd have to remove the added newline(s) and whitespace
> in each such instance.
Yes, makes sense, I didn't think of that.
João
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
1 sibling, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-12 16:30 UTC (permalink / raw
To: Dmitry Gutov; +Cc: João Távora, emacs-devel
> I don't think this is sufficient: what if I want to type a one-line function
> or object literal (say, an empty one)? Or an empty for, while or catch body?
> I'd have to remove the added newline(s) and whitespace in each
> such instance.
As explained, electric-layout-mode is directly inspired from cc-mode's
corresponding feature. In C modes and friends, you rarely (if ever)
write "{ }", so there was no need for such a thing.
> I'd much prefer a workflow where electric-layout-mode doesn't do anything
> until I press Return, and then does the autopair-newline thing if point is
> directly between two parens.
I think it makes sense. I'd welcome an extension of
electric-layout-mode which provides such a functionality.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 2 replies; 36+ messages in thread
From: João Távora @ 2013-12-12 17:06 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel, Dmitry Gutov
Stefan Monnier <monnier@IRO.UMontreal.CA> writes:
> As explained, electric-layout-mode is directly inspired from cc-mode's
> corresponding feature. In C modes and friends, you rarely (if ever)
> write "{ }", so there was no need for such a thing.
By the way, this is unrelated, but c-electric-backspace breaks the
`autobackspacing` feature
> I think it makes sense. I'd welcome an extension of
> electric-layout-mode which provides such a functionality.
Is something like this what you had in mind? Notice the
`electric-indent-mode' gotcha (described in another one of your FIXME's)
diff --git a/lisp/electric.el b/lisp/electric.el
index b227e3d..b852e5e 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -767,12 +767,12 @@ See options `electric-pair-pairs' and `electric-pair-skip-self'."
;;; Electric newlines after/before/around some chars.
-(defvar electric-layout-rules '()
+(defvar electric-layout-rules `((?\n . ,#'electric-pair-newline-between-pairs-rule))
"List of rules saying where to automatically insert newlines.
-Each rule has the form (CHAR . WHERE) where CHAR is the char
-that was just inserted and WHERE specifies where to insert newlines
-and can be: nil, `before', `after', `around', or a function of no
-arguments that returns one of those symbols.")
+Each rule has the form (CHAR . WHERE) where CHAR is the char that
+was just inserted and WHERE specifies where to insert newlines
+and can be: nil, `before', `after', `around', `after-stay', or a
+function of no arguments that returns one of those symbols.")
(defun electric-layout-post-self-insert-function ()
(let* ((rule (cdr (assq last-command-event electric-layout-rules)))
@@ -781,23 +781,45 @@ arguments that returns one of those symbols.")
(setq pos (electric--after-char-pos))
;; Not in a string or comment.
(not (nth 8 (save-excursion (syntax-ppss pos)))))
- (let ((end (copy-marker (point) t)))
+ (let ((end (copy-marker (point)))
+ (sym (if (functionp rule) (funcall rule) rule)))
+ (set-marker-insertion-type end (not (eq sym 'after-stay)))
(goto-char pos)
- (pcase (if (functionp rule) (funcall rule) rule)
+ (case sym
;; FIXME: we used `newline' down here which called
;; self-insert-command and ran post-self-insert-hook recursively.
;; It happened to make electric-indent-mode work automatically with
;; electric-layout-mode (at the cost of re-indenting lines
;; multiple times), but I'm not sure it's what we want.
+ ;;
+ ;; FIXME: check eolp before inserting \n?
(`before (goto-char (1- pos)) (skip-chars-backward " \t")
(unless (bolp) (insert "\n")))
- (`after (insert "\n")) ; FIXME: check eolp before inserting \n?
+ (`after (insert "\n"))
+ ;; FIXME: indenting here is a no-no, but see the beginning
+ ;; note in `electric-indent-post-self-insert-function'. We
+ ;; have to find someway to notify that function that we
+ ;; affected more text than just the one between `pos' and
+ ;; `end'.
+ (`after-stay (save-excursion
+ (insert "\n")
+ (if electric-indent-mode
+ (indent-according-to-mode))))
(`around (save-excursion
(goto-char (1- pos)) (skip-chars-backward " \t")
(unless (bolp) (insert "\n")))
(insert "\n"))) ; FIXME: check eolp before inserting \n?
(goto-char end)))))
+(defun electric-pair-newline-between-pairs-rule ()
+ (when (and electric-pair-mode
+ (not (eobp))
+ (eq (save-excursion
+ (skip-chars-backward "\n\t ")
+ (char-before))
+ (electric-pair--pair-of (char-after))))
+ 'after-stay))
+
;;;###autoload
(define-minor-mode electric-layout-mode
"Automatically insert newlines around some chars.
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-12 18:08 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
> - `electric-pair-pairs` now defaults to nil.
> See its updated docstring for the reasoning and the new (akwardly
> named) variables `electric-pair-non-code-pairs' and
> `electric-pair-non-code-syntax-table' for slightly better, more
> flexible alternatives to it, in my opinion.
I think this change is for the worst. I often rely on ".." pairing in
things like text-mode where they don't have `string' syntax.
The explanation for removing (?\" . ?\") doesn't make much sense: you
seem to say that the list should be kept empty because balance can't be
provided, but until now balance wasn't provided either.
Of course, when the syntax-table gives string syntax to ", then this
entry should be ignored since the syntax table gives more info, which
allows balancing.
I suggest you use "text" rather than "non-code" in the variable names
(and indeed, the behavior in strings and comments should largely be the
same as in text-mode).
> get autopair.el's backtick-and-quote pairing for elisp comments and
> strings...
This is good, yes, thanks.
> I read the other FIXME notes but didn't understand them so well. Can
> you provide examples of the conflicts between `electric-indent-mode'
> and other `electric-modes' that you mention there?
I can't remember off-hand, sorry.
> - I simplififed the previous `electric-pair--up-list' function and
> renamed it `electric-pair--balance-info'. But I still couldn't make it
> use `up-list' or get rid of some `forward-sexp'. `up-list' can
> probably be done with enough care, but I think replacing
> `forward-sexp' with `syntax-ppss' is only possible in the "pair", not
> the "skip" case.
We can use syntax-ppss to get the START of all the parens, but indeed,
to find the overall balance, we need to look at least for the close-paren
matching the outermost open paren, which syntax-ppss doesn't give us.
> And only when outside comments or strings.
The important case is the "pathological" case where we have to scan from
"almost point-min" to "almost point-max", and that should hopefully
never happen inside strings or comments.
> In this implementation, I think the the number of `forward-sexp''s is
> at most proportional to the nesting depth, which is less
> dangerous.
Why do you need more than a fixed number of calls? My naive
understanding is that we only need to know if we have more openers than
closers, or more closers than openers, or the same number of each; and
for that we basically need to do:
(goto-char (car (last (nth 9 (syntax-ppss)))))
(forward-sexp 1) => failure means more openers than closers
(up-list 1) => failure means the parens are balanced.
Tho maybe even simpler is
(save-excursion (car (syntax-ppss (point-max))))
this will also have the side effect of applying syntax-propertize over
the whole buffer, which could lead to performance problems, but would
also eliminate bugs when a sole-open paren is inside a special comment
only recognized by syntax-propertize.
This said, this dependence on correctly parsing the whole buffer makes
this feature brittle. Both for the case where the major mode has a bug
(so it fails to parse things properly), or when it knowingly doesn't
handle all cases (same thing, tho it's not considered as a bug,
e.g. "#ifdef FOO \n { \n #else \n {x \n #fi"), or when some (remote)
part of the buffer is simply incorrect/incomplete.
So I think we need a "electric-pair-preserve-balance" flag to control
those features.
> - some helper functions might be reinventing the wheel, such as
> `electric-pair--looking-at-mismatched-string-p' and
IIUC if the next string is "mismatched" that means it extends til EOB,
so you can test it with (nth 3 (syntax-ppss (point-max))).
> `electric-pair--inside-comment-string'.
I think
(nth 3 (parse-partial-sexp (nth 8 (syntax-ppss)) (point)))
will do.
But of course, this parses the content of the comment as if it were
code, which is naive. You could use another syntax-table during the
call to parse-partial-sexp (but not the call to syntax-ppss), but it'd
still be naive. IOW the problem is not in the implementation but in the
idea of detecting a string inside a comment.
> - I'm also trying my luck and changed lisp-mode.el defaults to
> accomodate some of my preferred defaults in variables
> `electric-pair-skip-whitespace' and `electric-pair-non-code-pairs').
See comments below.
> +(defun electric--sort-post-self-insertion-hook ()
> + "Ensure order of electric functions in `post-self-insertion-hook'.
> +
> +Hooks in this variable interact in non-trivial ways, so a
> +relative order must be maintained within it."
> + (let ((relative-order '(electric-pair-post-self-insert-function
> + electric-layout-post-self-insert-function
> + electric-indent-post-self-insert-function
> + blink-paren-post-self-insert-function)))
> + (setq post-self-insert-hook
> + (sort post-self-insert-hook
> + #'(lambda (fn1 fn2)
> + (let ((fn1-tail (memq fn1 relative-order))
> + (fn2-tail (memq fn2 relative-order)))
> + (cond ((and fn1-tail fn2-tail)
> + (> (length fn1-tail)
> + (length fn2-tail)))
> + (fn1-tail t)
> + (fn2-tail nil)
> + (t nil))))))))
I think the idea is OK, but be careful: these functions are
supposed to be placed on the global part of the hook, so you'll want to
use `default-value' and `setq-default'.
BTW: I resisted the temptation to do such a `sort', but I can't give
a good reason why. I just wish we had a better way to handle such
order-dependencies. The part I don't like is the lack of modularity.
I generally tend to dislike "priorities", but maybe adding priorities
would make sense here: blink-paren-post-self-insert-function would get
a priority of 100 since it should "come last because it does a sit-for,
electric-indent-post-self-insert-function would get a priority of 90
because "it just does some post-processing".
The priorities can be added as symbol properties, so the "sort" function
doesn't need to know about the existing hooks.
> +(make-variable-buffer-local 'electric-pair-non-code-pairs)
I think we don't want/need that. We can and do use setq-local when
needed, which should be sufficient.
> +(defcustom electric-pair-skip-whitespace t
> + "If non-nil skip whitespace when skipping over closing parens.
Unclear. IIUC from the code, what this doesn't just skip whitespace but
deletes it. It should also say which whitespace is skipped/deleted
(before/after the skipped paren).
> +(defvar electric-pair-non-code-syntax-table prog-mode-syntax-table
Why prog-mode-syntax-table, rather than (say) text-mode-syntax-table?
BTW, syntax-ppss will get majorly confused if you call it while
a different syntax-table is temporarily installed.
> + :keymap (let ((map (make-sparse-keymap)))
> + (define-key map [remap backward-delete-char-untabify]
> + 'electric-pair-backward-delete-char-untabify)
> + (define-key map [remap backward-delete-char]
> + 'electric-pair-backward-delete-char)
> + (define-key map [remap delete-backward-char]
> + 'electric-pair-backward-delete-char)
> + map)
Don't use this :keymap arg; instead defvar electric-pair-mode-map.
> - (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
> + (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
> + ; electric
> + (when elisp
> + (setq-local electric-pair-non-code-pairs '((?\` . ?\'))))
Good.
> + (setq-local electric-pair-skip-whitespace 'chomp))
Hmm... lemme think... well... maybe we can't keep it tentatively.
I suspect it will bite me sometimes, but let's give it a chance.
> --- a/lisp/simple.el
> +++ b/lisp/simple.el
> @@ -607,7 +607,7 @@ In some text modes, where TAB inserts a tab, this command indents to the
> column specified by the function `current-left-margin'."
> (interactive "*")
> (delete-horizontal-space t)
> - (newline)
> + (newline 1 (not (or executing-kbd-macro noninteractive)))
> (indent-according-to-mode))
Could you explain this change?
> +;;; electric-tests.el --- tests for electric.el -*- lexical-binding: t; -*-
Great!
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-12 17:06 ` João Távora
@ 2013-12-12 20:12 ` Stefan Monnier
2013-12-13 2:55 ` Dmitry Gutov
1 sibling, 0 replies; 36+ messages in thread
From: Stefan Monnier @ 2013-12-12 20:12 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel, Dmitry Gutov
> +(defvar electric-layout-rules `((?\n . ,#'electric-pair-newline-between-pairs-rule))
Not sure if it belongs in the default. At least it probably shouldn't
apply to Lisp modes.
> +Each rule has the form (CHAR . WHERE) where CHAR is the char that
> +was just inserted and WHERE specifies where to insert newlines
> +and can be: nil, `before', `after', `around', `after-stay', or a
> +function of no arguments that returns one of those symbols.")
Oh, I thought the desired feature was to go from "{ }" to "{ \n } \n",
but I see now that it's indeed much simpler and `after-stay' would work
fine, yes. But please explain in the docstring what `after-stay' means.
> + ;; FIXME: indenting here is a no-no, but see the beginning
> + ;; note in `electric-indent-post-self-insert-function'. We
> + ;; have to find someway to notify that function that we
> + ;; affected more text than just the one between `pos' and
> + ;; `end'.
> + (`after-stay (save-excursion
> + (insert "\n")
> + (if electric-indent-mode
> + (indent-according-to-mode))))
How 'bout using (let ((electric-layout-rules nil)) (newline 1 t))?
> +(defun electric-pair-newline-between-pairs-rule ()
> + (when (and electric-pair-mode
> + (not (eobp))
> + (eq (save-excursion
> + (skip-chars-backward "\n\t ")
> + (char-before))
> + (electric-pair--pair-of (char-after))))
> + 'after-stay))
I'd rather remove the \n from skip-chars-backward, so as to be a bit
more conservative w.r.t when we use after-stay.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-12 18:08 ` Stefan Monnier
@ 2013-12-13 1:02 ` João Távora
2013-12-13 2:32 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-13 1:02 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel
Stefan Monnier <monnier@IRO.UMontreal.CA> writes:
> I think this change is for the worst. I often rely on ".." pairing in
> things like text-mode where they don't have `string' syntax.
But if you "rely" on those in text-mode, isn't the correct thing to give
them that syntax there?
> The explanation for removing (?\" . ?\") doesn't make much sense: you
> seem to say that the list should be kept empty because balance can't be
> provided, but until now balance wasn't provided either.
I suggested in the docstring that before adding to this list, since it
has priority, the lower priority buffer's syntax table should be used
instead, as it also helps balancing.
> Of course, when the syntax-table gives string syntax to ", then this
> entry should be ignored since the syntax table gives more info, which
> allows balancing.
So you mean inverting the lookup order? Lookup electric-pair-pairs and
electric-pair-text-pairs only if no relevant syntax is found in
(syntax-table) and `electric-pair-text-syntax-table' respectively.
If so, it seems like a good idea. I got the opposite idea from the
original implementation of `electric-pair-syntax' and the mention of
"regardless of major mode" in `electric-pair-pairs''s docstring.
> I suggest you use "text" rather than "non-code" in the variable names
> (and indeed, the behavior in strings and comments should largely be the
> same as in text-mode).
I disagree, but won't insist. "strings" and "comments" exist when
programming, in my experience these typically are places where
programmatic syntax is useful, to write error messages, explain
structure, etc. But since `electric-pair-text-syntax-table' stays I'll
just set it globablly to prog-mode-syntax-table in my config. This way I
can get half-decent balancing for it.
>> you provide examples of the conflicts between `electric-indent-mode'
>> and other `electric-modes' that you mention there?
> I can't remember off-hand, sorry.
I think I just found one when implementing the `electric-layout-rules'
suggestion in the nearby thread.
> We can use syntax-ppss to get the START of all the parens, but indeed,
> to find the overall balance, we need to look at least for the close-paren
> matching the outermost open paren, which syntax-ppss doesn't give us.
OK, so I'm already using `syntax-ppss' where possible right?
>> In this implementation, I think the the number of `forward-sexp''s is
>> at most proportional to the nesting depth, which is less
>> dangerous.
> Why do you need more than a fixed number of calls? My naive
> understanding is that we only need to know if we have more openers than
> closers, or more closers than openers, or the same number of each; and
> for that we basically need to do:
>
> (goto-char (car (last (nth 9 (syntax-ppss)))))
> (forward-sexp 1) => failure means more openers than closers
> (up-list 1) => failure means the parens are balanced.
I tried to make this into a function and failed again.
> (save-excursion (car (syntax-ppss (point-max))))
This works... but only for one kind of parenthesis. It incorrectly
reports 0 for a buffer with '[)', the mixed parenthesis case. I think
none of these alternatives will make the "mixed parens" test cases pass,
i.e. the ones where one kind is unbalanced but you still want to keep
balancing the other kind. The motivation to invest in electric.el came
after I fixed one outstanding such bug in autopair.el.
> So I think we need a "electric-pair-preserve-balance" flag to control
> those features.
OK, but I would set it to t. Or maybe run `check-parens' in
prog-mode-hook and set it to nil if that errors out. Maybe issue a
warning to the user.
By the way, even with that evil #ifdef that breaks balancing of the `{}'
parens, we should strive to to still have `()' pair as if nothing
happened. (currently this is still shaky).
>> - some helper functions might be reinventing the wheel, such as
>> `electric-pair--looking-at-mismatched-string-p' and
>
> IIUC if the next string is "mismatched" that means it extends til EOB,
> so you can test it with (nth 3 (syntax-ppss (point-max))).
Looks much simpler, but weirdly fails some automated tests while
passing the same tests when run interactively. Will investigate.
> IOW the problem is not in the implementation but in the idea of
> detecting a string inside a comment.
Yes. This was always slightly shaky in autopair.el, but good enough for
most uses. It's seldom a problem. Anyway I'll try your suggestion.
>> +(defun electric--sort-post-self-insertion-hook ()
> I think the idea is OK, but be careful: these functions are
> supposed to be placed on the global part of the hook, so you'll want to
> use `default-value' and `setq-default'.
OK, good idea.
> The priorities can be added as symbol properties, so the "sort" function
> doesn't need to know about the existing hooks.
Sounds a bit overkill for this particular case, unless we
make it more generic, like maybe adding this to add-hook's understanding
of its APPEND arg. But OK.
>> +(make-variable-buffer-local 'electric-pair-non-code-pairs)
> I think we don't want/need that. We can and do use setq-local when
> needed, which should be sufficient.
OK.
>> +(defcustom electric-pair-skip-whitespace t
>> + "If non-nil skip whitespace when skipping over closing parens.
>
> Unclear. IIUC from the code, what this doesn't just skip whitespace but
> deletes it. It should also say which whitespace is skipped/deleted
> (before/after the skipped paren).
OK. Only the defcustom docs explain the whole story: it only deletes it
when set to 'chomp, the default is t.
>> +(defvar electric-pair-non-code-syntax-table prog-mode-syntax-table
> Why prog-mode-syntax-table, rather than (say) text-mode-syntax-table?
Explained above, but I don't object to text-mode-syntax-table.
> BTW, syntax-ppss will get majorly confused if you call it while
> a different syntax-table is temporarily installed.
Never bit me, but thanks for the heads-up. I'll probably end up calling
parse-partial-sexp with the correct table in comments or
strings.
> Don't use this :keymap arg; instead defvar electric-pair-mode-map.
OK.
>> + (setq-local electric-pair-skip-whitespace 'chomp))
>
> Hmm... lemme think... well... maybe we can't keep it tentatively.
> I suspect it will bite me sometimes, but let's give it a chance.
I don't undestand: we "can" or we "can't" keep it? I think we should
give it a chance for lisp based modes. elecric-mode is always going to
be a surprise, might as well make it a good one. And when could it
possibly bite you?
>> - (newline)
>> + (newline 1 (not (or executing-kbd-macro noninteractive)))
>> (indent-according-to-mode))
>
> Could you explain this change?
No, it doesn't belong here, sorry :-) Some earlier attempt at making the
related newline-between-pairs feature. But since you spotted it,
shoudn't newline-and-indent also call the post-self-insertion hooks? Or
should we leave that job to electric-indent-mode?
João
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-13 2:32 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
>> I think this change is for the worst. I often rely on ".." pairing in
>> things like text-mode where they don't have `string' syntax.
> But if you "rely" on those in text-mode, isn't the correct thing to give
> them that syntax there?
Not really, because then font-lock would make use of it, and they're not
always properly balanced. IOW, yes, kind of but it has downsides, so
being forced to do it is rather annoying.
Why would it be a problem to keep them there?
>> The explanation for removing (?\" . ?\") doesn't make much sense: you
>> seem to say that the list should be kept empty because balance can't be
>> provided, but until now balance wasn't provided either.
> I suggested in the docstring that before adding to this list, since it
> has priority, the lower priority buffer's syntax table should be used
> instead, as it also helps balancing.
Still: you changed the default on the grounds that "it doesn't work
well", but it's worked well enough so far.
>> Of course, when the syntax-table gives string syntax to ", then this
>> entry should be ignored since the syntax table gives more info, which
>> allows balancing.
> So you mean inverting the lookup order?
Yes.
> If so, it seems like a good idea. I got the opposite idea from the
> original implementation of `electric-pair-syntax' and the mention of
> "regardless of major mode" in `electric-pair-pairs''s docstring.
In the original behavior, the order was largely irrelevant.
>> I suggest you use "text" rather than "non-code" in the variable names
>> (and indeed, the behavior in strings and comments should largely be the
>> same as in text-mode).
> I disagree, but won't insist.
On which part (the quoted text has at least 2 separate arguments)?
>> (save-excursion (car (syntax-ppss (point-max))))
> This works... but only for one kind of parenthesis. It incorrectly
> reports 0 for a buffer with '[)',
Ah, I guess that indeed introduces some complications, indeed.
Thanks for clearing things up. You might give this obvious example
somewhere in a comment.
>> So I think we need a "electric-pair-preserve-balance" flag to control
>> those features.
> OK, but I would set it to t.
Yes, it should default to t (unless too many people complain).
>> The priorities can be added as symbol properties, so the "sort" function
>> doesn't need to know about the existing hooks.
> Sounds a bit overkill for this particular case, unless we
> make it more generic, like maybe adding this to add-hook's understanding
> of its APPEND arg. But OK.
I agree it's not too serious a problem, but, I think even in this case
it can slightly simplify the code, since the comparison function can be
a simpler (lambda (x y) (< (or (get x 'priority) 0) (or (get
y 'priority) 0))). And additionally that might be useful for other
people's post-self-insert-hooks.
[ FWIW: I just added such priority support to add-function and
advice-add. ]
>>> +(defvar electric-pair-non-code-syntax-table prog-mode-syntax-table
>> Why prog-mode-syntax-table, rather than (say) text-mode-syntax-table?
> Explained above, but I don't object to text-mode-syntax-table.
Can you give more concrete examples?
>> BTW, syntax-ppss will get majorly confused if you call it while
>> a different syntax-table is temporarily installed.
> Never bit me, but thanks for the heads-up.
It rarely bites, because in most cases syntax-ppss is pre-computed
during font-locking. Which also means that when it does bite it tends
to do so in fleeting, apparently unexplainable and unreproducible ways.
>>> + (setq-local electric-pair-skip-whitespace 'chomp))
>>
>> Hmm... lemme think... well... maybe we can't keep it tentatively.
>> I suspect it will bite me sometimes, but let's give it a chance.
> I don't undestand: we "can" or we "can't" keep it?
Sorry, damn typo. It was "can".
> No, it doesn't belong here, sorry :-) Some earlier attempt at making the
> related newline-between-pairs feature. But since you spotted it,
> shoudn't newline-and-indent also call the post-self-insertion hooks?
I guess so, yes.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
1 sibling, 1 reply; 36+ messages in thread
From: Dmitry Gutov @ 2013-12-13 2:55 UTC (permalink / raw
To: João Távora, Stefan Monnier; +Cc: emacs-devel
On 12.12.2013 19:06, João Távora wrote:
> ;;; Electric newlines after/before/around some chars.
>
> -(defvar electric-layout-rules '()
> +(defvar electric-layout-rules `((?\n . ,#'electric-pair-newline-between-pairs-rule))
Guess I'm a bit late with this comment (sorry), but does this mean that
the usage of the electric newline would be set on per-mode basis?
For example, like described previously, if I want js-mode to only insert
electric newlines when I press return, will I have to modify
electric-layout-rules in js-mode-hook, and do so for any other mode I
use that sets this variable?
Wouldn't a separate minor mode be better, electric-newline-mode maybe?
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-13 2:55 ` Dmitry Gutov
@ 2013-12-14 15:18 ` Stefan Monnier
2013-12-14 16:56 ` Dmitry Gutov
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-14 15:18 UTC (permalink / raw
To: Dmitry Gutov; +Cc: João Távora, emacs-devel
>> ;;; Electric newlines after/before/around some chars.
>>
>> -(defvar electric-layout-rules '()
>> +(defvar electric-layout-rules `((?\n . ,#'electric-pair-newline-between-pairs-rule))
> Guess I'm a bit late with this comment (sorry), but does this mean that the
> usage of the electric newline would be set on per-mode basis?
I'm not completely sure I understand our question w.r.t the code you
quoted: the code you quoted adds the behavior globally, so it obviously
wouldn't be set on a per-mode basis.
> For example, like described previously, if I want js-mode to only insert
> electric newlines when I press return, will I have to modify
> electric-layout-rules in js-mode-hook, and do so for any other mode I use
> that sets this variable?
I think setting it on a per-mode basis would be OK, but it wouldn't be
set by the user but instead by the major mode, based on the usual coding
style used for that mode. E.g. we wouldn't set it in Lisp, but we'd set
it in js-mode.
> Wouldn't a separate minor mode be better, electric-newline-mode maybe?
I don't see for it, currently. It should depend on
electric-layout-mode, tho, of course.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-14 15:18 ` Stefan Monnier
@ 2013-12-14 16:56 ` Dmitry Gutov
2013-12-15 1:39 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: Dmitry Gutov @ 2013-12-14 16:56 UTC (permalink / raw
To: Stefan Monnier; +Cc: João Távora, emacs-devel
On 14.12.2013 17:18, Stefan Monnier wrote:
> I'm not completely sure I understand our question w.r.t the code you
> quoted: the code you quoted adds the behavior globally, so it obviously
> wouldn't be set on a per-mode basis.
Yes, but if it's set via electric-layout-rules, to what value will this
variable be set in e.g. js-mode?
If it'll include what's there currently, '((?\; . after) (?\{ . after)
(?\} . before)), then to get the desired behavior I described previously
(NOT to insert a newline after I just typed `{', or any other
character), I'd have to modify it again in js-mode-hook.
IOW, turning on electric-layout-mode would turn on all
electric-layout-related behaviors defined for a given major mode. Are we
willing to remove electric newlines after `;', `{' and `}', by default,
from any major mode where one of those might conceivably be followed by
some character other than newline?
Speaking of cc-mode, I don't really program in C (though I'd like to
continue learning it at some point), but if I did, I'm not sure I'd want
the electric-layout-mode behavior there, but
electric-pair-newline-between-pairs-rule would be useful.
>> For example, like described previously, if I want js-mode to only insert
>> electric newlines when I press return, will I have to modify
>> electric-layout-rules in js-mode-hook, and do so for any other mode I use
>> that sets this variable?
>
> I think setting it on a per-mode basis would be OK, but it wouldn't be
> set by the user but instead by the major mode, based on the usual coding
> style used for that mode. E.g. we wouldn't set it in Lisp, but we'd set
> it in js-mode.
Yes. And similarly, if we have a separate minor mode, it will be
disabled (maybe via some -inhibit variable) by some major modes.
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-14 16:56 ` Dmitry Gutov
@ 2013-12-15 1:39 ` Stefan Monnier
2013-12-16 0:35 ` João Távora
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-15 1:39 UTC (permalink / raw
To: Dmitry Gutov; +Cc: João Távora, emacs-devel
> Yes, but if it's set via electric-layout-rules, to what value will this
> variable be set in e.g. js-mode?
The value which seems most useful for javascript.
> If it'll include what's there currently, '((?\; . after) (?\{ . after) (?\}
> . before)), then to get the desired behavior I described previously (NOT to
> insert a newline after I just typed `{', or any other character), I'd have
> to modify it again in js-mode-hook.
If there can be various competing choices, then indeed we have a problem.
The intention of electric-layout-mode is that it should more or less
(tho in a naive way) insert the newlines for you if you just
naively/sequentially type in the code.
There might indeed be various options as to "when" to insert the
newlines as well as "where".
> IOW, turning on electric-layout-mode would turn on all
> electric-layout-related behaviors defined for a given major mode. Are we
> willing to remove electric newlines after `;', `{' and `}', by default, from
> any major mode where one of those might conceivably be followed by some
> character other than newline?
I don't understand the question: if there are cases where
electric-layout-mode would insert a newline but the user doesn't want
it, indeed the user will be annoyed. I think in general there's no way
to be sure this never happens, other than turning off
electric-layout-mode. Same happens for the suggested
electric-pair-newline-between-pairs-rule.
> Speaking of cc-mode, I don't really program in C (though I'd like to
> continue learning it at some point), but if I did, I'm not sure I'd
> want the electric-layout-mode behavior there, but
> electric-pair-newline-between-pairs-rule would be useful.
{ not followed by a newline are rare in C, but they do happen, indeed.
E.g. for "enum"s or for immediate values of structs/arrays. So, indeed,
for those cases electric-layout-mode will be annoying.
For that reason electric-layout-mode is off by default, and I haven't
heard anyone argue to enable it by default.
From this POV, maybe electric-pair-newline-between-pairs-rule should be
made into a separate minor mode, indeed. It will reduce your use of RET
much less than electric-layout-mode but it's less likely to be annoying.
Less gain and less pain.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-13 2:32 ` Stefan Monnier
@ 2013-12-15 22:10 ` João Távora
2013-12-16 3:22 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-15 22:10 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel
Hi,
Here is a new patch. I integrated most your suggestions and I hope it'll
be in time for the feature freeze.
Also the `electric-pair-newline-between-pairs-rule' rule in
`electric-layout-rules'. Which I think should be global, but I'll reply
to those mails separately.
João
Stefan Monnier <monnier@IRO.UMontreal.CA> writes:
>>> I think this change is for the worst. I often rely on ".." pairing in
>>> things like text-mode where they don't have `string' syntax.
>> But if you "rely" on those in text-mode, isn't the correct thing to give
>> them that syntax there?
> Why would it be a problem to keep them there?
It's not, not anymore.
> Still: you changed the default on the grounds that "it doesn't work
> well", but it's worked well enough so far.
It didn't work well when you try to make the new default be inhibit- and
skip predicates that attempt to balance quotes (among other things). But
now that the order of lookup has been inverted and I can do that.
>>> I suggest you use "text" rather than "non-code" in the variable names
>>> (and indeed, the behavior in strings and comments should largely be the
>>> same as in text-mode).
>> I disagree, but won't insist.
> On which part (the quoted text has at least 2 separate arguments)?
Sorry, the second one. I like comments and strings to have prog-mode
syntax, so I can get half-assed quote balancing there. So in my config
I'll set `electric-pair-text-syntax-table'to `prog-mode-syntax-table'.
>>> (save-excursion (car (syntax-ppss (point-max))))
>> This works... but only for one kind of parenthesis. It incorrectly
>> reports 0 for a buffer with '[)',
> Ah, I guess that indeed introduces some complications, indeed.
> Thanks for clearing things up.
I've spent a substantial amount of time analysing the balancing problem
more thoroughly. Turns out, the mixed-parens situation is not the only
counter-example to your extreme simplification
(save-excursion (car (syntax-ppss (point-max))))
The buffer situation "())", for example, has too many closers. But, to
help balance, you want it to *not* autopair at the beginning and *do*
autopair at the end. There is a converse "too-many-openers" situation
for skipping. And then there are the mixed-parens situations.
I don't have a formalism for it (though it might be interesting to
create one, if one is so inclined), but do have a lots of unit tests,
that I invite you to inspect (especially their docstrings, which say
things like)
With " (]) ", try input ) at point 5. Should stay " (]) " and
point at 6
With " (())) ", try input ( at point 1. Should become "( (())) " and
point at 2
So, to summarize, it appears one does need an algorithm a little bit
more complicated and one that performs a kind of local search of
successive uplisting. Since the last patch, I've discovered and fixed
some balancing bugs and condensed such an algorithm in the function
`electric-pair--balance-info'. I've integrated your suggestions, using
`syntax-ppss' whenever possible.
It can probably be made simpler, but passes all the tests and seems now
as usable as autopair.el, if not more.
Your simplification for `electric-pair--looking-at-mismatched-string-p'
was also found to fail some tests, so I kept my previous naive
version. But it's not too many tests and I would not mind if you revert
it.
> You might give this obvious example somewhere in a comment.
I added a section there in the middle explaining the gist of it.
>>> So I think we need a "electric-pair-preserve-balance" flag to control
>>> those features.
>> OK, but I would set it to t.
> Yes, it should default to t (unless too many people complain).
Here too, I've kind of changed my mind :-) Balancing can already be
turned off by customizing two variables:
electric-pair-inhibit-predicate
electric-pair-skip-self
To disable balancing, we can set these to t and nil respectively. If not
we could also add function `electric-pair-toggle-balancing' that sets
the two variables locally for the buffers.
Lastly, if you insist, then do create `electric-pair-preserve-balance'
variable and set the above two vars to 2 new "default" functions that
check it and delegate to the "balance" functions appropriately. However
i think that defeats the kind simplicity of the defcustom (though I
don't much use `custom').
>>> The priorities can be added as symbol properties, so the "sort" function
Done.
>>>> +(defvar electric-pair-non-code-syntax-table prog-mode-syntax-table
>>> Why prog-mode-syntax-table, rather than (say) text-mode-syntax-table?
>> Explained above, but I don't object to text-mode-syntax-table.
> Can you give more concrete examples?
Explained above again. Using prog-mode-syntax-table allows me to get
some quote balancing in comments and strings.
>>> BTW, syntax-ppss will get majorly confused if you call it while
>>> a different syntax-table is temporarily installed.
>> Never bit me, but thanks for the heads-up.
> It rarely bites, because in most cases syntax-ppss is pre-computed
Anyway, I took the advice seriously and should have gotten rid of such
potential problems.
I created a `electric-pair--syntax-ppss' function that calls
`parse-partial-sexp' when it detects it's inside a comment. It also does
when it detects it's in c-mode or c++-mode, since in my testing
syntax-ppss is sometimes broken there (tried in in src/syntax.c)
>> shoudn't newline-and-indent also call the post-self-insertion hooks?
> I guess so, yes.
OK. And is the `(not (or executing-kbd-macro noninteractive))' valid?
OTOH if `newline-and-indent' is somehow unfavoured over
`electric-indent-mode', we could leave it as is to encourage people to
move.
diff --git a/lisp/electric.el b/lisp/electric.el
index 91b99b4..c2bbbb5 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -187,6 +187,22 @@ Returns nil when we can't find this char."
(eq (char-before) last-command-event)))))
pos)))
+(put 'electric-pair-post-self-insert-function 'priority 20)
+(put 'electric-layout-post-self-insert-function 'priority 40)
+(put 'electric-indent-post-self-insert-function 'priority 60)
+(put 'blink-paren-post-self-insert-function 'priority 100)
+
+(defun electric--sort-post-self-insertion-hook ()
+ "Ensure order of electric functions in `post-self-insertion-hook'.
+
+Hooks in this variable interact in non-trivial ways, so a
+relative order must be maintained within it."
+ (setq-default post-self-insert-hook
+ (sort (default-value 'post-self-insert-hook)
+ #'(lambda (fn1 fn2)
+ (< (or (get fn1 'priority) 0)
+ (or (get fn2 'priority) 0))))))
+
;;; Electric indentation.
;; Autoloading variables is generally undesirable, but major modes
@@ -295,20 +311,9 @@ insert a character from `electric-indent-chars'."
#'electric-indent-post-self-insert-function))
(when (eq (lookup-key global-map [?\C-j]) 'newline-and-indent)
(define-key global-map [?\C-j] 'electric-indent-just-newline))
- ;; post-self-insert-hooks interact in non-trivial ways.
- ;; It turns out that electric-indent-mode generally works better if run
- ;; late, but still before blink-paren.
(add-hook 'post-self-insert-hook
- #'electric-indent-post-self-insert-function
- 'append)
- ;; FIXME: Ugly!
- (let ((bp (memq #'blink-paren-post-self-insert-function
- (default-value 'post-self-insert-hook))))
- (when (memq #'electric-indent-post-self-insert-function bp)
- (setcar bp #'electric-indent-post-self-insert-function)
- (setcdr bp (cons #'blink-paren-post-self-insert-function
- (delq #'electric-indent-post-self-insert-function
- (cdr bp))))))))
+ #'electric-indent-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)))
;;;###autoload
(define-minor-mode electric-indent-local-mode
@@ -327,32 +332,122 @@ insert a character from `electric-indent-chars'."
(defcustom electric-pair-pairs
'((?\" . ?\"))
- "Alist of pairs that should be used regardless of major mode."
+ "Alist of pairs that should be used regardless of major mode.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the mode's syntax
+table.
+
+See also the variable `electric-pair-text-pairs'."
:version "24.1"
:type '(repeat (cons character character)))
-(defcustom electric-pair-skip-self t
+(defcustom electric-pair-text-pairs
+ '((?\" . ?\" ))
+ "Alist of pairs that should always be used in comments and strings.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the syntax table
+defined in `electric-pair-text-syntax-table'"
+ :version "24.4"
+ :type '(repeat (cons character character)))
+
+(defcustom electric-pair-skip-self #'electric-pair-skip-if-helps-balance
"If non-nil, skip char instead of inserting a second closing paren.
+
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."
+
+This can be convenient for people who find it easier to hit ) than C-f.
+
+Can also be a function of one argument (the closer char just
+inserted), in which case that function's return value is
+considered instead."
:version "24.1"
- :type 'boolean)
+ :type '(choice
+ (const :tag "Never skip" nil)
+ (const :tag "Help balance" electric-pair-skip-if-helps-balance)
+ (const :tag "Always skip" t)
+ 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)
+(defcustom electric-pair-delete-adjacent-pairs t
+ "If non-nil, backspacing an open paren also deletes adjacent closer.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace t
+ "If non-nil skip whitespace when skipping over closing parens.
+
+The symbol `chomp' specifies that the skipped-over whitespace
+should be deleted.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes, jump over whitespace" t)
+ (const :tag "Yes, and delete whitespace" 'chomp)
+ (const :tag "No, no whitespace skipping" nil)
+ function))
+
+(defvar electric-pair-text-syntax-table text-mode-syntax-table
+ "Syntax table used when pairing inside comments and strings.
+
+`electric-pair-mode' considers this syntax table only when point
+in inside quotes or comments. If lookup fails here,
+`electric-pair-text-pairs' will be considered.")
+
+(defun electric-pair-backward-delete-char (n &optional killflag untabify)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char' or, if
+UNTABIFY is non-nil, `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (let* ((prev (char-before))
+ (next (char-after))
+ (syntax-info (electric-pair-syntax-info prev))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (when (and (if (functionp electric-pair-delete-adjacent-pairs)
+ (funcall electric-pair-delete-adjacent-pairs)
+ electric-pair-delete-adjacent-pairs)
+ next
+ (memq syntax '(?\( ?\" ?\$))
+ (eq pair next))
+ (delete-char 1 killflag))
+ (if untabify
+ (backward-delete-char-untabify n killflag)
+ (backward-delete-char n killflag))))
+
+(defun electric-pair-backward-delete-char-untabify (n &optional killflag)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (electric-pair-backward-delete-char n killflag t))
+
+(defun electric-pair-conservative-inhibit (char)
(or
;; I find it more often preferable not to pair when the
;; same char is next.
@@ -363,14 +458,40 @@ closer."
;; I also find it often preferable not to pair next to a word.
(eq (char-syntax (following-char)) ?w)))
-(defun electric-pair-syntax (command-event)
- (let ((x (assq command-event electric-pair-pairs)))
+(defun electric-pair-syntax-info (command-event &optional offset)
+ "Calculate a list (SYNTAX PAIR UNCONDITIONAL STRING-OR-COMMENT-START).
+
+SYNTAX is COMMAND-EVENT's syntax character. PAIR is
+COMMAND-EVENT's pair. UNCONDITIONAL indicates the variables
+`electric-pair-pairs' or `electric-pair-text-pairs' were used to
+lookup syntax. STRING-OR-COMMENT-START indicates that point is
+inside a comment of string."
+ (let* ((pre-string-or-comment (nth 8 (save-excursion
+ (syntax-ppss (1- (point))))))
+ (post-string-or-comment (nth 8 (syntax-ppss (point))))
+ (string-or-comment (and post-string-or-comment
+ pre-string-or-comment))
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (table-syntax-and-pair (with-syntax-table table
+ (list (char-syntax command-event)
+ (or (matching-paren command-event)
+ command-event))))
+ (fallback (if string-or-comment
+ (append electric-pair-text-pairs
+ electric-pair-pairs)
+ electric-pair-pairs))
+ (direct (assq command-event fallback))
+ (reverse (rassq command-event fallback)))
(cond
- (x (if (eq (car x) (cdr x)) ?\" ?\())
- ((rassq command-event electric-pair-pairs) ?\))
- ((nth 8 (syntax-ppss))
- (with-syntax-table text-mode-syntax-table (char-syntax command-event)))
- (t (char-syntax command-event)))))
+ ((memq (car table-syntax-and-pair)
+ '(?\" ?\( ?\) ?\$))
+ (append table-syntax-and-pair (list nil string-or-comment)))
+ (direct (if (eq (car direct) (cdr direct))
+ (list ?\" command-event t string-or-comment)
+ (list ?\( (cdr direct) t string-or-comment)))
+ (reverse (list ?\) (car reverse) t string-or-comment)))))
(defun electric-pair--insert (char)
(let ((last-command-event char)
@@ -378,56 +499,262 @@ closer."
(electric-pair-mode nil))
(self-insert-command 1)))
+(defun electric-pair--syntax-ppss (&optional pos where)
+ "Like `syntax-ppss', but sometimes fallback to `parse-partial-sexp'.
+
+WHERE is list defaulting to '(string comment) and indicates
+when to fallback to `parse-partial-sexp'."
+ (let* ((pos (or pos (point)))
+ (where (or where '(string comment)))
+ (quick-ppss (syntax-ppss))
+ (quick-ppss-at-pos (syntax-ppss pos)))
+ (if (or (and (nth 3 quick-ppss) (memq 'string where))
+ (and (nth 4 quick-ppss) (memq 'comment where)))
+ (with-syntax-table electric-pair-text-syntax-table
+ (parse-partial-sexp (1+ (nth 8 quick-ppss)) pos))
+ ;; HACK! cc-mode apparently has some `syntax-ppss' bugs
+ (if (memq major-mode '(c-mode c++ mode))
+ (parse-partial-sexp (point-min) pos)
+ quick-ppss-at-pos))))
+
+;; Balancing means controlling pairing and skipping of parentheses so
+;; that, if possible, the buffer ends up at least as balanced as
+;; before, if not more. The algorithm is slightly complex because some
+;; situations like "()))" need pairing to occur at the end but not at
+;; the beginning. Balancing should also happen independently for
+;; different types of parentheses, so that having your {}'s unbalanced
+;; doesn't keep `electric-pair-mode' from balancing your ()'s and your
+;; []'s.
+(defun electric-pair--balance-info (direction string-or-comment)
+ "Examine lists forward or backward according to DIRECTIONS's sign.
+
+STRING-OR-COMMENT is info suitable for running `parse-partial-sexp'.
+
+Return a cons of two descritions (MATCHED-P . PAIR) for the
+innermost and outermost lists that enclose point. The outermost
+list enclosing point is either the first top-level or first
+mismatched list found by uplisting.
+
+If the outermost list is matched, don't rely on its PAIR. If
+point is not enclosed by any lists, return ((T) (T))."
+ (let* (innermost
+ outermost
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (at-top-level-or-equivalent-fn
+ ;; called when `scan-sexps' ran perfectly, when when it
+ ;; found a parenthesis pointing in the direction of
+ ;; travel. Also when travel started inside a comment and
+ ;; exited it
+ #'(lambda ()
+ (setq outermost (list t))
+ (unless innermost
+ (setq innermost (list t)))))
+ (ended-prematurely-fn
+ ;; called when `scan-sexps' crashed against a parenthesis
+ ;; pointing opposite the direction of travel. After
+ ;; traversing that character, the idea is to travel one sexp
+ ;; in the opposite direction looking for a matching
+ ;; delimiter.
+ #'(lambda ()
+ (let* ((pos (point))
+ (matched
+ (save-excursion
+ (cond ((< direction 0)
+ (condition-case nil
+ (eq (char-after pos)
+ (with-syntax-table table
+ (matching-paren
+ (char-before
+ (scan-sexps (point) 1)))))
+ (scan-error nil)))
+ (t
+ ;; In this case, no need to use
+ ;; `scan-sexps', we can use some
+ ;; `electric-pair--syntax-ppss' in this
+ ;; case (which uses the quicker
+ ;; `syntax-ppss' in some cases)
+ (let* ((ppss (electric-pair--syntax-ppss
+ (1- (point))))
+ (start (car (last (nth 9 ppss))))
+ (opener (char-after start)))
+ (and start
+ (eq (char-before pos)
+ (or (with-syntax-table table
+ (matching-paren opener))
+ opener))))))))
+ (actual-pair (if (> direction 0)
+ (char-before (point))
+ (char-after (point)))))
+ (unless innermost
+ (setq innermost (cons matched actual-pair)))
+ (unless matched
+ (setq outermost (cons matched actual-pair)))))))
+ (save-excursion
+ (while (not outermost)
+ (condition-case err
+ (with-syntax-table table
+ (scan-sexps (point) (if (> direction 0)
+ (point-max)
+ (- (point-max))))
+ (funcall at-top-level-or-equivalent-fn))
+ (scan-error
+ (cond ((or
+ ;; some error happened and it is not of the "ended
+ ;; prematurely" kind"...
+ (not (string-match "ends prematurely" (nth 1 err)))
+ ;; ... or we were in a comment and just came out of
+ ;; it.
+ (and string-or-comment
+ (not (nth 8 (syntax-ppss)))))
+ (funcall at-top-level-or-equivalent-fn))
+ (t
+ ;; exit the sexp
+ (goto-char (nth 3 err))
+ (funcall ended-prematurely-fn)))))))
+ (cons innermost outermost)))
+
+(defun electric-pair--looking-at-unterminated-string-p (char)
+ "Say if following string starts with CHAR and is unterminated."
+ ;; FIXME: ugly/naive
+ (save-excursion
+ (skip-chars-forward (format "^%c" char))
+ (while (not (zerop (% (save-excursion (skip-syntax-backward "\\")) 2)))
+ (unless (eobp)
+ (forward-char 1)
+ (skip-chars-forward (format "^%c" char))))
+ (and (not (eobp))
+ (condition-case err
+ (progn (forward-sexp) nil)
+ (scan-error t)))))
+
+(defun electric-pair--inside-string-p (char)
+ "Say if point is inside a string started by CHAR.
+
+A comments text is parsed with `electric-pair-text-syntax-table'.
+Also consider strings within comments, but not strings within
+strings."
+ ;; FIXME: could also consider strings within strings by examining
+ ;; delimiters.
+ (let* ((ppss (electric-pair--syntax-ppss (point) '(comment))))
+ (memq (nth 3 ppss) (list t char))))
+
+(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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq ?\( syntax)
+ (let* ((pair-data
+ (electric-pair--balance-info 1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (cond ((car outermost)
+ nil)
+ (t
+ (eq (cdr outermost) pair)))))
+ ((eq syntax ?\")
+ (electric-pair--looking-at-unterminated-string-p 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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq syntax ?\))
+ (let* ((pair-data
+ (electric-pair--balance-info
+ -1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (and
+ (cond ((car outermost)
+ (car innermost))
+ ((car innermost)
+ (not (eq (cdr outermost) pair)))))))
+ ((eq syntax ?\")
+ (electric-pair--inside-string-p 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)))
- (closer (if (eq syntax ?\()
- (cdr (or (assq last-command-event electric-pair-pairs)
- (aref (syntax-table) last-command-event)))
- last-command-event)))
- (cond
- ((null pos) nil)
- ;; Wrap a pair around the active region.
- ((and (memq syntax '(?\( ?\" ?\$)) (use-region-p))
- ;; FIXME: To do this right, we'd need a post-self-insert-function
- ;; so we could add-function around it and insert the closer after
- ;; all the rest of the hook has run.
- (if (>= (mark) (point))
- (goto-char (mark))
- ;; We already inserted the open-paren but at the end of the
- ;; region, so we have to remove it and start over.
- (delete-region (1- pos) (point))
- (save-excursion
- (goto-char (mark))
- (electric-pair--insert last-command-event)))
- ;; Since we're right after the closer now, we could tell the rest of
- ;; post-self-insert-hook that we inserted `closer', but then we'd get
- ;; blink-paren to kick in, which is annoying.
- ;;(setq last-command-event closer)
- (insert closer))
- ;; Backslash-escaped: no pairing, no skipping.
- ((save-excursion
- (goto-char (1- pos))
- (not (zerop (% (skip-syntax-backward "\\") 2))))
- nil)
- ;; Skip self.
- ((and (memq syntax '(?\) ?\" ?\$))
- 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
- ;; undo-log and in the intermediate state which might be visible to other
- ;; post-self-insert-hook. We'll just have to live with it for now.
- (delete-char 1))
- ;; Insert matching pair.
- ((not (or (not (memq syntax `(?\( ?\" ?\$)))
- overwrite-mode
- (funcall electric-pair-inhibit-predicate last-command-event)))
- (save-excursion (electric-pair--insert closer))))))
+ (skip-whitespace-info))
+ (pcase (electric-pair-syntax-info last-command-event -1)
+ (`(,syntax ,pair ,unconditional ,_)
+ (cond
+ ((null pos) nil)
+ ;; Wrap a pair around the active region.
+ ;;
+ ((and (memq syntax '(?\( ?\) ?\" ?\$)) (use-region-p))
+ ;; FIXME: To do this right, we'd need a post-self-insert-function
+ ;; so we could add-function around it and insert the closer after
+ ;; all the rest of the hook has run.
+ (if (or (eq syntax ?\")
+ (and (eq syntax ?\))
+ (>= (point) (mark)))
+ (and (not (eq syntax ?\)))
+ (>= (mark) (point))))
+ (save-excursion
+ (goto-char (mark))
+ (electric-pair--insert pair))
+ (delete-region pos (1- pos))
+ (electric-pair--insert pair)
+ (goto-char (mark))
+ (electric-pair--insert last-command-event)))
+ ;; Backslash-escaped: no pairing, no skipping.
+ ((save-excursion
+ (goto-char (1- pos))
+ (not (zerop (% (skip-syntax-backward "\\") 2))))
+ nil)
+ ;; Skip self.
+ ((and (memq syntax '(?\) ?\" ?\$))
+ (and (or unconditional
+ (if (functionp electric-pair-skip-self)
+ (funcall electric-pair-skip-self last-command-event)
+ electric-pair-skip-self))
+ (save-excursion
+ (when (setq skip-whitespace-info
+ (if (functionp electric-pair-skip-whitespace)
+ (funcall electric-pair-skip-whitespace)
+ electric-pair-skip-whitespace))
+ (skip-chars-forward "\n\t\s"))
+ (eq (char-after) 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
+ ;; undo-log and in the intermediate state which might be visible to other
+ ;; post-self-insert-hook. We'll just have to live with it for now.
+ (when skip-whitespace-info
+ (skip-chars-forward "\n\t\s"))
+ (delete-region (1- pos) (if (eq skip-whitespace-info 'chomp)
+ (point)
+ pos))
+ (forward-char))
+ ;; Insert matching pair.
+ ((and (memq syntax `(?\( ?\" ?\$))
+ (not overwrite-mode)
+ (or unconditional
+ (not (funcall electric-pair-inhibit-predicate
+ last-command-event))))
+ (save-excursion (electric-pair--insert pair))))))))
(defun electric-pair-will-use-region ()
(and (use-region-p)
- (memq (electric-pair-syntax last-command-event) '(?\( ?\" ?\$))))
+ (memq (car (electric-pair-syntax-info last-command-event))
+ '(?\( ?\) ?\" ?\$))))
;;;###autoload
(define-minor-mode electric-pair-mode
@@ -442,10 +769,19 @@ closing parenthesis. \(Likewise for brackets, etc.)
See options `electric-pair-pairs' and `electric-pair-skip-self'."
:global t :group 'electricity
+ :keymap (let ((map (make-sparse-keymap)))
+ (define-key map [remap backward-delete-char-untabify]
+ 'electric-pair-backward-delete-char-untabify)
+ (define-key map [remap backward-delete-char]
+ 'electric-pair-backward-delete-char)
+ (define-key map [remap delete-backward-char]
+ 'electric-pair-backward-delete-char)
+ map)
(if electric-pair-mode
(progn
(add-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)
(add-hook 'self-insert-uses-region-functions
#'electric-pair-will-use-region))
(remove-hook 'post-self-insert-hook
@@ -455,12 +791,18 @@ See options `electric-pair-pairs' and `electric-pair-skip-self'."
;;; Electric newlines after/before/around some chars.
-(defvar electric-layout-rules '()
+(defvar electric-layout-rules
+ `((?\n . ,#'electric-pair-newline-between-pairs-rule))
"List of rules saying where to automatically insert newlines.
-Each rule has the form (CHAR . WHERE) where CHAR is the char
-that was just inserted and WHERE specifies where to insert newlines
-and can be: nil, `before', `after', `around', or a function of no
-arguments that returns one of those symbols.")
+
+Each rule has the form (CHAR . WHERE) where CHAR is the char that
+was just inserted and WHERE specifies where to insert newlines
+and can be: nil, `before', `after', `around', `after-stay', or a
+function of no arguments that returns one of those symbols.
+
+The symbols specify where in relation to CHAR the newline
+character(s) should be inserted. `after-stay' means insert a
+newline-after CHAR but stay in the same place.")
(defun electric-layout-post-self-insert-function ()
(let* ((rule (cdr (assq last-command-event electric-layout-rules)))
@@ -469,23 +811,39 @@ arguments that returns one of those symbols.")
(setq pos (electric--after-char-pos))
;; Not in a string or comment.
(not (nth 8 (save-excursion (syntax-ppss pos)))))
- (let ((end (copy-marker (point) t)))
+ (let ((end (copy-marker (point)))
+ (sym (if (functionp rule) (funcall rule) rule)))
+ (set-marker-insertion-type end (not (eq sym 'after-stay)))
(goto-char pos)
- (pcase (if (functionp rule) (funcall rule) rule)
+ (case sym
;; FIXME: we used `newline' down here which called
;; self-insert-command and ran post-self-insert-hook recursively.
;; It happened to make electric-indent-mode work automatically with
;; electric-layout-mode (at the cost of re-indenting lines
;; multiple times), but I'm not sure it's what we want.
+ ;;
+ ;; FIXME: check eolp before inserting \n?
(`before (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (`after (insert "\n")) ; FIXME: check eolp before inserting \n?
+ (unless (bolp) (insert "\n")))
+ (`after (insert "\n"))
+ (`after-stay (save-excursion
+ (let ((electric-layout-rules nil))
+ (newline 1 t))))
(`around (save-excursion
- (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (insert "\n"))) ; FIXME: check eolp before inserting \n?
+ (goto-char (1- pos)) (skip-chars-backward " \t")
+ (unless (bolp) (insert "\n")))
+ (insert "\n"))) ; FIXME: check eolp before inserting \n?
(goto-char end)))))
+(defun electric-pair-newline-between-pairs-rule ()
+ (when (and electric-pair-mode
+ (not (eobp))
+ (eq (save-excursion
+ (skip-chars-backward "\t\s")
+ (char-before (1- (point))))
+ (matching-paren (char-after))))
+ 'after-stay))
+
;;;###autoload
(define-minor-mode electric-layout-mode
"Automatically insert newlines around some chars.
@@ -494,11 +852,13 @@ positive, and disable it otherwise. If called from Lisp, enable
the mode if ARG is omitted or nil.
The variable `electric-layout-rules' says when and how to insert newlines."
:global t :group 'electricity
- (if electric-layout-mode
- (add-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)
- (remove-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)))
+ (cond (electric-layout-mode
+ (add-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook))
+ (t
+ (remove-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function))))
(provide 'electric)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index f4e9b31..5194e73 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -472,7 +472,12 @@ font-lock keywords will not be case sensitive."
(font-lock-mark-block-function . mark-defun)
(font-lock-syntactic-face-function
. lisp-font-lock-syntactic-face-function)))
- (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
+ (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
+ ;; electric
+ (when elisp
+ (setq-local electric-pair-text-pairs
+ (cons '(?\` . ?\') electric-pair-text-pairs)))
+ (setq-local electric-pair-skip-whitespace 'chomp))
(defun lisp-outline-level ()
"Lisp mode `outline-level' function."
diff --git a/lisp/simple.el b/lisp/simple.el
index 260c170..c591cee 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -607,7 +607,7 @@ In some text modes, where TAB inserts a tab, this command indents to the
column specified by the function `current-left-margin'."
(interactive "*")
(delete-horizontal-space t)
- (newline)
+ (newline 1 (not (or executing-kbd-macro noninteractive)))
(indent-according-to-mode))
(defun reindent-then-newline-and-indent ()
diff --git a/test/automated/electric-tests.el b/test/automated/electric-tests.el
new file mode 100644
index 0000000..c31808e
--- /dev/null
+++ b/test/automated/electric-tests.el
@@ -0,0 +1,482 @@
+;;; electric-tests.el --- tests for electric.el -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2013 João Távora
+
+;; Author: João Távora <joaotavora@gmail.com>
+;; Keywords:
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'ert-x)
+(require 'electric)
+(require 'cl-lib)
+
+(defun call-with-saved-electric-modes (fn)
+ (let ((saved-electric (if electric-pair-mode 1 -1))
+ (saved-layout (if electric-layout-mode 1 -1))
+ (saved-indent (if electric-indent-mode 1 -1)))
+ (electric-pair-mode -1)
+ (electric-layout-mode -1)
+ (electric-indent-mode -1)
+ (unwind-protect
+ (funcall fn)
+ (electric-pair-mode saved-electric)
+ (electric-indent-mode saved-indent)
+ (electric-layout-mode saved-layout))))
+
+(defmacro save-electric-modes (&rest body)
+ (declare (indent defun) (debug t))
+ `(call-with-saved-electric-modes #'(lambda () ,@body)))
+
+(defun electric-pair-test-for (fixture where char expected-string
+ expected-point mode bindings fixture-fn)
+ (with-temp-buffer
+ (funcall mode)
+ (insert fixture)
+ (save-electric-modes
+ (let ((last-command-event char))
+ (goto-char where)
+ (funcall fixture-fn)
+ (progv
+ (mapcar #'car bindings)
+ (mapcar #'cdr bindings)
+ (self-insert-command 1))))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected-string))
+ (should (equal (point)
+ expected-point))))
+
+(eval-when-compile
+ (defun electric-pair-define-test-form (name fixture
+ char
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn)
+ (let* ((expected-string-and-point
+ (if skip-pair-string
+ (with-temp-buffer
+ (progv
+ ;; FIXME: avoid `eval'
+ (mapcar #'car (eval bindings))
+ (mapcar #'cdr (eval bindings))
+ (funcall mode)
+ (insert fixture)
+ (goto-char (1+ pos))
+ (insert char)
+ (cond ((eq (aref skip-pair-string pos)
+ ?p)
+ (insert (cadr (electric-pair-syntax-info char)))
+ (backward-char 1))
+ ((eq (aref skip-pair-string pos)
+ ?s)
+ (delete-char -1)
+ (forward-char 1)))
+ (list
+ (buffer-substring-no-properties (point-min) (point-max))
+ (point))))
+ (list expected-string expected-point)))
+ (expected-string (car expected-string-and-point))
+ (expected-point (cadr expected-string-and-point))
+ (fixture (format "%s%s%s" prefix fixture suffix))
+ (expected-string (format "%s%s%s" prefix expected-string suffix))
+ (expected-point (+ (length prefix) expected-point))
+ (pos (+ (length prefix) pos)))
+ `(ert-deftest ,(intern (format "electric-pair-%s-at-point-%s-in-%s%s"
+ name
+ (1+ pos)
+ mode
+ extra-desc))
+ ()
+ ,(format "With \"%s\", try input %c at point %d. \
+Should %s \"%s\" and point at %d"
+ fixture
+ char
+ (1+ pos)
+ (if (string= fixture expected-string)
+ "stay"
+ "become")
+ (replace-regexp-in-string "\n" "\\\\n" expected-string)
+ expected-point)
+ (electric-pair-test-for ,fixture
+ ,(1+ pos)
+ ,char
+ ,expected-string
+ ,expected-point
+ ',mode
+ ,bindings
+ ,fixture-fn)))))
+
+(cl-defmacro define-electric-pair-test
+ (name fixture
+ input
+ &key
+ skip-pair-string
+ expected-string
+ expected-point
+ bindings
+ (modes '(quote (emacs-lisp-mode ruby-mode c++-mode)))
+ (test-in-comments t)
+ (test-in-strings t)
+ (test-in-code t)
+ (fixture-fn #'(lambda ()
+ (electric-pair-mode 1))))
+ `(progn
+ ,@(cl-loop
+ for mode in (eval modes) ;FIXME: avoid `eval'
+ append
+ (cl-loop
+ for (prefix suffix extra-desc) in
+ (append (if test-in-comments
+ `((,(with-temp-buffer
+ (funcall mode)
+ (insert "z")
+ (comment-region (point-min) (point-max))
+ (buffer-substring-no-properties (point-min)
+ (1- (point-max))))
+ ""
+ "-in-comments")))
+ (if test-in-strings
+ `(("\"" "\"" "-in-strings")))
+ (if test-in-code
+ `(("" "" ""))))
+ append
+ (cl-loop
+ for char across input
+ for pos from 0
+ unless (eq char ?-)
+ collect (electric-pair-define-test-form
+ name
+ fixture
+ (aref input pos)
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn))))))
+\f
+;;; Basic pairings and skippings
+;;;
+(define-electric-pair-test balanced-situation
+ " (()) " "(((((((" :skip-pair-string "ppppppp"
+ :modes '(ruby-mode))
+
+(define-electric-pair-test too-many-openings
+ " ((()) " "(((((((" :skip-pair-string "ppppppp")
+
+(define-electric-pair-test too-many-closings
+ " (())) " "(((((((" :skip-pair-string "------p")
+
+(define-electric-pair-test too-many-closings-2
+ "() ) " "---(---" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-3
+ ")() " "(------" :skip-pair-string "-------")
+
+(define-electric-pair-test balanced-autoskipping
+ " (()) " "---))--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test too-many-openings-autoskipping
+ " ((()) " "----))-" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-autoskipping
+ " (())) " "---)))-" :skip-pair-string "---sss-")
+
+\f
+;;; Mixed parens
+;;;
+(define-electric-pair-test mixed-paren-1
+ " ()] " "-(-(---" :skip-pair-string "-p-p---")
+
+(define-electric-pair-test mixed-paren-2
+ " [() " "-(-()--" :skip-pair-string "-p-ps--")
+
+(define-electric-pair-test mixed-paren-3
+ " (]) " "-(-()--" :skip-pair-string "---ps--")
+
+(define-electric-pair-test mixed-paren-4
+ " ()] " "---)]--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test mixed-paren-5
+ " [() " "----(--" :skip-pair-string "----p--")
+
+(define-electric-pair-test find-matching-different-paren-type
+ " ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test find-matching-different-paren-type-inside-list
+ "( ()]) " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test ignore-different-unmatching-paren-type
+ "( ()]) " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test autopair-keep-least-amount-of-mixed-unbalance
+ "( ()] " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test dont-autopair-to-resolve-mixed-unbalance
+ "( ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test autopair-so-as-not-to-worsen-unbalance-situation
+ "( (]) " "-[-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test skip-over-partially-balanced
+ " [([]) " "-----)---" :skip-pair-string "-----s---")
+
+(define-electric-pair-test only-skip-over-at-least-partially-balanced-stuff
+ " [([()) " "-----))--" :skip-pair-string "-----s---")
+
+
+
+\f
+;;; Quotes
+;;;
+(define-electric-pair-test pair-some-quotes-skip-others
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test skip-single-quotes-in-ruby-mode
+ " '' " "--'-" :skip-pair-string "--s-"
+ :modes '(ruby-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone
+ " \"' " "-\"'-" :skip-pair-string "----"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-2
+ " \"\\\"' " "-\"--'-" :skip-pair-string "------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-3
+ " foo\\''" "'------" :skip-pair-string "-------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test inhibit-only-if-next-is-mismatched
+ "\"foo\"\"bar" "\""
+ :expected-string "\"\"\"foo\"\"bar"
+ :expected-point 2
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+\f
+;;; More quotes, but now don't bind `electric-pair-text-syntax-table'
+;;; to `prog-mode-syntax-table'. Use the defaults for
+;;; `electric-pair-pairs' and `electric-pair-text-pairs'.
+;;;
+(define-electric-pair-test pairing-skipping-quotes-in-code
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :test-in-comments nil)
+
+(define-electric-pair-test skipping-quotes-in-comments
+ " \"\" " "--\"-----" :skip-pair-string "--s------"
+ :test-in-strings nil)
+
+\f
+;;; Skipping over whitespace
+;;;
+(define-electric-pair-test whitespace-jumping
+ " ( ) " "--))))---" :expected-string " ( ) " :expected-point 8
+ :bindings '((electric-pair-skip-whitespace . t)))
+
+(define-electric-pair-test whitespace-chomping
+ " ( ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+(define-electric-pair-test whitespace-chomping-2
+ " ( \n\t\t\n ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+\f
+;;; Pairing arbitrary characters
+;;;
+(define-electric-pair-test angle-brackets-everywhere
+ "<>" "<>" :skip-pair-string "ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(define-electric-pair-test angle-brackets-everywhere-2
+ "(<>" "-<>" :skip-pair-string "-ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(defvar electric-pair-test-angle-brackets-table
+ (let ((table (make-syntax-table prog-mode-syntax-table)))
+ (modify-syntax-entry ?\< "(>" table)
+ (modify-syntax-entry ?\> ")<`" table)
+ table))
+
+(define-electric-pair-test angle-brackets-pair
+ "<>" "<" :expected-string "<><>" :expected-point 2
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test angle-brackets-skip
+ "<>" "->" :expected-string "<>" :expected-point 3
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test pair-backtick-and-quote-in-comments
+ ";; " "---`" :expected-string ";; `'" :expected-point 5
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-comments
+ ";; `foo'" "-------'" :expected-string ";; `foo'" :expected-point 9
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test pair-backtick-and-quote-in-strings
+ "\"\"" "-`" :expected-string "\"`'\"" :expected-point 3
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings
+ "\"`'\"" "--'" :expected-string "\"`'\"" :expected-point 4
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings-2
+ " \"`'\"" "----'" :expected-string " \"`'\"" :expected-point 6
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+\f
+;;; `js-mode' has `electric-layout-rules' for '{ and '}
+;;;
+(define-electric-pair-test js-mode-braces
+ "" "{" :expected-string "{}" :expected-point 2
+ :modes '(js-mode)
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout
+ "" "{" :expected-string "{\n\n}" :expected-point 3
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-layout-mode 1)
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout-and-indent
+ "" "{" :expected-string "{\n \n}" :expected-point 7
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (electric-layout-mode 1)))
+
+\f
+;;; Backspacing
+;;; TODO: better tests
+;;;
+(ert-deftest electric-pair-backspace-1 ()
+ (save-electric-modes
+ (with-temp-buffer
+ (insert "()")
+ (goto-char 2)
+ (electric-pair-backward-delete-char 1)
+ (should (equal "" (buffer-string))))))
+
+\f
+;;; Autowrapping
+;;;
+(define-electric-pair-test autowrapping-1
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-2
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-3
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-4
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-5
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-6
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(provide 'electric-pair-tests)
+;;; electric-pair-tests.el ends here
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-15 1:39 ` Stefan Monnier
@ 2013-12-16 0:35 ` João Távora
2013-12-16 3:34 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-16 0:35 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel, Dmitry Gutov
Stefan Monnier <monnier@IRO.UMontreal.CA> writes:
>> Yes, but if it's set via electric-layout-rules, to what value will this
>> variable be set in e.g. js-mode?
>
> The value which seems most useful for javascript.
Right.
>> If it'll include what's there currently, '((?\; . after) (?\{ . after) (?\}
>> . before)), then to get the desired behavior I described previously (NOT to
>> insert a newline after I just typed `{', or any other character), I'd have
>> to modify it again in js-mode-hook.
I think that the current js-mode rules, when appended to the default
rule I proposed in the latest patch, will mostly supersede, but not
completely, the newline-between-pairs rule.
>
> If there can be various competing choices, then indeed we have a problem.
> The intention of electric-layout-mode is that it should more or less
> (tho in a naive way) insert the newlines for you if you just
> naively/sequentially type in the code.
Funny, since I didn't know of that "intention", I appreciated the
`electric-layout-mode' immediately for its potential, but found those
rules really akward.
I personally would prefer that the current rules would *not* be the
default in js-mode, as they are now, or any other mode). They could be
enabled with some function like
`electric-layout-toggle-electric-braces`.
Anyway, the default value should really be the newline-between-pairs
rule, which currently only kicks in when electric-pair-mode is
additionally enabled (btw, should it not?).
The only mode I know where it doesn't make sense is lisp-based-modes
(though probably one can find others). Still, even in lisp one hardly
ever newlines after a opening parens right? And even then, when coupled
with a `chomp' value for `electric-pair-skip-whitespace', it's quite
harmless. And finally, lisp-mode.el, or any other mode where we find it
to be harmful, can simply choose to clear all the rules locally.
FWIW, and from my experience in autopair.el, this rule is probably also
natural for people coming from other editors where the "electric"
behaviour is built-in, namely textmate (and possibly some of its
descendants). So users desperate to get that behaviour can just enable
all three electric modes and be happy 99% of the time.
> For that reason electric-layout-mode is off by default, and I haven't
> heard anyone argue to enable it by default.
But, if we wanted, we could make it on by default provided the default
rules are not annoying in the vast majority of modes (while the minority
locally sets them).
> From this POV, maybe electric-pair-newline-between-pairs-rule should be
> made into a separate minor mode, indeed.
This is also possible, but overkill IMO. I like the current triad of
electric modes.
João
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-16 3:22 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
>> On which part (the quoted text has at least 2 separate arguments)?
> Sorry, the second one. I like comments and strings to have prog-mode
> syntax, so I can get half-assed quote balancing there. So in my config
> I'll set `electric-pair-text-syntax-table'to `prog-mode-syntax-table'.
I'm not completely set on using text-mode-syntax-table. I think I can
be convinced to try prog-mode-syntax-table.
> The buffer situation "())", for example, has too many closers. But, to
> help balance, you want it to *not* autopair at the beginning and *do*
> autopair at the end.
Maybe you could handle this by considering
(- (car (syntax-ppss))
(save-excursion (car (syntax-ppss (point-max))))
But that doesn't solve the mixed-parens issue.
> Your simplification for `electric-pair--looking-at-mismatched-string-p'
> was also found to fail some tests, so I kept my previous naive
> version.
It'd be useful to keep track of which ones.
>> You might give this obvious example somewhere in a comment.
> I added a section there in the middle explaining the gist of it.
Great, thanks.
>>>> So I think we need a "electric-pair-preserve-balance" flag to control
>>>> those features.
>>> OK, but I would set it to t.
>> Yes, it should default to t (unless too many people complain).
> Here too, I've kind of changed my mind :-) Balancing can already be
> turned off by customizing two variables:
> electric-pair-inhibit-predicate
> electric-pair-skip-self
The thing is that the two kinda go together. Having to tweak both
together sounds like "coding" rather than "configuring".
> Lastly, if you insist, then do create `electric-pair-preserve-balance'
> variable and set the above two vars to 2 new "default" functions that
> check it and delegate to the "balance" functions appropriately. However
> i think that defeats the kind simplicity of the defcustom (though I
> don't much use `custom').
We could have the two functions check a new defcustom
electric-pair-preserve-balance and if nil fallback on the old default.
>>>>> +(defvar electric-pair-non-code-syntax-table prog-mode-syntax-table
>>>> Why prog-mode-syntax-table, rather than (say) text-mode-syntax-table?
>>> Explained above, but I don't object to text-mode-syntax-table.
>> Can you give more concrete examples?
> Explained above again. Using prog-mode-syntax-table allows me to get
> some quote balancing in comments and strings.
This is not really an example, let alone example*S*. Which quotes?
Why are they there? Is it only for quotes?
> It also does when it detects it's in c-mode or c++-mode, since in my
> testing syntax-ppss is sometimes broken there (tried in in
> src/syntax.c)
I'm not really surprised, sadly, but please report it as a bug (the fix
is probably to make cc-mode use syntax-propertize-function, which is
not a quick&easy fix since cc-mode currently sets the syntax-table in
a very contorted way spread over various places and times).
>>> shoudn't newline-and-indent also call the post-self-insertion hooks?
>> I guess so, yes.
> OK. And is the `(not (or executing-kbd-macro noninteractive))' valid?
I don't understand why you'd want (not (or executing-kbd-macro
noninteractive)) rather than any non-nil constant. Where does this (not
(or executing-kbd-macro noninteractive)) come from?
> +(put 'electric-pair-post-self-insert-function 'priority 20)
> +(put 'electric-layout-post-self-insert-function 'priority 40)
> +(put 'electric-indent-post-self-insert-function 'priority 60)
> +(put 'blink-paren-post-self-insert-function 'priority 100)
These belong next to the corresponding functions.
Also, if you know why the order is this way, please add comments
explaining it (I do know for blink-paren-post-self-insert-function and
electric-indent-post-self-insert-function, but not sure why
electric-layout-post-self-insert-function should come after
electric-pair-post-self-insert-function rather than the opposite).
> +(defcustom electric-pair-skip-whitespace t
> + "If non-nil skip whitespace when skipping over closing parens.
> +
> +The symbol `chomp' specifies that the skipped-over whitespace
> +should be deleted.
> +
> +Can also be a function of no arguments, in which case that function's
> +return value is considered instead."
This docstring still needs to be improved, because it still doesn't
explain what really happens. More specifically, which whitespace
is skipped (before or after the skipped paren?).
Other than those nitpicks, feel free to install those changes,
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-16 3:34 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel, Dmitry Gutov
> I personally would prefer that the current rules would *not* be the
> default in js-mode, as they are now, or any other mode).
I understand what you're saying as "I don't like electric-layout-mode".
FWIW, I agree with you.
> Anyway, the default value should really be the newline-between-pairs
> rule, which currently only kicks in when electric-pair-mode is
> additionally enabled (btw, should it not?).
Enabling electric-pair-mode is definitely not on the table for 24.4, no.
>> From this POV, maybe electric-pair-newline-between-pairs-rule should be
>> made into a separate minor mode, indeed.
> This is also possible, but overkill IMO. I like the current triad of
> electric modes.
I don't think it's overkill at all. It should be a separate minor mode
and default to enabled (but still conditional on electric-pair-mode).
I.e. make it part of electric-pair-mode and not try to shoe-horn it into
electric-layout-mode. I may have stated the opposite earlier, but
I think it's pretty clear to me, now, that the purpose of
electric-layout-mode is different.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-16 3:22 ` Stefan Monnier
@ 2013-12-16 14:21 ` João Távora
2013-12-16 15:30 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-16 14:21 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel
Stefan Monnier <monnier@IRO.UMontreal.CA> writes:
> I'm not completely set on using text-mode-syntax-table. I think I can
> be convinced to try prog-mode-syntax-table.
Oh :) Read below I'll give the example you asked for :-)
>> The buffer situation "())", for example, has too many closers. But, to
> Maybe you could handle this by considering
> (- (car (syntax-ppss))
> (save-excursion (car (syntax-ppss (point-max))))
> But that doesn't solve the mixed-parens issue.
Interesting.
>> Your simplification for `electric-pair--looking-at-mismatched-string-p'
>> was also found to fail some tests
> It'd be useful to keep track of which ones.
Using this simplification:
(save-excursion (eq char (nth 3 (syntax-ppss (point-max)))))
one gets 5 failures
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-c++-mode
With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-emacs-lisp-mode
With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-ruby-mode
With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
F electric-pair-leave-unbalanced-quotes-alone-2-at-point-4-in-ruby-mode-in-comments
With "# "\"' ", try input " at point 4. Should become "# ""\"' " and point at 5
F electric-pair-leave-unbalanced-quotes-alone-at-point-4-in-ruby-mode-in-comments
With "# "' ", try input " at point 4. Should become "# ""' " and point at 5
Using `electric-pair--syntax-ppss' fixes the last two but adds three
more (which are actually less becuase they're just mode-variations on the same test)
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-c++-mode
With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-emacs-lisp-mode
With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-ruby-mode
With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-3-in-ruby-mode-in-comments
With "# "foo""bar", try input " at point 3. Should become "# """foo""bar" and point at 4
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-4-in-c++-mode-in-comments
With "// "foo""bar", try input " at point 4. Should become "// """foo""bar" and point at 5
F electric-pair-inhibit-only-if-next-is-mismatched-at-point-4-in-emacs-lisp-mode-in-comments
With ";; "foo""bar", try input " at point 4. Should become ";; """foo""bar" and point at 5
>> Here too, I've kind of changed my mind :-) Balancing can already be
>> turned off by customizing two variables:
>> electric-pair-inhibit-predicate
>> electric-pair-skip-self
> The thing is that the two kinda go together. Having to tweak both
> together sounds like "coding" rather than "configuring".
Yeah, that's true.
> We could have the two functions check a new defcustom
> electric-pair-preserve-balance and if nil fallback on the old default.
Yes, that's what I meant, too. OK. So in the existing 2 variables custom
menu there will be basically 4 options:
- "always" or "never" (depending on whether it's pair or skip)
- "balance, maybe" (the default we're discussing)
- "balance, always"
- "don't balance, be conservative"
>> Explained above again. Using prog-mode-syntax-table allows me to get
>> some quote balancing in comments and strings.
> This is not really an example, let alone example*S*. Which quotes?
> Why are they there? Is it only for quotes?
OK. So Emacs -Q, M-x electric-pair-mode and then in the scratch buffer
go to some place in the comment's text and type a double quote. You get
it autopaired. If you type it again you get a skip. OK.
Now go back to the first quote and type it again. You get a skip. In my
opinion, not so nice. Delete the first quote, go back some words and
type another quote. You'll get an unbalanced string inside the
comment. Again not so nice.
Now if you do (setq electric-pair-text-syntax-table
prog-mode-syntax-table) and repeat the second paragraph, you'll get
results that I personally think are nicer.
BTW these are exactly the results that you mostly loose if you do the
`electric-pair--looking-at-mismatched-string-p' simplification above.
>> It also does when it detects it's in c-mode or c++-mode, since in my
>> testing syntax-ppss is sometimes broken there (tried in in
>> src/syntax.c)
>
> I'm not really surprised, sadly, but please report it as a bug (the fix
> is probably to make cc-mode use syntax-propertize-function, which is
> not a quick&easy fix since cc-mode currently sets the syntax-table in
> a very contorted way spread over various places and times).
OK.
> I don't understand why you'd want (not (or executing-kbd-macro
> noninteractive)) rather than any non-nil constant. Where does this (not
> (or executing-kbd-macro noninteractive)) come from?
I read it in `called-interactively-p''s docstring... I mean, if one
calls `newline-and-indent' from lisp it shouldn't call newline with
interactive=t right?
>> +(put 'electric-pair-post-self-insert-function 'priority 20)
>> +(put 'electric-layout-post-self-insert-function 'priority 40)
>> +(put 'electric-indent-post-self-insert-function 'priority 60)
>> +(put 'blink-paren-post-self-insert-function 'priority 100)
> These belong next to the corresponding functions.
Do they? The relative order is between them, spreading them throughout
the buffer makes it difficult to read, doesn't it? I would agree to
spread if the priority spec was something like.
(put 'electric-pair-post-self-insert-function
'priority '(before electric-layout-post-self-insert-function))
and so on.
> Also, if you know why the order is this way, please add comments
> explaining it (I do know for blink-paren-post-self-insert-function and
> electric-indent-post-self-insert-function, but not sure why
> electric-layout-post-self-insert-function should come after
> electric-pair-post-self-insert-function rather than the opposite).
In js-mode layout rules, pairing must come before layout, there's a test
for that. Otherwise it was just a thumb rule. But OK, I can add comments
for the known dependencies.
>> +(defcustom electric-pair-skip-whitespace t
> This docstring still needs to be improved, because it still doesn't
> explain what really happens. More specifically, which whitespace
> is skipped (before or after the skipped paren?).
OK.
> Other than those nitpicks, feel free to install those changes,
Hmmm, I don't know if I have write privs. I'll check. And I'll have to
check some bzr tutorials and README's.
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-16 14:21 ` João Távora
@ 2013-12-16 15:30 ` Stefan Monnier
2013-12-16 18:40 ` Stefan Monnier
[not found] ` <CALDnm52AoShN891-L9=Cbng98UtYPEntzO+n_XDMmEL+UV0r-A@mail.gmail.com>
0 siblings, 2 replies; 36+ messages in thread
From: Stefan Monnier @ 2013-12-16 15:30 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
> F electric-pair-inhibit-only-if-next-is-mismatched-at-point-1-in-c++-mode
> With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
Why should it become """"foo""bar"?
>> We could have the two functions check a new defcustom
>> electric-pair-preserve-balance and if nil fallback on the old default.
> Yes, that's what I meant, too. OK. So in the existing 2 variables custom
> menu there will be basically 4 options:
> - "always" or "never" (depending on whether it's pair or skip)
> - "balance, maybe" (the default we're discussing)
> - "balance, always"
> - "don't balance, be conservative"
Not sure what the "balance, always" refers to or why we need it, but
other than that, yes. The "balance" entry will be a "balance maybe".
>>> Explained above again. Using prog-mode-syntax-table allows me to get
>>> some quote balancing in comments and strings.
>> This is not really an example, let alone example*S*. Which quotes?
>> Why are they there? Is it only for quotes?
> OK. So Emacs -Q, M-x electric-pair-mode and then in the scratch buffer
> go to some place in the comment's text and type a double quote. You get
> it autopaired. If you type it again you get a skip. OK.
> Now go back to the first quote and type it again. You get a skip. In my
> opinion, not so nice. Delete the first quote, go back some words and
> type another quote. You'll get an unbalanced string inside the
> comment. Again not so nice.
> Now if you do (setq electric-pair-text-syntax-table
> prog-mode-syntax-table) and repeat the second paragraph, you'll get
> results that I personally think are nicer.
> BTW these are exactly the results that you mostly loose if you do the
> `electric-pair--looking-at-mismatched-string-p' simplification above.
>> I don't understand why you'd want (not (or executing-kbd-macro
>> noninteractive)) rather than any non-nil constant. Where does this (not
>> (or executing-kbd-macro noninteractive)) come from?
> I read it in `called-interactively-p''s docstring...
That's different: this `or' test is to distinguish between two different
notions of "interactively" (i.e. whether the user interactively
triggered this specific function, vs whether the function was called
using call-interactively).
> I mean, if one calls `newline-and-indent' from lisp it shouldn't call
> newline with interactive=t right?
Maybe, maybe not (it probably depends on the specific case), but since
it's very rarely called from Lisp, I think we'd better not worry about
it until someone reports an actual problem with it.
>>> +(put 'electric-pair-post-self-insert-function 'priority 20)
>>> +(put 'electric-layout-post-self-insert-function 'priority 40)
>>> +(put 'electric-indent-post-self-insert-function 'priority 60)
>>> +(put 'blink-paren-post-self-insert-function 'priority 100)
>> These belong next to the corresponding functions.
> Do they? The relative order is between them,
The whole point of using priorities is to make them not depend on each
other but on some external total order.
So blink-paren-post-self-insert-function gets 100 because it does
a sit-for so it has to be "at the very end".
electric-indent-post-self-insert-function gets 90 because it does some
generic post-processing which should end happen "towards the end,
without needing to be the absolute last".
For the other two, I don't know what to say, because I don't know why
they should come after "other functions" (i.e. why they're >0), nor why
pair is smaller than layout.
> (put 'electric-pair-post-self-insert-function
> 'priority '(before electric-layout-post-self-insert-function))
No, the whole point is to eliminate mutual dependencies. They should
know as little about each other as possible.
> In js-mode layout rules, pairing must come before layout, there's a test
> for that.
Do you know the underlying reason why the test fails if you do it the
other way around?
> Hmmm, I don't know if I have write privs.
Ah, indeed, you don't. OK, send me your latest code and I'll install
it, thank you.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-16 15:30 ` Stefan Monnier
@ 2013-12-16 18:40 ` Stefan Monnier
2013-12-16 19:06 ` João Távora
[not found] ` <CALDnm52AoShN891-L9=Cbng98UtYPEntzO+n_XDMmEL+UV0r-A@mail.gmail.com>
1 sibling, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-16 18:40 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
>> OK. So Emacs -Q, M-x electric-pair-mode and then in the scratch buffer
>> go to some place in the comment's text and type a double quote. You get
>> it autopaired. If you type it again you get a skip. OK.
>> Now go back to the first quote and type it again. You get a skip. In my
>> opinion, not so nice. Delete the first quote, go back some words and
>> type another quote. You'll get an unbalanced string inside the
>> comment. Again not so nice.
>> Now if you do (setq electric-pair-text-syntax-table
>> prog-mode-syntax-table) and repeat the second paragraph, you'll get
>> results that I personally think are nicer.
>> BTW these are exactly the results that you mostly loose if you do the
>> `electric-pair--looking-at-mismatched-string-p' simplification above.
IIUC the difference between prog-mode-syntax-table and
text-mode-syntax-table is the syntax of ".
I see what you mean, and it's probably true that the problems with
making " have string-syntax in text-mode don't apply here, so we're
better off using a syntax-table where " has string-syntax.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Fwd: [patch] make electric-pair-mode smarter/more useful
[not found] ` <CALDnm52AoShN891-L9=Cbng98UtYPEntzO+n_XDMmEL+UV0r-A@mail.gmail.com>
@ 2013-12-16 19:02 ` João Távora
0 siblings, 0 replies; 36+ messages in thread
From: João Távora @ 2013-12-16 19:02 UTC (permalink / raw
To: emacs-devel
On Mon, Dec 16, 2013 at 3:30 PM, Stefan Monnier
<monnier@iro.umontreal.ca> wrote:
>> With ""foo""bar", try input " at point 1. Should become """"foo""bar" and point at 2
> Why should it become """"foo""bar"?
Mind you, don't confuse the docstring's quotes with the quotes in the
fixture text. The fixture is
"foo" "bar
Which I (and font-lock) interpret as one perfectly terminated string
followed by an unterminated string.
So typing a quote at the very beginning should make it autopair IMO,
so that you get.
"""foo" "bar
Which is a perfectly terminated empty string, followed by the original fixture's
terminated and unterminated strings.
>> - "always" or "never" (depending on whether it's pair or skip)
>> - "balance, maybe" (the default we're discussing)
>> - "balance, always"
>> - "don't balance, be conservative"
> Not sure what the "balance, always" refers to or why we need it, but
> other than that, yes. The "balance" entry will be a "balance maybe".
Yes, it's silly probably.
"balance, always" would be "balance without depending on
electric-pair-preserve-balance.
"balance, maybe" would be "balance, but depending on
electric-pair-preserve-balance.".
You want me to strike "balance, always" and keep only "balance, maybe",
renamed as as "balance", right?
> That's different: this `or' test is to distinguish between two different
> notions of "interactively" (i.e. whether the user interactively
> triggered this specific function, vs whether the function was called
> using call-interactively).
Oh OK, I read the docstring in diagonal and didn't test much. But that
docstring might be a little confusing...
> Maybe, maybe not (it probably depends on the specific case), but since
> it's very rarely called from Lisp, I think we'd better not worry about
> it until someone reports an actual problem with it.
Then (newline 1 t) it is!
>>>> +(put 'electric-pair-post-self-insert-function 'priority 20)
>>>> +(put 'electric-layout-post-self-insert-function 'priority 40)
>>>> +(put 'electric-indent-post-self-insert-function 'priority 60)
>>>> +(put 'blink-paren-post-self-insert-function 'priority 100)
>>> These belong next to the corresponding functions.
>> Do they? The relative order is between them,
> The whole point of using priorities is to make them not depend on each
> other but on some external total order.
But in the end they do depend directly on each other. OK, you can
make it generic and say this one gets 20 because it inserts and deletes
characters, this other one gets 40 because it inserts extra newlines,
that 60 because something, and this one gets 100 because it does the
sit-for. But developing this theory for now seemed like overkill to me,
and without it would appear to the reader that ETOOMANYWORDS.
OK I'll do it.
>> In js-mode layout rules, pairing must come before layout, there's a test
>> for that.
> Do you know the underlying reason why the test fails if you do it the
> other way around?
I don't remember anymore, but its very easy to reproduce if you scratch
the sort function and write an additional test with the order of minor modes
reversed. Then I can check. I don't have emacs handy now.
Hoping this message isn't too garbled by the gmail interface,
João
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-16 18:40 ` Stefan Monnier
@ 2013-12-16 19:06 ` João Távora
2013-12-17 1:42 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-16 19:06 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel
On Mon, Dec 16, 2013 at 6:40 PM, Stefan Monnier
<monnier@iro.umontreal.ca> wrote:
> so we're better off using a syntax-table where " has
> string-syntax.
so (defcustom electric-pair-text-syntax-table prog-mode-syntax-table)
in the end?
João
(or was it defvar?)
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-16 3:34 ` Stefan Monnier
@ 2013-12-16 19:26 ` João Távora
2013-12-17 1:54 ` Stefan Monnier
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-16 19:26 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel, Dmitry Gutov
On Mon, Dec 16, 2013 at 3:34 AM, Stefan Monnier
<monnier@iro.umontreal.ca> wrote:
>> I personally would prefer that the current rules would *not* be the
>> default in js-mode, as they are now, or any other mode).
> I understand what you're saying as "I don't like electric-layout-mode".
> FWIW, I agree with you.
Actually, no. Really. I just didn't expect it to behave like that. It's unseen
in any other editor. That intention to type everything without ever reaching
for RET, if candid, is very interesting. It's just a little foreign to me, and
probably more people. I think the newline-between-pairs rule is less foreign.
>> Anyway, the default value should really be the newline-between-pairs
>> rule, which currently only kicks in when electric-pair-mode is
>> additionally enabled (btw, should it not?).
> Enabling electric-pair-mode is definitely not on the table for 24.4, no.
Yes, of course. You misunterstood me. I meant that the rule's predicate
checks for electric-pair-mode before returning the symbol 'after-stay. It could
not care about it, so we get newlines between pairs with just
electric-layout-mode.
>> This is also possible, but overkill IMO. I like the current triad of
>> electric modes.
> I don't think it's overkill at all. It should be a separate minor mode
> and default to enabled (but still conditional on electric-pair-mode).
> I.e. make it part of electric-pair-mode and not try to shoe-horn it into
> electric-layout-mode. I may have stated the opposite earlier, but
> I think it's pretty clear to me, now, that the purpose of
> electric-layout-mode is different.
Oh, pity, I much agreed with the earlier stefan :-(.
So a separate minor mode that on activation/deactivation adds/removes
the rule to/from the default value of electric-layout-rules, right?
But should the new minor mode automatically enable electric-pair-mode
and electric-layout-mode if it finds they're not enabled?
If it doesn't then a user who wants just the newlines will be surprised when
he enabled electric-newlines-mode and nothing happens...
OR did you mean a new minor mode completely independent from
electric-layout-mode, i.e., loose that 'after-stay nonsense that I proposed
earlier.
--
João Távora
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-16 19:06 ` João Távora
@ 2013-12-17 1:42 ` Stefan Monnier
0 siblings, 0 replies; 36+ messages in thread
From: Stefan Monnier @ 2013-12-17 1:42 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel
> so (defcustom electric-pair-text-syntax-table prog-mode-syntax-table)
> in the end?
Right.
> (or was it defvar?)
Either way.
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: Stefan Monnier @ 2013-12-17 1:54 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel, Dmitry Gutov
> So a separate minor mode that on activation/deactivation adds/removes
> the rule to/from the default value of electric-layout-rules, right?
No, I was thinking of a minor mode completely independent from
electric-layout-mode. Integrated in electric-pair-mode, instead.
> OR did you mean a new minor mode completely independent from
> electric-layout-mode, i.e., loose that 'after-stay nonsense that I proposed
> earlier.
Right (tho I wouldn't say it's non-sense, it might even be useful in
other circumstances).
Stefan
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-18 2:43 UTC (permalink / raw
To: Stefan Monnier; +Cc: Dmitry Gutov, emacs-devel
Hope I'm getting closer with this patch... Differences to the last one I
sent:
- New defcustom `electric-pair-open-newline-between-pairs', replaces the
earlier newline-between-pairs rule for `electric-layout-mode' (which
still keeps the `after-stay' option, unused though).
The new var is checked at the end of
`electric-pair-post-self-insert-function', so it's subordinate to
`electric-pair-mode' like you requested, and a good parallel to the
`electric-pair-delete-adjacent-pairs' defcustom in my opinion.
Found this easier than a separate minor mode, and probably just as
flexible.
- Priority specs for post insert hooks now placed after function
definition.
- New `electric-pair-preserve-balance' defcustom, used by new functions
`electric-pair-default-inhibit' and `electric-pair-default-skip',
which, as the name implies are the new defaults.
- New variable `electric-pair-whitespace-skip-chars' for controlling
exactly the kind of whitespace that is skipped when
`electric-pair-skip-whitespace' is on.
- Bugfix: new `electric-pair--skip-whitespace' helper checks if when
skipping whitespace we're not crossing a comment boundary (which leads
to silly chomping and unbalance).
- No longer use `keymap' arg to `define-minor-mode'.
- A couple more tests, now 617 in total
- Find myself doing
(setq-local electric-pair-pairs (cons '((?\` . ?\'))
electric-pair-pairs))
quite often in message-mode. Works with the portuguese input method
surprisingly.
So here it is,
João
diff --git a/lisp/electric.el b/lisp/electric.el
index 91b99b4..4a52cab 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -187,6 +187,17 @@ Returns nil when we can't find this char."
(eq (char-before) last-command-event)))))
pos)))
+(defun electric--sort-post-self-insertion-hook ()
+ "Ensure order of electric functions in `post-self-insertion-hook'.
+
+Hooks in this variable interact in non-trivial ways, so a
+relative order must be maintained within it."
+ (setq-default post-self-insert-hook
+ (sort (default-value 'post-self-insert-hook)
+ #'(lambda (fn1 fn2)
+ (< (or (get fn1 'priority) 0)
+ (or (get fn2 'priority) 0))))))
+
;;; Electric indentation.
;; Autoloading variables is generally undesirable, but major modes
@@ -267,6 +278,8 @@ mode set `electric-indent-inhibit', but this can be used as a workaround.")
(> pos (line-beginning-position)))
(indent-according-to-mode)))))
+(put 'electric-indent-post-self-insert-function 'priority 60)
+
(defun electric-indent-just-newline (arg)
"Insert just a newline, without any auto-indentation."
(interactive "*P")
@@ -295,20 +308,9 @@ insert a character from `electric-indent-chars'."
#'electric-indent-post-self-insert-function))
(when (eq (lookup-key global-map [?\C-j]) 'newline-and-indent)
(define-key global-map [?\C-j] 'electric-indent-just-newline))
- ;; post-self-insert-hooks interact in non-trivial ways.
- ;; It turns out that electric-indent-mode generally works better if run
- ;; late, but still before blink-paren.
(add-hook 'post-self-insert-hook
- #'electric-indent-post-self-insert-function
- 'append)
- ;; FIXME: Ugly!
- (let ((bp (memq #'blink-paren-post-self-insert-function
- (default-value 'post-self-insert-hook))))
- (when (memq #'electric-indent-post-self-insert-function bp)
- (setcar bp #'electric-indent-post-self-insert-function)
- (setcdr bp (cons #'blink-paren-post-self-insert-function
- (delq #'electric-indent-post-self-insert-function
- (cdr bp))))))))
+ #'electric-indent-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)))
;;;###autoload
(define-minor-mode electric-indent-local-mode
@@ -327,32 +329,163 @@ insert a character from `electric-indent-chars'."
(defcustom electric-pair-pairs
'((?\" . ?\"))
- "Alist of pairs that should be used regardless of major mode."
+ "Alist of pairs that should be used regardless of major mode.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the mode's syntax
+table.
+
+See also the variable `electric-pair-text-pairs'."
:version "24.1"
:type '(repeat (cons character character)))
-(defcustom electric-pair-skip-self t
+(defcustom electric-pair-text-pairs
+ '((?\" . ?\" ))
+ "Alist of pairs that should always be used in comments and strings.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the syntax table
+defined in `electric-pair-text-syntax-table'"
+ :version "24.4"
+ :type '(repeat (cons character character)))
+
+(defcustom electric-pair-skip-self #'electric-pair-skip-if-helps-balance
"If non-nil, skip char instead of inserting a second closing paren.
+
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."
+
+This can be convenient for people who find it easier to hit ) than C-f.
+
+Can also be a function of one argument (the closer char just
+inserted), in which case that function's return value is
+considered instead."
:version "24.1"
- :type 'boolean)
+ :type '(choice
+ (const :tag "Never skip" nil)
+ (const :tag "Help balance" electric-pair-default-skip-self)
+ (const :tag "Always skip" t)
+ 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-default-inhibit)
(const :tag "Always pair" ignore)
function))
-(defun electric-pair-default-inhibit (char)
+(defcustom electric-pair-preserve-balance t
+ "Non-nil if default pairing and skipping should help balance parentheses.
+
+The default values of `electric-pair-inhibit-predicate' and
+`electric-pair-skip-self' check this variable before delegating to other
+predicates reponsible for making decisions on whether to pair/skip some
+characters based on the actual state of the buffer's parenthesis and
+quotes."
+ :version "24.4"
+ :type 'boolean)
+
+(defcustom electric-pair-delete-adjacent-pairs t
+ "If non-nil, backspacing an open paren also deletes adjacent closer.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-open-newline-between-pairs t
+ "If non-nil, a newline between adjacent parentheses opens an extra one.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace t
+ "If non-nil skip whitespace when skipping over closing parens.
+
+The specific kind of whitespace skipped is given by the variable
+`electric-pair-skip-whitespace-chars'.
+
+The symbol `chomp' specifies that the skipped-over whitespace
+should be deleted.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes, jump over whitespace" t)
+ (const :tag "Yes, and delete whitespace" 'chomp)
+ (const :tag "No, no whitespace skipping" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace-chars (list ?\t ?\s ?\n)
+ "Whitespace characters considered by `electric-pair-skip-whitespace'."
+ :version "24.4"
+ :type '(choice (set (const :tag "Space" ?\s)
+ (const :tag "Tab" ?\t)
+ (const :tag "Newline" ?\n))
+ (list character)))
+
+(defun electric-pair--skip-whitespace ()
+ "Skip whitespace forward, not crossing comment or string boundaries."
+ (let ((saved (point))
+ (string-or-comment (nth 8 (syntax-ppss))))
+ (skip-chars-forward (apply #'string electric-pair-skip-whitespace-chars))
+ (unless (eq string-or-comment (nth 8 (syntax-ppss)))
+ (goto-char saved))))
+
+(defvar electric-pair-text-syntax-table text-mode-syntax-table
+ "Syntax table used when pairing inside comments and strings.
+
+`electric-pair-mode' considers this syntax table only when point
+in inside quotes or comments. If lookup fails here,
+`electric-pair-text-pairs' will be considered.")
+
+(defun electric-pair-backward-delete-char (n &optional killflag untabify)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char' or, if
+UNTABIFY is non-nil, `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (let* ((prev (char-before))
+ (next (char-after))
+ (syntax-info (electric-pair-syntax-info prev))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (when (and (if (functionp electric-pair-delete-adjacent-pairs)
+ (funcall electric-pair-delete-adjacent-pairs)
+ electric-pair-delete-adjacent-pairs)
+ next
+ (memq syntax '(?\( ?\" ?\$))
+ (eq pair next))
+ (delete-char 1 killflag))
+ (if untabify
+ (backward-delete-char-untabify n killflag)
+ (backward-delete-char n killflag))))
+
+(defun electric-pair-backward-delete-char-untabify (n &optional killflag)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (electric-pair-backward-delete-char n killflag t))
+
+(defun electric-pair-conservative-inhibit (char)
(or
;; I find it more often preferable not to pair when the
;; same char is next.
@@ -363,14 +496,40 @@ closer."
;; I also find it often preferable not to pair next to a word.
(eq (char-syntax (following-char)) ?w)))
-(defun electric-pair-syntax (command-event)
- (let ((x (assq command-event electric-pair-pairs)))
+(defun electric-pair-syntax-info (command-event)
+ "Calculate a list (SYNTAX PAIR UNCONDITIONAL STRING-OR-COMMENT-START).
+
+SYNTAX is COMMAND-EVENT's syntax character. PAIR is
+COMMAND-EVENT's pair. UNCONDITIONAL indicates the variables
+`electric-pair-pairs' or `electric-pair-text-pairs' were used to
+lookup syntax. STRING-OR-COMMENT-START indicates that point is
+inside a comment of string."
+ (let* ((pre-string-or-comment (nth 8 (save-excursion
+ (syntax-ppss (1- (point))))))
+ (post-string-or-comment (nth 8 (syntax-ppss (point))))
+ (string-or-comment (and post-string-or-comment
+ pre-string-or-comment))
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (table-syntax-and-pair (with-syntax-table table
+ (list (char-syntax command-event)
+ (or (matching-paren command-event)
+ command-event))))
+ (fallback (if string-or-comment
+ (append electric-pair-text-pairs
+ electric-pair-pairs)
+ electric-pair-pairs))
+ (direct (assq command-event fallback))
+ (reverse (rassq command-event fallback)))
(cond
- (x (if (eq (car x) (cdr x)) ?\" ?\())
- ((rassq command-event electric-pair-pairs) ?\))
- ((nth 8 (syntax-ppss))
- (with-syntax-table text-mode-syntax-table (char-syntax command-event)))
- (t (char-syntax command-event)))))
+ ((memq (car table-syntax-and-pair)
+ '(?\" ?\( ?\) ?\$))
+ (append table-syntax-and-pair (list nil string-or-comment)))
+ (direct (if (eq (car direct) (cdr direct))
+ (list ?\" command-event t string-or-comment)
+ (list ?\( (cdr direct) t string-or-comment)))
+ (reverse (list ?\) (car reverse) t string-or-comment)))))
(defun electric-pair--insert (char)
(let ((last-command-event char)
@@ -378,56 +537,286 @@ closer."
(electric-pair-mode nil))
(self-insert-command 1)))
+(defun electric-pair--syntax-ppss (&optional pos where)
+ "Like `syntax-ppss', but sometimes fallback to `parse-partial-sexp'.
+
+WHERE is list defaulting to '(string comment) and indicates
+when to fallback to `parse-partial-sexp'."
+ (let* ((pos (or pos (point)))
+ (where (or where '(string comment)))
+ (quick-ppss (syntax-ppss))
+ (quick-ppss-at-pos (syntax-ppss pos)))
+ (if (or (and (nth 3 quick-ppss) (memq 'string where))
+ (and (nth 4 quick-ppss) (memq 'comment where)))
+ (with-syntax-table electric-pair-text-syntax-table
+ (parse-partial-sexp (1+ (nth 8 quick-ppss)) pos))
+ ;; HACK! cc-mode apparently has some `syntax-ppss' bugs
+ (if (memq major-mode '(c-mode c++ mode))
+ (parse-partial-sexp (point-min) pos)
+ quick-ppss-at-pos))))
+
+;; Balancing means controlling pairing and skipping of parentheses so
+;; that, if possible, the buffer ends up at least as balanced as
+;; before, if not more. The algorithm is slightly complex because some
+;; situations like "()))" need pairing to occur at the end but not at
+;; the beginning. Balancing should also happen independently for
+;; different types of parentheses, so that having your {}'s unbalanced
+;; doesn't keep `electric-pair-mode' from balancing your ()'s and your
+;; []'s.
+(defun electric-pair--balance-info (direction string-or-comment)
+ "Examine lists forward or backward according to DIRECTIONS's sign.
+
+STRING-OR-COMMENT is info suitable for running `parse-partial-sexp'.
+
+Return a cons of two descritions (MATCHED-P . PAIR) for the
+innermost and outermost lists that enclose point. The outermost
+list enclosing point is either the first top-level or first
+mismatched list found by uplisting.
+
+If the outermost list is matched, don't rely on its PAIR. If
+point is not enclosed by any lists, return ((T) (T))."
+ (let* (innermost
+ outermost
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (at-top-level-or-equivalent-fn
+ ;; called when `scan-sexps' ran perfectly, when when it
+ ;; found a parenthesis pointing in the direction of
+ ;; travel. Also when travel started inside a comment and
+ ;; exited it
+ #'(lambda ()
+ (setq outermost (list t))
+ (unless innermost
+ (setq innermost (list t)))))
+ (ended-prematurely-fn
+ ;; called when `scan-sexps' crashed against a parenthesis
+ ;; pointing opposite the direction of travel. After
+ ;; traversing that character, the idea is to travel one sexp
+ ;; in the opposite direction looking for a matching
+ ;; delimiter.
+ #'(lambda ()
+ (let* ((pos (point))
+ (matched
+ (save-excursion
+ (cond ((< direction 0)
+ (condition-case nil
+ (eq (char-after pos)
+ (with-syntax-table table
+ (matching-paren
+ (char-before
+ (scan-sexps (point) 1)))))
+ (scan-error nil)))
+ (t
+ ;; In this case, no need to use
+ ;; `scan-sexps', we can use some
+ ;; `electric-pair--syntax-ppss' in this
+ ;; case (which uses the quicker
+ ;; `syntax-ppss' in some cases)
+ (let* ((ppss (electric-pair--syntax-ppss
+ (1- (point))))
+ (start (car (last (nth 9 ppss))))
+ (opener (char-after start)))
+ (and start
+ (eq (char-before pos)
+ (or (with-syntax-table table
+ (matching-paren opener))
+ opener))))))))
+ (actual-pair (if (> direction 0)
+ (char-before (point))
+ (char-after (point)))))
+ (unless innermost
+ (setq innermost (cons matched actual-pair)))
+ (unless matched
+ (setq outermost (cons matched actual-pair)))))))
+ (save-excursion
+ (while (not outermost)
+ (condition-case err
+ (with-syntax-table table
+ (scan-sexps (point) (if (> direction 0)
+ (point-max)
+ (- (point-max))))
+ (funcall at-top-level-or-equivalent-fn))
+ (scan-error
+ (cond ((or
+ ;; some error happened and it is not of the "ended
+ ;; prematurely" kind"...
+ (not (string-match "ends prematurely" (nth 1 err)))
+ ;; ... or we were in a comment and just came out of
+ ;; it.
+ (and string-or-comment
+ (not (nth 8 (syntax-ppss)))))
+ (funcall at-top-level-or-equivalent-fn))
+ (t
+ ;; exit the sexp
+ (goto-char (nth 3 err))
+ (funcall ended-prematurely-fn)))))))
+ (cons innermost outermost)))
+
+(defun electric-pair--looking-at-unterminated-string-p (char)
+ "Say if following string starts with CHAR and is unterminated."
+ ;; FIXME: ugly/naive
+ (save-excursion
+ (skip-chars-forward (format "^%c" char))
+ (while (not (zerop (% (save-excursion (skip-syntax-backward "\\")) 2)))
+ (unless (eobp)
+ (forward-char 1)
+ (skip-chars-forward (format "^%c" char))))
+ (and (not (eobp))
+ (condition-case err
+ (progn (forward-sexp) nil)
+ (scan-error t)))))
+
+(defun electric-pair--inside-string-p (char)
+ "Say if point is inside a string started by CHAR.
+
+A comments text is parsed with `electric-pair-text-syntax-table'.
+Also consider strings within comments, but not strings within
+strings."
+ ;; FIXME: could also consider strings within strings by examining
+ ;; delimiters.
+ (let* ((ppss (electric-pair--syntax-ppss (point) '(comment))))
+ (memq (nth 3 ppss) (list t char))))
+
+(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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq ?\( syntax)
+ (let* ((pair-data
+ (electric-pair--balance-info 1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (cond ((car outermost)
+ nil)
+ (t
+ (eq (cdr outermost) pair)))))
+ ((eq syntax ?\")
+ (electric-pair--looking-at-unterminated-string-p 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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq syntax ?\))
+ (let* ((pair-data
+ (electric-pair--balance-info
+ -1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (and
+ (cond ((car outermost)
+ (car innermost))
+ ((car innermost)
+ (not (eq (cdr outermost) pair)))))))
+ ((eq syntax ?\")
+ (electric-pair--inside-string-p char))))
+ (insert-char char)))))
+
+(defun electric-pair-default-skip-self (char)
+ (if electric-pair-preserve-balance
+ (electric-pair-skip-if-helps-balance char)
+ t))
+
+(defun electric-pair-default-inhibit (char)
+ (if electric-pair-preserve-balance
+ (electric-pair-inhibit-if-helps-balance char)
+ (electric-pair-conservative-inhibit 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)))
- (closer (if (eq syntax ?\()
- (cdr (or (assq last-command-event electric-pair-pairs)
- (aref (syntax-table) last-command-event)))
- last-command-event)))
- (cond
- ((null pos) nil)
- ;; Wrap a pair around the active region.
- ((and (memq syntax '(?\( ?\" ?\$)) (use-region-p))
- ;; FIXME: To do this right, we'd need a post-self-insert-function
- ;; so we could add-function around it and insert the closer after
- ;; all the rest of the hook has run.
- (if (>= (mark) (point))
- (goto-char (mark))
- ;; We already inserted the open-paren but at the end of the
- ;; region, so we have to remove it and start over.
- (delete-region (1- pos) (point))
- (save-excursion
- (goto-char (mark))
- (electric-pair--insert last-command-event)))
- ;; Since we're right after the closer now, we could tell the rest of
- ;; post-self-insert-hook that we inserted `closer', but then we'd get
- ;; blink-paren to kick in, which is annoying.
- ;;(setq last-command-event closer)
- (insert closer))
- ;; Backslash-escaped: no pairing, no skipping.
- ((save-excursion
- (goto-char (1- pos))
- (not (zerop (% (skip-syntax-backward "\\") 2))))
- nil)
- ;; Skip self.
- ((and (memq syntax '(?\) ?\" ?\$))
- 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
- ;; undo-log and in the intermediate state which might be visible to other
- ;; post-self-insert-hook. We'll just have to live with it for now.
- (delete-char 1))
- ;; Insert matching pair.
- ((not (or (not (memq syntax `(?\( ?\" ?\$)))
- overwrite-mode
- (funcall electric-pair-inhibit-predicate last-command-event)))
- (save-excursion (electric-pair--insert closer))))))
+ (skip-whitespace-info))
+ (pcase (electric-pair-syntax-info last-command-event)
+ (`(,syntax ,pair ,unconditional ,_)
+ (cond
+ ((null pos) nil)
+ ;; Wrap a pair around the active region.
+ ;;
+ ((and (memq syntax '(?\( ?\) ?\" ?\$)) (use-region-p))
+ ;; FIXME: To do this right, we'd need a post-self-insert-function
+ ;; so we could add-function around it and insert the closer after
+ ;; all the rest of the hook has run.
+ (if (or (eq syntax ?\")
+ (and (eq syntax ?\))
+ (>= (point) (mark)))
+ (and (not (eq syntax ?\)))
+ (>= (mark) (point))))
+ (save-excursion
+ (goto-char (mark))
+ (electric-pair--insert pair))
+ (delete-region pos (1- pos))
+ (electric-pair--insert pair)
+ (goto-char (mark))
+ (electric-pair--insert last-command-event)))
+ ;; Backslash-escaped: no pairing, no skipping.
+ ((save-excursion
+ (goto-char (1- pos))
+ (not (zerop (% (skip-syntax-backward "\\") 2))))
+ nil)
+ ;; Skip self.
+ ((and (memq syntax '(?\) ?\" ?\$))
+ (and (or unconditional
+ (if (functionp electric-pair-skip-self)
+ (funcall electric-pair-skip-self last-command-event)
+ electric-pair-skip-self))
+ (save-excursion
+ (when (setq skip-whitespace-info
+ (if (functionp electric-pair-skip-whitespace)
+ (funcall electric-pair-skip-whitespace)
+ electric-pair-skip-whitespace))
+ (electric-pair--skip-whitespace))
+ (eq (char-after) 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 undo-log and in the intermediate state which might
+ ;; be visible to other post-self-insert-hook. We'll just have to
+ ;; live with it for now.
+ (when skip-whitespace-info
+ (electric-pair--skip-whitespace))
+ (delete-region (1- pos) (if (eq skip-whitespace-info 'chomp)
+ (point)
+ pos))
+ (forward-char))
+ ;; Insert matching pair.
+ ((and (memq syntax `(?\( ?\" ?\$))
+ (not overwrite-mode)
+ (or unconditional
+ (not (funcall electric-pair-inhibit-predicate
+ last-command-event))))
+ (save-excursion (electric-pair--insert pair)))))
+ (t
+ (when (and (if (functionp electric-pair-open-newline-between-pairs)
+ (funcall electric-pair-open-newline-between-pairs)
+ electric-pair-open-newline-between-pairs)
+ (eq last-command-event ?\n)
+ (not (eobp))
+ (eq (save-excursion
+ (skip-chars-backward "\t\s")
+ (char-before (1- (point))))
+ (matching-paren (char-after))))
+ (save-excursion (newline 1 t)))))))
+
+(put 'electric-pair-post-self-insert-function 'priority 20)
(defun electric-pair-will-use-region ()
(and (use-region-p)
- (memq (electric-pair-syntax last-command-event) '(?\( ?\" ?\$))))
+ (memq (car (electric-pair-syntax-info last-command-event))
+ '(?\( ?\) ?\" ?\$))))
;;;###autoload
(define-minor-mode electric-pair-mode
@@ -446,21 +835,38 @@ See options `electric-pair-pairs' and `electric-pair-skip-self'."
(progn
(add-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)
(add-hook 'self-insert-uses-region-functions
#'electric-pair-will-use-region))
(remove-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
(remove-hook 'self-insert-uses-region-functions
- #'electric-pair-will-use-region)))
+ #'electric-pair-will-use-region)))
+
+(defvar electric-pair-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map [remap backward-delete-char-untabify]
+ 'electric-pair-backward-delete-char-untabify)
+ (define-key map [remap backward-delete-char]
+ 'electric-pair-backward-delete-char)
+ (define-key map [remap delete-backward-char]
+ 'electric-pair-backward-delete-char)
+ map)
+ "Keymap used by `electric-pair-mode'.")
;;; Electric newlines after/before/around some chars.
-(defvar electric-layout-rules '()
+(defvar electric-layout-rules nil
"List of rules saying where to automatically insert newlines.
-Each rule has the form (CHAR . WHERE) where CHAR is the char
-that was just inserted and WHERE specifies where to insert newlines
-and can be: nil, `before', `after', `around', or a function of no
-arguments that returns one of those symbols.")
+
+Each rule has the form (CHAR . WHERE) where CHAR is the char that
+was just inserted and WHERE specifies where to insert newlines
+and can be: nil, `before', `after', `around', `after-stay', or a
+function of no arguments that returns one of those symbols.
+
+The symbols specify where in relation to CHAR the newline
+character(s) should be inserted. `after-stay' means insert a
+newline after CHAR but stay in the same place.")
(defun electric-layout-post-self-insert-function ()
(let* ((rule (cdr (assq last-command-event electric-layout-rules)))
@@ -469,23 +875,32 @@ arguments that returns one of those symbols.")
(setq pos (electric--after-char-pos))
;; Not in a string or comment.
(not (nth 8 (save-excursion (syntax-ppss pos)))))
- (let ((end (copy-marker (point) t)))
+ (let ((end (copy-marker (point)))
+ (sym (if (functionp rule) (funcall rule) rule)))
+ (set-marker-insertion-type end (not (eq sym 'after-stay)))
(goto-char pos)
- (pcase (if (functionp rule) (funcall rule) rule)
+ (case sym
;; FIXME: we used `newline' down here which called
;; self-insert-command and ran post-self-insert-hook recursively.
;; It happened to make electric-indent-mode work automatically with
;; electric-layout-mode (at the cost of re-indenting lines
;; multiple times), but I'm not sure it's what we want.
+ ;;
+ ;; FIXME: check eolp before inserting \n?
(`before (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (`after (insert "\n")) ; FIXME: check eolp before inserting \n?
+ (unless (bolp) (insert "\n")))
+ (`after (insert "\n"))
+ (`after-stay (save-excursion
+ (let ((electric-layout-rules nil))
+ (newline 1 t))))
(`around (save-excursion
- (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (insert "\n"))) ; FIXME: check eolp before inserting \n?
+ (goto-char (1- pos)) (skip-chars-backward " \t")
+ (unless (bolp) (insert "\n")))
+ (insert "\n"))) ; FIXME: check eolp before inserting \n?
(goto-char end)))))
+(put 'electric-layout-post-self-insert-function 'priority 40)
+
;;;###autoload
(define-minor-mode electric-layout-mode
"Automatically insert newlines around some chars.
@@ -494,11 +909,13 @@ positive, and disable it otherwise. If called from Lisp, enable
the mode if ARG is omitted or nil.
The variable `electric-layout-rules' says when and how to insert newlines."
:global t :group 'electricity
- (if electric-layout-mode
- (add-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)
- (remove-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)))
+ (cond (electric-layout-mode
+ (add-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook))
+ (t
+ (remove-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function))))
(provide 'electric)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index f4e9b31..5194e73 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -472,7 +472,12 @@ font-lock keywords will not be case sensitive."
(font-lock-mark-block-function . mark-defun)
(font-lock-syntactic-face-function
. lisp-font-lock-syntactic-face-function)))
- (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
+ (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
+ ;; electric
+ (when elisp
+ (setq-local electric-pair-text-pairs
+ (cons '(?\` . ?\') electric-pair-text-pairs)))
+ (setq-local electric-pair-skip-whitespace 'chomp))
(defun lisp-outline-level ()
"Lisp mode `outline-level' function."
diff --git a/lisp/simple.el b/lisp/simple.el
index 260c170..207f3d9 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -607,7 +607,7 @@ In some text modes, where TAB inserts a tab, this command indents to the
column specified by the function `current-left-margin'."
(interactive "*")
(delete-horizontal-space t)
- (newline)
+ (newline 1 (not (or executing-kbd-macro noninteractive)))
(indent-according-to-mode))
(defun reindent-then-newline-and-indent ()
@@ -6410,10 +6410,14 @@ More precisely, a char with closeparen syntax is self-inserted.")
(point))))))
(funcall blink-paren-function)))
+(put 'blink-paren-post-self-insert-function 'priority 100)
+
(add-hook 'post-self-insert-hook #'blink-paren-post-self-insert-function
;; Most likely, this hook is nil, so this arg doesn't matter,
;; but I use it as a reminder that this function usually
- ;; likes to be run after others since it does `sit-for'.
+ ;; likes to be run after others since it does
+ ;; `sit-for'. That's also the reason it get a `priority' prop
+ ;; of 100.
'append)
\f
;; This executes C-g typed while Emacs is waiting for a command.
diff --git a/test/automated/electric-tests.el b/test/automated/electric-tests.el
new file mode 100644
index 0000000..f4abdcd
--- /dev/null
+++ b/test/automated/electric-tests.el
@@ -0,0 +1,509 @@
+;;; electric-tests.el --- tests for electric.el -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2013 João Távora
+
+;; Author: João Távora <joaotavora@gmail.com>
+;; Keywords:
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'ert-x)
+(require 'electric)
+(require 'cl-lib)
+
+(defun call-with-saved-electric-modes (fn)
+ (let ((saved-electric (if electric-pair-mode 1 -1))
+ (saved-layout (if electric-layout-mode 1 -1))
+ (saved-indent (if electric-indent-mode 1 -1)))
+ (electric-pair-mode -1)
+ (electric-layout-mode -1)
+ (electric-indent-mode -1)
+ (unwind-protect
+ (funcall fn)
+ (electric-pair-mode saved-electric)
+ (electric-indent-mode saved-indent)
+ (electric-layout-mode saved-layout))))
+
+(defmacro save-electric-modes (&rest body)
+ (declare (indent defun) (debug t))
+ `(call-with-saved-electric-modes #'(lambda () ,@body)))
+
+(defun electric-pair-test-for (fixture where char expected-string
+ expected-point mode bindings fixture-fn)
+ (with-temp-buffer
+ (funcall mode)
+ (insert fixture)
+ (save-electric-modes
+ (let ((last-command-event char))
+ (goto-char where)
+ (funcall fixture-fn)
+ (progv
+ (mapcar #'car bindings)
+ (mapcar #'cdr bindings)
+ (self-insert-command 1))))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected-string))
+ (should (equal (point)
+ expected-point))))
+
+(eval-when-compile
+ (defun electric-pair-define-test-form (name fixture
+ char
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn)
+ (let* ((expected-string-and-point
+ (if skip-pair-string
+ (with-temp-buffer
+ (progv
+ ;; FIXME: avoid `eval'
+ (mapcar #'car (eval bindings))
+ (mapcar #'cdr (eval bindings))
+ (funcall mode)
+ (insert fixture)
+ (goto-char (1+ pos))
+ (insert char)
+ (cond ((eq (aref skip-pair-string pos)
+ ?p)
+ (insert (cadr (electric-pair-syntax-info char)))
+ (backward-char 1))
+ ((eq (aref skip-pair-string pos)
+ ?s)
+ (delete-char -1)
+ (forward-char 1)))
+ (list
+ (buffer-substring-no-properties (point-min) (point-max))
+ (point))))
+ (list expected-string expected-point)))
+ (expected-string (car expected-string-and-point))
+ (expected-point (cadr expected-string-and-point))
+ (fixture (format "%s%s%s" prefix fixture suffix))
+ (expected-string (format "%s%s%s" prefix expected-string suffix))
+ (expected-point (+ (length prefix) expected-point))
+ (pos (+ (length prefix) pos)))
+ `(ert-deftest ,(intern (format "electric-pair-%s-at-point-%s-in-%s%s"
+ name
+ (1+ pos)
+ mode
+ extra-desc))
+ ()
+ ,(format "With \"%s\", try input %c at point %d. \
+Should %s \"%s\" and point at %d"
+ fixture
+ char
+ (1+ pos)
+ (if (string= fixture expected-string)
+ "stay"
+ "become")
+ (replace-regexp-in-string "\n" "\\\\n" expected-string)
+ expected-point)
+ (electric-pair-test-for ,fixture
+ ,(1+ pos)
+ ,char
+ ,expected-string
+ ,expected-point
+ ',mode
+ ,bindings
+ ,fixture-fn)))))
+
+(cl-defmacro define-electric-pair-test
+ (name fixture
+ input
+ &key
+ skip-pair-string
+ expected-string
+ expected-point
+ bindings
+ (modes '(quote (emacs-lisp-mode ruby-mode c++-mode)))
+ (test-in-comments t)
+ (test-in-strings t)
+ (test-in-code t)
+ (fixture-fn #'(lambda ()
+ (electric-pair-mode 1))))
+ `(progn
+ ,@(cl-loop
+ for mode in (eval modes) ;FIXME: avoid `eval'
+ append
+ (cl-loop
+ for (prefix suffix extra-desc) in
+ (append (if test-in-comments
+ `((,(with-temp-buffer
+ (funcall mode)
+ (insert "z")
+ (comment-region (point-min) (point-max))
+ (buffer-substring-no-properties (point-min)
+ (1- (point-max))))
+ ""
+ "-in-comments")))
+ (if test-in-strings
+ `(("\"" "\"" "-in-strings")))
+ (if test-in-code
+ `(("" "" ""))))
+ append
+ (cl-loop
+ for char across input
+ for pos from 0
+ unless (eq char ?-)
+ collect (electric-pair-define-test-form
+ name
+ fixture
+ (aref input pos)
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn))))))
+\f
+;;; Basic pairings and skippings
+;;;
+(define-electric-pair-test balanced-situation
+ " (()) " "(((((((" :skip-pair-string "ppppppp"
+ :modes '(ruby-mode))
+
+(define-electric-pair-test too-many-openings
+ " ((()) " "(((((((" :skip-pair-string "ppppppp")
+
+(define-electric-pair-test too-many-closings
+ " (())) " "(((((((" :skip-pair-string "------p")
+
+(define-electric-pair-test too-many-closings-2
+ "() ) " "---(---" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-3
+ ")() " "(------" :skip-pair-string "-------")
+
+(define-electric-pair-test balanced-autoskipping
+ " (()) " "---))--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test too-many-openings-autoskipping
+ " ((()) " "----))-" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-autoskipping
+ " (())) " "---)))-" :skip-pair-string "---sss-")
+
+\f
+;;; Mixed parens
+;;;
+(define-electric-pair-test mixed-paren-1
+ " ()] " "-(-(---" :skip-pair-string "-p-p---")
+
+(define-electric-pair-test mixed-paren-2
+ " [() " "-(-()--" :skip-pair-string "-p-ps--")
+
+(define-electric-pair-test mixed-paren-3
+ " (]) " "-(-()--" :skip-pair-string "---ps--")
+
+(define-electric-pair-test mixed-paren-4
+ " ()] " "---)]--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test mixed-paren-5
+ " [() " "----(--" :skip-pair-string "----p--")
+
+(define-electric-pair-test find-matching-different-paren-type
+ " ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test find-matching-different-paren-type-inside-list
+ "( ()]) " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test ignore-different-unmatching-paren-type
+ "( ()]) " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test autopair-keep-least-amount-of-mixed-unbalance
+ "( ()] " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test dont-autopair-to-resolve-mixed-unbalance
+ "( ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test autopair-so-as-not-to-worsen-unbalance-situation
+ "( (]) " "-[-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test skip-over-partially-balanced
+ " [([]) " "-----)---" :skip-pair-string "-----s---")
+
+(define-electric-pair-test only-skip-over-at-least-partially-balanced-stuff
+ " [([()) " "-----))--" :skip-pair-string "-----s---")
+
+
+
+\f
+;;; Quotes
+;;;
+(define-electric-pair-test pair-some-quotes-skip-others
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test skip-single-quotes-in-ruby-mode
+ " '' " "--'-" :skip-pair-string "--s-"
+ :modes '(ruby-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone
+ " \"' " "-\"'-" :skip-pair-string "----"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-2
+ " \"\\\"' " "-\"--'-" :skip-pair-string "------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-3
+ " foo\\''" "'------" :skip-pair-string "-------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test inhibit-only-if-next-is-mismatched
+ "\"foo\"\"bar" "\""
+ :expected-string "\"\"\"foo\"\"bar"
+ :expected-point 2
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+\f
+;;; More quotes, but now don't bind `electric-pair-text-syntax-table'
+;;; to `prog-mode-syntax-table'. Use the defaults for
+;;; `electric-pair-pairs' and `electric-pair-text-pairs'.
+;;;
+(define-electric-pair-test pairing-skipping-quotes-in-code
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :test-in-comments nil)
+
+(define-electric-pair-test skipping-quotes-in-comments
+ " \"\" " "--\"-----" :skip-pair-string "--s------"
+ :test-in-strings nil)
+
+\f
+;;; Skipping over whitespace
+;;;
+(define-electric-pair-test whitespace-jumping
+ " ( ) " "--))))---" :expected-string " ( ) " :expected-point 8
+ :bindings '((electric-pair-skip-whitespace . t)))
+
+(define-electric-pair-test whitespace-chomping
+ " ( ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+(define-electric-pair-test whitespace-chomping-2
+ " ( \n\t\t\n ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp))
+ :test-in-comments nil)
+
+(define-electric-pair-test whitespace-chomping-dont-cross-comments
+ " ( \n\t\t\n ) " "--)------" :expected-string " () \n\t\t\n ) "
+ :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp))
+ :test-in-strings nil
+ :test-in-code nil
+ :test-in-comments t)
+
+\f
+;;; Pairing arbitrary characters
+;;;
+(define-electric-pair-test angle-brackets-everywhere
+ "<>" "<>" :skip-pair-string "ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(define-electric-pair-test angle-brackets-everywhere-2
+ "(<>" "-<>" :skip-pair-string "-ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(defvar electric-pair-test-angle-brackets-table
+ (let ((table (make-syntax-table prog-mode-syntax-table)))
+ (modify-syntax-entry ?\< "(>" table)
+ (modify-syntax-entry ?\> ")<`" table)
+ table))
+
+(define-electric-pair-test angle-brackets-pair
+ "<>" "<" :expected-string "<><>" :expected-point 2
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test angle-brackets-skip
+ "<>" "->" :expected-string "<>" :expected-point 3
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test pair-backtick-and-quote-in-comments
+ ";; " "---`" :expected-string ";; `'" :expected-point 5
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-comments
+ ";; `foo'" "-------'" :expected-string ";; `foo'" :expected-point 9
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test pair-backtick-and-quote-in-strings
+ "\"\"" "-`" :expected-string "\"`'\"" :expected-point 3
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings
+ "\"`'\"" "--'" :expected-string "\"`'\"" :expected-point 4
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings-2
+ " \"`'\"" "----'" :expected-string " \"`'\"" :expected-point 6
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+\f
+;;; `js-mode' has `electric-layout-rules' for '{ and '}
+;;;
+(define-electric-pair-test js-mode-braces
+ "" "{" :expected-string "{}" :expected-point 2
+ :modes '(js-mode)
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout
+ "" "{" :expected-string "{\n\n}" :expected-point 3
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-layout-mode 1)
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout-and-indent
+ "" "{" :expected-string "{\n \n}" :expected-point 7
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (electric-layout-mode 1)))
+
+\f
+;;; Backspacing
+;;; TODO: better tests
+;;;
+(ert-deftest electric-pair-backspace-1 ()
+ (save-electric-modes
+ (with-temp-buffer
+ (insert "()")
+ (goto-char 2)
+ (electric-pair-backward-delete-char 1)
+ (should (equal "" (buffer-string))))))
+
+\f
+;;; Electric newlines between pairs
+;;; TODO: better tests
+(ert-deftest electric-pair-open-extra-newline ()
+ (save-electric-modes
+ (with-temp-buffer
+ (c-mode)
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (insert "int main {}")
+ (backward-char 1)
+ (let ((c-basic-offset 4))
+ (newline 1 t)
+ (should (equal "int main {\n \n}"
+ (buffer-string)))
+ (should (equal (point) (- (point-max) 2)))))))
+
+
+\f
+;;; Autowrapping
+;;;
+(define-electric-pair-test autowrapping-1
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-2
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-3
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-4
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-5
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-6
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(provide 'electric-pair-tests)
+;;; electric-pair-tests.el ends here
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-18 15:32 UTC (permalink / raw
To: Stefan Monnier; +Cc: emacs-devel, Dmitry Gutov
joaotavora@gmail.com (João Távora) writes:
> Hope I'm getting closer with this patch... Differences to the last one I
> sent:
Some minor tweaks (but sending the whole thing once again).
- Added a changelog entry
- should be `pcase' and not `case' in
`electric-layout-post-self-insert-function'
- electric-tests.el sets lexical-binding=nil otherwise tests all fail if
file is just loaded, not compiled.
- Replace progv with cl-progv in electric-tests.el
- minor whitespace fixes.
diff --git a/lisp/ChangeLog b/lisp/ChangeLog
index 320a82b..d62dc2f 100644
--- a/lisp/ChangeLog
+++ b/lisp/ChangeLog
@@ -1,3 +1,24 @@
+2013-12-18 João Távora <joaotavora@gmail.com>
+
+ * electric.el (electric-pair-mode): More flexible engine for skip-
+ and inhibit predicates, new options for pairing-related
+ functionality.
+ (electric-pair-preserve-balance): Pair/skip parentheses and quotes
+ if that keeps or improves their balance in buffers.
+ (electric-pair-delete-adjacent-pairs): Delete the pair when
+ backspacing over adjacent matched delimiters.
+ (electric-pair-open-extra-newline): Open extra newline when
+ inserting newlines between adjacent matched delimiters.
+ (electric--sort-post-self-insertion-hook): Sort
+ post-self-insert-hook according to priority values when
+ minor-modes are activated.
+ * simple.el (newline-and-indent): Call newline with interactive
+ set to t.
+ (blink-paren-post-self-insert-function): Set priority to 100.
+ * emacs-lisp/lisp-mode.el (lisp-mode-variables): Use
+ electric-pair-text-pairs to pair backtick-and-quote in strings and
+ comments. Set electric-pair-skip-whitespace to 'chomp.
+
2013-12-18 Tassilo Horn <tsdh@gnu.org>
* textmodes/reftex-vars.el (reftex-label-alist-builtin): Reference
diff --git a/lisp/electric.el b/lisp/electric.el
index 91b99b4..4ec0b96 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -187,6 +187,17 @@ Returns nil when we can't find this char."
(eq (char-before) last-command-event)))))
pos)))
+(defun electric--sort-post-self-insertion-hook ()
+ "Ensure order of electric functions in `post-self-insertion-hook'.
+
+Hooks in this variable interact in non-trivial ways, so a
+relative order must be maintained within it."
+ (setq-default post-self-insert-hook
+ (sort (default-value 'post-self-insert-hook)
+ #'(lambda (fn1 fn2)
+ (< (or (get fn1 'priority) 0)
+ (or (get fn2 'priority) 0))))))
+
;;; Electric indentation.
;; Autoloading variables is generally undesirable, but major modes
@@ -267,6 +278,8 @@ mode set `electric-indent-inhibit', but this can be used as a workaround.")
(> pos (line-beginning-position)))
(indent-according-to-mode)))))
+(put 'electric-indent-post-self-insert-function 'priority 60)
+
(defun electric-indent-just-newline (arg)
"Insert just a newline, without any auto-indentation."
(interactive "*P")
@@ -295,20 +308,9 @@ insert a character from `electric-indent-chars'."
#'electric-indent-post-self-insert-function))
(when (eq (lookup-key global-map [?\C-j]) 'newline-and-indent)
(define-key global-map [?\C-j] 'electric-indent-just-newline))
- ;; post-self-insert-hooks interact in non-trivial ways.
- ;; It turns out that electric-indent-mode generally works better if run
- ;; late, but still before blink-paren.
(add-hook 'post-self-insert-hook
- #'electric-indent-post-self-insert-function
- 'append)
- ;; FIXME: Ugly!
- (let ((bp (memq #'blink-paren-post-self-insert-function
- (default-value 'post-self-insert-hook))))
- (when (memq #'electric-indent-post-self-insert-function bp)
- (setcar bp #'electric-indent-post-self-insert-function)
- (setcdr bp (cons #'blink-paren-post-self-insert-function
- (delq #'electric-indent-post-self-insert-function
- (cdr bp))))))))
+ #'electric-indent-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)))
;;;###autoload
(define-minor-mode electric-indent-local-mode
@@ -327,32 +329,163 @@ insert a character from `electric-indent-chars'."
(defcustom electric-pair-pairs
'((?\" . ?\"))
- "Alist of pairs that should be used regardless of major mode."
+ "Alist of pairs that should be used regardless of major mode.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the mode's syntax
+table.
+
+See also the variable `electric-pair-text-pairs'."
:version "24.1"
:type '(repeat (cons character character)))
-(defcustom electric-pair-skip-self t
+(defcustom electric-pair-text-pairs
+ '((?\" . ?\" ))
+ "Alist of pairs that should always be used in comments and strings.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the syntax table
+defined in `electric-pair-text-syntax-table'"
+ :version "24.4"
+ :type '(repeat (cons character character)))
+
+(defcustom electric-pair-skip-self #'electric-pair-skip-if-helps-balance
"If non-nil, skip char instead of inserting a second closing paren.
+
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."
+
+This can be convenient for people who find it easier to hit ) than C-f.
+
+Can also be a function of one argument (the closer char just
+inserted), in which case that function's return value is
+considered instead."
:version "24.1"
- :type 'boolean)
+ :type '(choice
+ (const :tag "Never skip" nil)
+ (const :tag "Help balance" electric-pair-default-skip-self)
+ (const :tag "Always skip" t)
+ 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-default-inhibit)
(const :tag "Always pair" ignore)
function))
-(defun electric-pair-default-inhibit (char)
+(defcustom electric-pair-preserve-balance t
+ "Non-nil if default pairing and skipping should help balance parentheses.
+
+The default values of `electric-pair-inhibit-predicate' and
+`electric-pair-skip-self' check this variable before delegating to other
+predicates reponsible for making decisions on whether to pair/skip some
+characters based on the actual state of the buffer's parenthesis and
+quotes."
+ :version "24.4"
+ :type 'boolean)
+
+(defcustom electric-pair-delete-adjacent-pairs t
+ "If non-nil, backspacing an open paren also deletes adjacent closer.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-open-newline-between-pairs t
+ "If non-nil, a newline between adjacent parentheses opens an extra one.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace t
+ "If non-nil skip whitespace when skipping over closing parens.
+
+The specific kind of whitespace skipped is given by the variable
+`electric-pair-skip-whitespace-chars'.
+
+The symbol `chomp' specifies that the skipped-over whitespace
+should be deleted.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes, jump over whitespace" t)
+ (const :tag "Yes, and delete whitespace" 'chomp)
+ (const :tag "No, no whitespace skipping" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace-chars (list ?\t ?\s ?\n)
+ "Whitespace characters considered by `electric-pair-skip-whitespace'."
+ :version "24.4"
+ :type '(choice (set (const :tag "Space" ?\s)
+ (const :tag "Tab" ?\t)
+ (const :tag "Newline" ?\n))
+ (list character)))
+
+(defun electric-pair--skip-whitespace ()
+ "Skip whitespace forward, not crossing comment or string boundaries."
+ (let ((saved (point))
+ (string-or-comment (nth 8 (syntax-ppss))))
+ (skip-chars-forward (apply #'string electric-pair-skip-whitespace-chars))
+ (unless (eq string-or-comment (nth 8 (syntax-ppss)))
+ (goto-char saved))))
+
+(defvar electric-pair-text-syntax-table prog-mode-syntax-table
+ "Syntax table used when pairing inside comments and strings.
+
+`electric-pair-mode' considers this syntax table only when point in inside
+quotes or comments. If lookup fails here, `electric-pair-text-pairs' will
+be considered.")
+
+(defun electric-pair-backward-delete-char (n &optional killflag untabify)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char' or, if UNTABIFY is
+non-nil, `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (let* ((prev (char-before))
+ (next (char-after))
+ (syntax-info (electric-pair-syntax-info prev))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (when (and (if (functionp electric-pair-delete-adjacent-pairs)
+ (funcall electric-pair-delete-adjacent-pairs)
+ electric-pair-delete-adjacent-pairs)
+ next
+ (memq syntax '(?\( ?\" ?\$))
+ (eq pair next))
+ (delete-char 1 killflag))
+ (if untabify
+ (backward-delete-char-untabify n killflag)
+ (backward-delete-char n killflag))))
+
+(defun electric-pair-backward-delete-char-untabify (n &optional killflag)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (electric-pair-backward-delete-char n killflag t))
+
+(defun electric-pair-conservative-inhibit (char)
(or
;; I find it more often preferable not to pair when the
;; same char is next.
@@ -363,14 +496,40 @@ closer."
;; I also find it often preferable not to pair next to a word.
(eq (char-syntax (following-char)) ?w)))
-(defun electric-pair-syntax (command-event)
- (let ((x (assq command-event electric-pair-pairs)))
+(defun electric-pair-syntax-info (command-event)
+ "Calculate a list (SYNTAX PAIR UNCONDITIONAL STRING-OR-COMMENT-START).
+
+SYNTAX is COMMAND-EVENT's syntax character. PAIR is
+COMMAND-EVENT's pair. UNCONDITIONAL indicates the variables
+`electric-pair-pairs' or `electric-pair-text-pairs' were used to
+lookup syntax. STRING-OR-COMMENT-START indicates that point is
+inside a comment of string."
+ (let* ((pre-string-or-comment (nth 8 (save-excursion
+ (syntax-ppss (1- (point))))))
+ (post-string-or-comment (nth 8 (syntax-ppss (point))))
+ (string-or-comment (and post-string-or-comment
+ pre-string-or-comment))
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (table-syntax-and-pair (with-syntax-table table
+ (list (char-syntax command-event)
+ (or (matching-paren command-event)
+ command-event))))
+ (fallback (if string-or-comment
+ (append electric-pair-text-pairs
+ electric-pair-pairs)
+ electric-pair-pairs))
+ (direct (assq command-event fallback))
+ (reverse (rassq command-event fallback)))
(cond
- (x (if (eq (car x) (cdr x)) ?\" ?\())
- ((rassq command-event electric-pair-pairs) ?\))
- ((nth 8 (syntax-ppss))
- (with-syntax-table text-mode-syntax-table (char-syntax command-event)))
- (t (char-syntax command-event)))))
+ ((memq (car table-syntax-and-pair)
+ '(?\" ?\( ?\) ?\$))
+ (append table-syntax-and-pair (list nil string-or-comment)))
+ (direct (if (eq (car direct) (cdr direct))
+ (list ?\" command-event t string-or-comment)
+ (list ?\( (cdr direct) t string-or-comment)))
+ (reverse (list ?\) (car reverse) t string-or-comment)))))
(defun electric-pair--insert (char)
(let ((last-command-event char)
@@ -378,56 +537,286 @@ closer."
(electric-pair-mode nil))
(self-insert-command 1)))
+(defun electric-pair--syntax-ppss (&optional pos where)
+ "Like `syntax-ppss', but sometimes fallback to `parse-partial-sexp'.
+
+WHERE is list defaulting to '(string comment) and indicates
+when to fallback to `parse-partial-sexp'."
+ (let* ((pos (or pos (point)))
+ (where (or where '(string comment)))
+ (quick-ppss (syntax-ppss))
+ (quick-ppss-at-pos (syntax-ppss pos)))
+ (if (or (and (nth 3 quick-ppss) (memq 'string where))
+ (and (nth 4 quick-ppss) (memq 'comment where)))
+ (with-syntax-table electric-pair-text-syntax-table
+ (parse-partial-sexp (1+ (nth 8 quick-ppss)) pos))
+ ;; HACK! cc-mode apparently has some `syntax-ppss' bugs
+ (if (memq major-mode '(c-mode c++ mode))
+ (parse-partial-sexp (point-min) pos)
+ quick-ppss-at-pos))))
+
+;; Balancing means controlling pairing and skipping of parentheses so
+;; that, if possible, the buffer ends up at least as balanced as
+;; before, if not more. The algorithm is slightly complex because some
+;; situations like "()))" need pairing to occur at the end but not at
+;; the beginning. Balancing should also happen independently for
+;; different types of parentheses, so that having your {}'s unbalanced
+;; doesn't keep `electric-pair-mode' from balancing your ()'s and your
+;; []'s.
+(defun electric-pair--balance-info (direction string-or-comment)
+ "Examine lists forward or backward according to DIRECTIONS's sign.
+
+STRING-OR-COMMENT is info suitable for running `parse-partial-sexp'.
+
+Return a cons of two descritions (MATCHED-P . PAIR) for the
+innermost and outermost lists that enclose point. The outermost
+list enclosing point is either the first top-level or first
+mismatched list found by uplisting.
+
+If the outermost list is matched, don't rely on its PAIR. If
+point is not enclosed by any lists, return ((T) (T))."
+ (let* (innermost
+ outermost
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (at-top-level-or-equivalent-fn
+ ;; called when `scan-sexps' ran perfectly, when when it
+ ;; found a parenthesis pointing in the direction of
+ ;; travel. Also when travel started inside a comment and
+ ;; exited it
+ #'(lambda ()
+ (setq outermost (list t))
+ (unless innermost
+ (setq innermost (list t)))))
+ (ended-prematurely-fn
+ ;; called when `scan-sexps' crashed against a parenthesis
+ ;; pointing opposite the direction of travel. After
+ ;; traversing that character, the idea is to travel one sexp
+ ;; in the opposite direction looking for a matching
+ ;; delimiter.
+ #'(lambda ()
+ (let* ((pos (point))
+ (matched
+ (save-excursion
+ (cond ((< direction 0)
+ (condition-case nil
+ (eq (char-after pos)
+ (with-syntax-table table
+ (matching-paren
+ (char-before
+ (scan-sexps (point) 1)))))
+ (scan-error nil)))
+ (t
+ ;; In this case, no need to use
+ ;; `scan-sexps', we can use some
+ ;; `electric-pair--syntax-ppss' in this
+ ;; case (which uses the quicker
+ ;; `syntax-ppss' in some cases)
+ (let* ((ppss (electric-pair--syntax-ppss
+ (1- (point))))
+ (start (car (last (nth 9 ppss))))
+ (opener (char-after start)))
+ (and start
+ (eq (char-before pos)
+ (or (with-syntax-table table
+ (matching-paren opener))
+ opener))))))))
+ (actual-pair (if (> direction 0)
+ (char-before (point))
+ (char-after (point)))))
+ (unless innermost
+ (setq innermost (cons matched actual-pair)))
+ (unless matched
+ (setq outermost (cons matched actual-pair)))))))
+ (save-excursion
+ (while (not outermost)
+ (condition-case err
+ (with-syntax-table table
+ (scan-sexps (point) (if (> direction 0)
+ (point-max)
+ (- (point-max))))
+ (funcall at-top-level-or-equivalent-fn))
+ (scan-error
+ (cond ((or
+ ;; some error happened and it is not of the "ended
+ ;; prematurely" kind"...
+ (not (string-match "ends prematurely" (nth 1 err)))
+ ;; ... or we were in a comment and just came out of
+ ;; it.
+ (and string-or-comment
+ (not (nth 8 (syntax-ppss)))))
+ (funcall at-top-level-or-equivalent-fn))
+ (t
+ ;; exit the sexp
+ (goto-char (nth 3 err))
+ (funcall ended-prematurely-fn)))))))
+ (cons innermost outermost)))
+
+(defun electric-pair--looking-at-unterminated-string-p (char)
+ "Say if following string starts with CHAR and is unterminated."
+ ;; FIXME: ugly/naive
+ (save-excursion
+ (skip-chars-forward (format "^%c" char))
+ (while (not (zerop (% (save-excursion (skip-syntax-backward "\\")) 2)))
+ (unless (eobp)
+ (forward-char 1)
+ (skip-chars-forward (format "^%c" char))))
+ (and (not (eobp))
+ (condition-case err
+ (progn (forward-sexp) nil)
+ (scan-error t)))))
+
+(defun electric-pair--inside-string-p (char)
+ "Say if point is inside a string started by CHAR.
+
+A comments text is parsed with `electric-pair-text-syntax-table'.
+Also consider strings within comments, but not strings within
+strings."
+ ;; FIXME: could also consider strings within strings by examining
+ ;; delimiters.
+ (let* ((ppss (electric-pair--syntax-ppss (point) '(comment))))
+ (memq (nth 3 ppss) (list t char))))
+
+(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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq ?\( syntax)
+ (let* ((pair-data
+ (electric-pair--balance-info 1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (cond ((car outermost)
+ nil)
+ (t
+ (eq (cdr outermost) pair)))))
+ ((eq syntax ?\")
+ (electric-pair--looking-at-unterminated-string-p 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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq syntax ?\))
+ (let* ((pair-data
+ (electric-pair--balance-info
+ -1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (and
+ (cond ((car outermost)
+ (car innermost))
+ ((car innermost)
+ (not (eq (cdr outermost) pair)))))))
+ ((eq syntax ?\")
+ (electric-pair--inside-string-p char))))
+ (insert-char char)))))
+
+(defun electric-pair-default-skip-self (char)
+ (if electric-pair-preserve-balance
+ (electric-pair-skip-if-helps-balance char)
+ t))
+
+(defun electric-pair-default-inhibit (char)
+ (if electric-pair-preserve-balance
+ (electric-pair-inhibit-if-helps-balance char)
+ (electric-pair-conservative-inhibit 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)))
- (closer (if (eq syntax ?\()
- (cdr (or (assq last-command-event electric-pair-pairs)
- (aref (syntax-table) last-command-event)))
- last-command-event)))
- (cond
- ((null pos) nil)
- ;; Wrap a pair around the active region.
- ((and (memq syntax '(?\( ?\" ?\$)) (use-region-p))
- ;; FIXME: To do this right, we'd need a post-self-insert-function
- ;; so we could add-function around it and insert the closer after
- ;; all the rest of the hook has run.
- (if (>= (mark) (point))
- (goto-char (mark))
- ;; We already inserted the open-paren but at the end of the
- ;; region, so we have to remove it and start over.
- (delete-region (1- pos) (point))
- (save-excursion
- (goto-char (mark))
- (electric-pair--insert last-command-event)))
- ;; Since we're right after the closer now, we could tell the rest of
- ;; post-self-insert-hook that we inserted `closer', but then we'd get
- ;; blink-paren to kick in, which is annoying.
- ;;(setq last-command-event closer)
- (insert closer))
- ;; Backslash-escaped: no pairing, no skipping.
- ((save-excursion
- (goto-char (1- pos))
- (not (zerop (% (skip-syntax-backward "\\") 2))))
- nil)
- ;; Skip self.
- ((and (memq syntax '(?\) ?\" ?\$))
- 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
- ;; undo-log and in the intermediate state which might be visible to other
- ;; post-self-insert-hook. We'll just have to live with it for now.
- (delete-char 1))
- ;; Insert matching pair.
- ((not (or (not (memq syntax `(?\( ?\" ?\$)))
- overwrite-mode
- (funcall electric-pair-inhibit-predicate last-command-event)))
- (save-excursion (electric-pair--insert closer))))))
+ (skip-whitespace-info))
+ (pcase (electric-pair-syntax-info last-command-event)
+ (`(,syntax ,pair ,unconditional ,_)
+ (cond
+ ((null pos) nil)
+ ;; Wrap a pair around the active region.
+ ;;
+ ((and (memq syntax '(?\( ?\) ?\" ?\$)) (use-region-p))
+ ;; FIXME: To do this right, we'd need a post-self-insert-function
+ ;; so we could add-function around it and insert the closer after
+ ;; all the rest of the hook has run.
+ (if (or (eq syntax ?\")
+ (and (eq syntax ?\))
+ (>= (point) (mark)))
+ (and (not (eq syntax ?\)))
+ (>= (mark) (point))))
+ (save-excursion
+ (goto-char (mark))
+ (electric-pair--insert pair))
+ (delete-region pos (1- pos))
+ (electric-pair--insert pair)
+ (goto-char (mark))
+ (electric-pair--insert last-command-event)))
+ ;; Backslash-escaped: no pairing, no skipping.
+ ((save-excursion
+ (goto-char (1- pos))
+ (not (zerop (% (skip-syntax-backward "\\") 2))))
+ nil)
+ ;; Skip self.
+ ((and (memq syntax '(?\) ?\" ?\$))
+ (and (or unconditional
+ (if (functionp electric-pair-skip-self)
+ (funcall electric-pair-skip-self last-command-event)
+ electric-pair-skip-self))
+ (save-excursion
+ (when (setq skip-whitespace-info
+ (if (functionp electric-pair-skip-whitespace)
+ (funcall electric-pair-skip-whitespace)
+ electric-pair-skip-whitespace))
+ (electric-pair--skip-whitespace))
+ (eq (char-after) 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 undo-log and in the intermediate state which might
+ ;; be visible to other post-self-insert-hook. We'll just have to
+ ;; live with it for now.
+ (when skip-whitespace-info
+ (electric-pair--skip-whitespace))
+ (delete-region (1- pos) (if (eq skip-whitespace-info 'chomp)
+ (point)
+ pos))
+ (forward-char))
+ ;; Insert matching pair.
+ ((and (memq syntax `(?\( ?\" ?\$))
+ (not overwrite-mode)
+ (or unconditional
+ (not (funcall electric-pair-inhibit-predicate
+ last-command-event))))
+ (save-excursion (electric-pair--insert pair)))))
+ (t
+ (when (and (if (functionp electric-pair-open-newline-between-pairs)
+ (funcall electric-pair-open-newline-between-pairs)
+ electric-pair-open-newline-between-pairs)
+ (eq last-command-event ?\n)
+ (not (eobp))
+ (eq (save-excursion
+ (skip-chars-backward "\t\s")
+ (char-before (1- (point))))
+ (matching-paren (char-after))))
+ (save-excursion (newline 1 t)))))))
+
+(put 'electric-pair-post-self-insert-function 'priority 20)
(defun electric-pair-will-use-region ()
(and (use-region-p)
- (memq (electric-pair-syntax last-command-event) '(?\( ?\" ?\$))))
+ (memq (car (electric-pair-syntax-info last-command-event))
+ '(?\( ?\) ?\" ?\$))))
;;;###autoload
(define-minor-mode electric-pair-mode
@@ -446,21 +835,38 @@ See options `electric-pair-pairs' and `electric-pair-skip-self'."
(progn
(add-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)
(add-hook 'self-insert-uses-region-functions
#'electric-pair-will-use-region))
(remove-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
(remove-hook 'self-insert-uses-region-functions
- #'electric-pair-will-use-region)))
+ #'electric-pair-will-use-region)))
+
+(defvar electric-pair-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map [remap backward-delete-char-untabify]
+ 'electric-pair-backward-delete-char-untabify)
+ (define-key map [remap backward-delete-char]
+ 'electric-pair-backward-delete-char)
+ (define-key map [remap delete-backward-char]
+ 'electric-pair-backward-delete-char)
+ map)
+ "Keymap used by `electric-pair-mode'.")
;;; Electric newlines after/before/around some chars.
-(defvar electric-layout-rules '()
+(defvar electric-layout-rules nil
"List of rules saying where to automatically insert newlines.
-Each rule has the form (CHAR . WHERE) where CHAR is the char
-that was just inserted and WHERE specifies where to insert newlines
-and can be: nil, `before', `after', `around', or a function of no
-arguments that returns one of those symbols.")
+
+Each rule has the form (CHAR . WHERE) where CHAR is the char that
+was just inserted and WHERE specifies where to insert newlines
+and can be: nil, `before', `after', `around', `after-stay', or a
+function of no arguments that returns one of those symbols.
+
+The symbols specify where in relation to CHAR the newline
+character(s) should be inserted. `after-stay' means insert a
+newline after CHAR but stay in the same place.")
(defun electric-layout-post-self-insert-function ()
(let* ((rule (cdr (assq last-command-event electric-layout-rules)))
@@ -469,23 +875,32 @@ arguments that returns one of those symbols.")
(setq pos (electric--after-char-pos))
;; Not in a string or comment.
(not (nth 8 (save-excursion (syntax-ppss pos)))))
- (let ((end (copy-marker (point) t)))
+ (let ((end (copy-marker (point)))
+ (sym (if (functionp rule) (funcall rule) rule)))
+ (set-marker-insertion-type end (not (eq sym 'after-stay)))
(goto-char pos)
- (pcase (if (functionp rule) (funcall rule) rule)
+ (pcase sym
;; FIXME: we used `newline' down here which called
;; self-insert-command and ran post-self-insert-hook recursively.
;; It happened to make electric-indent-mode work automatically with
;; electric-layout-mode (at the cost of re-indenting lines
;; multiple times), but I'm not sure it's what we want.
+ ;;
+ ;; FIXME: check eolp before inserting \n?
(`before (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (`after (insert "\n")) ; FIXME: check eolp before inserting \n?
+ (unless (bolp) (insert "\n")))
+ (`after (insert "\n"))
+ (`after-stay (save-excursion
+ (let ((electric-layout-rules nil))
+ (newline 1 t))))
(`around (save-excursion
- (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (insert "\n"))) ; FIXME: check eolp before inserting \n?
+ (goto-char (1- pos)) (skip-chars-backward " \t")
+ (unless (bolp) (insert "\n")))
+ (insert "\n"))) ; FIXME: check eolp before inserting \n?
(goto-char end)))))
+(put 'electric-layout-post-self-insert-function 'priority 40)
+
;;;###autoload
(define-minor-mode electric-layout-mode
"Automatically insert newlines around some chars.
@@ -494,11 +909,13 @@ positive, and disable it otherwise. If called from Lisp, enable
the mode if ARG is omitted or nil.
The variable `electric-layout-rules' says when and how to insert newlines."
:global t :group 'electricity
- (if electric-layout-mode
- (add-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)
- (remove-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)))
+ (cond (electric-layout-mode
+ (add-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook))
+ (t
+ (remove-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function))))
(provide 'electric)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index f4e9b31..5194e73 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -472,7 +472,12 @@ font-lock keywords will not be case sensitive."
(font-lock-mark-block-function . mark-defun)
(font-lock-syntactic-face-function
. lisp-font-lock-syntactic-face-function)))
- (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
+ (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
+ ;; electric
+ (when elisp
+ (setq-local electric-pair-text-pairs
+ (cons '(?\` . ?\') electric-pair-text-pairs)))
+ (setq-local electric-pair-skip-whitespace 'chomp))
(defun lisp-outline-level ()
"Lisp mode `outline-level' function."
diff --git a/lisp/simple.el b/lisp/simple.el
index 61068ef..30fa5a1 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -607,7 +607,7 @@ In some text modes, where TAB inserts a tab, this command indents to the
column specified by the function `current-left-margin'."
(interactive "*")
(delete-horizontal-space t)
- (newline)
+ (newline 1 (not (or executing-kbd-macro noninteractive)))
(indent-according-to-mode))
(defun reindent-then-newline-and-indent ()
@@ -6437,10 +6437,14 @@ More precisely, a char with closeparen syntax is self-inserted.")
(point))))))
(funcall blink-paren-function)))
+(put 'blink-paren-post-self-insert-function 'priority 100)
+
(add-hook 'post-self-insert-hook #'blink-paren-post-self-insert-function
;; Most likely, this hook is nil, so this arg doesn't matter,
;; but I use it as a reminder that this function usually
- ;; likes to be run after others since it does `sit-for'.
+ ;; likes to be run after others since it does
+ ;; `sit-for'. That's also the reason it get a `priority' prop
+ ;; of 100.
'append)
\f
;; This executes C-g typed while Emacs is waiting for a command.
diff --git a/test/automated/electric-tests.el b/test/automated/electric-tests.el
new file mode 100644
index 0000000..fe71096
--- /dev/null
+++ b/test/automated/electric-tests.el
@@ -0,0 +1,509 @@
+;;; electric-tests.el --- tests for electric.el
+
+;; Copyright (C) 2013 João Távora
+
+;; Author: João Távora <joaotavora@gmail.com>
+;; Keywords:
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'ert-x)
+(require 'electric)
+(require 'cl-lib)
+
+(defun call-with-saved-electric-modes (fn)
+ (let ((saved-electric (if electric-pair-mode 1 -1))
+ (saved-layout (if electric-layout-mode 1 -1))
+ (saved-indent (if electric-indent-mode 1 -1)))
+ (electric-pair-mode -1)
+ (electric-layout-mode -1)
+ (electric-indent-mode -1)
+ (unwind-protect
+ (funcall fn)
+ (electric-pair-mode saved-electric)
+ (electric-indent-mode saved-indent)
+ (electric-layout-mode saved-layout))))
+
+(defmacro save-electric-modes (&rest body)
+ (declare (indent defun) (debug t))
+ `(call-with-saved-electric-modes #'(lambda () ,@body)))
+
+(defun electric-pair-test-for (fixture where char expected-string
+ expected-point mode bindings fixture-fn)
+ (with-temp-buffer
+ (funcall mode)
+ (insert fixture)
+ (save-electric-modes
+ (let ((last-command-event char))
+ (goto-char where)
+ (funcall fixture-fn)
+ (cl-progv
+ (mapcar #'car bindings)
+ (mapcar #'cdr bindings)
+ (self-insert-command 1))))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected-string))
+ (should (equal (point)
+ expected-point))))
+
+(eval-when-compile
+ (defun electric-pair-define-test-form (name fixture
+ char
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn)
+ (let* ((expected-string-and-point
+ (if skip-pair-string
+ (with-temp-buffer
+ (cl-progv
+ ;; FIXME: avoid `eval'
+ (mapcar #'car (eval bindings))
+ (mapcar #'cdr (eval bindings))
+ (funcall mode)
+ (insert fixture)
+ (goto-char (1+ pos))
+ (insert char)
+ (cond ((eq (aref skip-pair-string pos)
+ ?p)
+ (insert (cadr (electric-pair-syntax-info char)))
+ (backward-char 1))
+ ((eq (aref skip-pair-string pos)
+ ?s)
+ (delete-char -1)
+ (forward-char 1)))
+ (list
+ (buffer-substring-no-properties (point-min) (point-max))
+ (point))))
+ (list expected-string expected-point)))
+ (expected-string (car expected-string-and-point))
+ (expected-point (cadr expected-string-and-point))
+ (fixture (format "%s%s%s" prefix fixture suffix))
+ (expected-string (format "%s%s%s" prefix expected-string suffix))
+ (expected-point (+ (length prefix) expected-point))
+ (pos (+ (length prefix) pos)))
+ `(ert-deftest ,(intern (format "electric-pair-%s-at-point-%s-in-%s%s"
+ name
+ (1+ pos)
+ mode
+ extra-desc))
+ ()
+ ,(format "With \"%s\", try input %c at point %d. \
+Should %s \"%s\" and point at %d"
+ fixture
+ char
+ (1+ pos)
+ (if (string= fixture expected-string)
+ "stay"
+ "become")
+ (replace-regexp-in-string "\n" "\\\\n" expected-string)
+ expected-point)
+ (electric-pair-test-for ,fixture
+ ,(1+ pos)
+ ,char
+ ,expected-string
+ ,expected-point
+ ',mode
+ ,bindings
+ ,fixture-fn)))))
+
+(cl-defmacro define-electric-pair-test
+ (name fixture
+ input
+ &key
+ skip-pair-string
+ expected-string
+ expected-point
+ bindings
+ (modes '(quote (emacs-lisp-mode ruby-mode c++-mode)))
+ (test-in-comments t)
+ (test-in-strings t)
+ (test-in-code t)
+ (fixture-fn #'(lambda ()
+ (electric-pair-mode 1))))
+ `(progn
+ ,@(cl-loop
+ for mode in (eval modes) ;FIXME: avoid `eval'
+ append
+ (cl-loop
+ for (prefix suffix extra-desc) in
+ (append (if test-in-comments
+ `((,(with-temp-buffer
+ (funcall mode)
+ (insert "z")
+ (comment-region (point-min) (point-max))
+ (buffer-substring-no-properties (point-min)
+ (1- (point-max))))
+ ""
+ "-in-comments")))
+ (if test-in-strings
+ `(("\"" "\"" "-in-strings")))
+ (if test-in-code
+ `(("" "" ""))))
+ append
+ (cl-loop
+ for char across input
+ for pos from 0
+ unless (eq char ?-)
+ collect (electric-pair-define-test-form
+ name
+ fixture
+ (aref input pos)
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn))))))
+\f
+;;; Basic pairings and skippings
+;;;
+(define-electric-pair-test balanced-situation
+ " (()) " "(((((((" :skip-pair-string "ppppppp"
+ :modes '(ruby-mode))
+
+(define-electric-pair-test too-many-openings
+ " ((()) " "(((((((" :skip-pair-string "ppppppp")
+
+(define-electric-pair-test too-many-closings
+ " (())) " "(((((((" :skip-pair-string "------p")
+
+(define-electric-pair-test too-many-closings-2
+ "() ) " "---(---" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-3
+ ")() " "(------" :skip-pair-string "-------")
+
+(define-electric-pair-test balanced-autoskipping
+ " (()) " "---))--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test too-many-openings-autoskipping
+ " ((()) " "----))-" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-autoskipping
+ " (())) " "---)))-" :skip-pair-string "---sss-")
+
+\f
+;;; Mixed parens
+;;;
+(define-electric-pair-test mixed-paren-1
+ " ()] " "-(-(---" :skip-pair-string "-p-p---")
+
+(define-electric-pair-test mixed-paren-2
+ " [() " "-(-()--" :skip-pair-string "-p-ps--")
+
+(define-electric-pair-test mixed-paren-3
+ " (]) " "-(-()--" :skip-pair-string "---ps--")
+
+(define-electric-pair-test mixed-paren-4
+ " ()] " "---)]--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test mixed-paren-5
+ " [() " "----(--" :skip-pair-string "----p--")
+
+(define-electric-pair-test find-matching-different-paren-type
+ " ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test find-matching-different-paren-type-inside-list
+ "( ()]) " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test ignore-different-unmatching-paren-type
+ "( ()]) " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test autopair-keep-least-amount-of-mixed-unbalance
+ "( ()] " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test dont-autopair-to-resolve-mixed-unbalance
+ "( ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test autopair-so-as-not-to-worsen-unbalance-situation
+ "( (]) " "-[-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test skip-over-partially-balanced
+ " [([]) " "-----)---" :skip-pair-string "-----s---")
+
+(define-electric-pair-test only-skip-over-at-least-partially-balanced-stuff
+ " [([()) " "-----))--" :skip-pair-string "-----s---")
+
+
+
+\f
+;;; Quotes
+;;;
+(define-electric-pair-test pair-some-quotes-skip-others
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test skip-single-quotes-in-ruby-mode
+ " '' " "--'-" :skip-pair-string "--s-"
+ :modes '(ruby-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone
+ " \"' " "-\"'-" :skip-pair-string "----"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-2
+ " \"\\\"' " "-\"--'-" :skip-pair-string "------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-3
+ " foo\\''" "'------" :skip-pair-string "-------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test inhibit-only-if-next-is-mismatched
+ "\"foo\"\"bar" "\""
+ :expected-string "\"\"\"foo\"\"bar"
+ :expected-point 2
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+\f
+;;; More quotes, but now don't bind `electric-pair-text-syntax-table'
+;;; to `prog-mode-syntax-table'. Use the defaults for
+;;; `electric-pair-pairs' and `electric-pair-text-pairs'.
+;;;
+(define-electric-pair-test pairing-skipping-quotes-in-code
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :test-in-comments nil)
+
+(define-electric-pair-test skipping-quotes-in-comments
+ " \"\" " "--\"-----" :skip-pair-string "--s------"
+ :test-in-strings nil)
+
+\f
+;;; Skipping over whitespace
+;;;
+(define-electric-pair-test whitespace-jumping
+ " ( ) " "--))))---" :expected-string " ( ) " :expected-point 8
+ :bindings '((electric-pair-skip-whitespace . t)))
+
+(define-electric-pair-test whitespace-chomping
+ " ( ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+(define-electric-pair-test whitespace-chomping-2
+ " ( \n\t\t\n ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp))
+ :test-in-comments nil)
+
+(define-electric-pair-test whitespace-chomping-dont-cross-comments
+ " ( \n\t\t\n ) " "--)------" :expected-string " () \n\t\t\n ) "
+ :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp))
+ :test-in-strings nil
+ :test-in-code nil
+ :test-in-comments t)
+
+\f
+;;; Pairing arbitrary characters
+;;;
+(define-electric-pair-test angle-brackets-everywhere
+ "<>" "<>" :skip-pair-string "ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(define-electric-pair-test angle-brackets-everywhere-2
+ "(<>" "-<>" :skip-pair-string "-ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(defvar electric-pair-test-angle-brackets-table
+ (let ((table (make-syntax-table prog-mode-syntax-table)))
+ (modify-syntax-entry ?\< "(>" table)
+ (modify-syntax-entry ?\> ")<`" table)
+ table))
+
+(define-electric-pair-test angle-brackets-pair
+ "<>" "<" :expected-string "<><>" :expected-point 2
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test angle-brackets-skip
+ "<>" "->" :expected-string "<>" :expected-point 3
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test pair-backtick-and-quote-in-comments
+ ";; " "---`" :expected-string ";; `'" :expected-point 5
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-comments
+ ";; `foo'" "-------'" :expected-string ";; `foo'" :expected-point 9
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test pair-backtick-and-quote-in-strings
+ "\"\"" "-`" :expected-string "\"`'\"" :expected-point 3
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings
+ "\"`'\"" "--'" :expected-string "\"`'\"" :expected-point 4
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings-2
+ " \"`'\"" "----'" :expected-string " \"`'\"" :expected-point 6
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+\f
+;;; `js-mode' has `electric-layout-rules' for '{ and '}
+;;;
+(define-electric-pair-test js-mode-braces
+ "" "{" :expected-string "{}" :expected-point 2
+ :modes '(js-mode)
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout
+ "" "{" :expected-string "{\n\n}" :expected-point 3
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-layout-mode 1)
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout-and-indent
+ "" "{" :expected-string "{\n \n}" :expected-point 7
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (electric-layout-mode 1)))
+
+\f
+;;; Backspacing
+;;; TODO: better tests
+;;;
+(ert-deftest electric-pair-backspace-1 ()
+ (save-electric-modes
+ (with-temp-buffer
+ (insert "()")
+ (goto-char 2)
+ (electric-pair-backward-delete-char 1)
+ (should (equal "" (buffer-string))))))
+
+\f
+;;; Electric newlines between pairs
+;;; TODO: better tests
+(ert-deftest electric-pair-open-extra-newline ()
+ (save-electric-modes
+ (with-temp-buffer
+ (c-mode)
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (insert "int main {}")
+ (backward-char 1)
+ (let ((c-basic-offset 4))
+ (newline 1 t)
+ (should (equal "int main {\n \n}"
+ (buffer-string)))
+ (should (equal (point) (- (point-max) 2)))))))
+
+
+\f
+;;; Autowrapping
+;;;
+(define-electric-pair-test autowrapping-1
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-2
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-3
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-4
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-5
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-6
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(provide 'electric-pair-tests)
+;;; electric-pair-tests.el ends here
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
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
0 siblings, 1 reply; 36+ messages in thread
From: João Távora @ 2013-12-23 14:41 UTC (permalink / raw
To: Stefan Monnier; +Cc: Dmitry Gutov, emacs-devel
Stefan,
Don't want to be pushy, but is there anything holding up this patch?
Here's yet another version that fixes some more details:
- fixed default values for `electric-pair-inhibit-predicate' and
`electric-pair-skip-self'.
- electric-pair-mode's docstring doesn't mention related customization
variables because there are now too many of them.
- emacs-lisp/lisp-mode.el locally sets
`electric-pair-open-newline-between-pairs' to t.
- the newline call in `newline-and-indent` now uses interactive set to
t
Thanks
João
diff --git a/lisp/ChangeLog b/lisp/ChangeLog
index 12889de..0564484 100644
--- a/lisp/ChangeLog
+++ b/lisp/ChangeLog
@@ -1,3 +1,25 @@
+2013-12-99 João Távora <joaotavora@gmail.com>
+
+ * electric.el (electric-pair-mode): More flexible engine for skip-
+ and inhibit predicates, new options for pairing-related
+ functionality.
+ (electric-pair-preserve-balance): Pair/skip parentheses and quotes
+ if that keeps or improves their balance in buffers.
+ (electric-pair-delete-adjacent-pairs): Delete the pair when
+ backspacing over adjacent matched delimiters.
+ (electric-pair-open-extra-newline): Open extra newline when
+ inserting newlines between adjacent matched delimiters.
+ (electric--sort-post-self-insertion-hook): Sort
+ post-self-insert-hook according to priority values when
+ minor-modes are activated.
+ * simple.el (newline-and-indent): Call newline with interactive
+ set to t.
+ (blink-paren-post-self-insert-function): Set priority to 100.
+ * emacs-lisp/lisp-mode.el (lisp-mode-variables): Use
+ electric-pair-text-pairs to pair backtick-and-quote in strings and
+ comments. Locally set electric-pair-skip-whitespace to 'chomp and
+ electric-pair-open-newline-between-pairs to nil.
+
2013-12-23 Chong Yidong <cyd@gnu.org>
* subr.el (set-transient-map): Rename from
diff --git a/lisp/electric.el b/lisp/electric.el
index 91b99b4..ca4616b 100644
--- a/lisp/electric.el
+++ b/lisp/electric.el
@@ -187,6 +187,17 @@ Returns nil when we can't find this char."
(eq (char-before) last-command-event)))))
pos)))
+(defun electric--sort-post-self-insertion-hook ()
+ "Ensure order of electric functions in `post-self-insertion-hook'.
+
+Hooks in this variable interact in non-trivial ways, so a
+relative order must be maintained within it."
+ (setq-default post-self-insert-hook
+ (sort (default-value 'post-self-insert-hook)
+ #'(lambda (fn1 fn2)
+ (< (or (get fn1 'priority) 0)
+ (or (get fn2 'priority) 0))))))
+
;;; Electric indentation.
;; Autoloading variables is generally undesirable, but major modes
@@ -267,6 +278,8 @@ mode set `electric-indent-inhibit', but this can be used as a workaround.")
(> pos (line-beginning-position)))
(indent-according-to-mode)))))
+(put 'electric-indent-post-self-insert-function 'priority 60)
+
(defun electric-indent-just-newline (arg)
"Insert just a newline, without any auto-indentation."
(interactive "*P")
@@ -295,20 +308,9 @@ insert a character from `electric-indent-chars'."
#'electric-indent-post-self-insert-function))
(when (eq (lookup-key global-map [?\C-j]) 'newline-and-indent)
(define-key global-map [?\C-j] 'electric-indent-just-newline))
- ;; post-self-insert-hooks interact in non-trivial ways.
- ;; It turns out that electric-indent-mode generally works better if run
- ;; late, but still before blink-paren.
(add-hook 'post-self-insert-hook
- #'electric-indent-post-self-insert-function
- 'append)
- ;; FIXME: Ugly!
- (let ((bp (memq #'blink-paren-post-self-insert-function
- (default-value 'post-self-insert-hook))))
- (when (memq #'electric-indent-post-self-insert-function bp)
- (setcar bp #'electric-indent-post-self-insert-function)
- (setcdr bp (cons #'blink-paren-post-self-insert-function
- (delq #'electric-indent-post-self-insert-function
- (cdr bp))))))))
+ #'electric-indent-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)))
;;;###autoload
(define-minor-mode electric-indent-local-mode
@@ -327,32 +329,163 @@ insert a character from `electric-indent-chars'."
(defcustom electric-pair-pairs
'((?\" . ?\"))
- "Alist of pairs that should be used regardless of major mode."
+ "Alist of pairs that should be used regardless of major mode.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the mode's syntax
+table.
+
+See also the variable `electric-pair-text-pairs'."
:version "24.1"
:type '(repeat (cons character character)))
-(defcustom electric-pair-skip-self t
+(defcustom electric-pair-text-pairs
+ '((?\" . ?\" ))
+ "Alist of pairs that should always be used in comments and strings.
+
+Pairs of delimiters in this list are a fallback in case they have
+no syntax relevant to `electric-pair-mode' in the syntax table
+defined in `electric-pair-text-syntax-table'"
+ :version "24.4"
+ :type '(repeat (cons character character)))
+
+(defcustom electric-pair-skip-self #'electric-pair-default-skip-self
"If non-nil, skip char instead of inserting a second closing paren.
+
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."
+
+This can be convenient for people who find it easier to hit ) than C-f.
+
+Can also be a function of one argument (the closer char just
+inserted), in which case that function's return value is
+considered instead."
:version "24.1"
- :type 'boolean)
+ :type '(choice
+ (const :tag "Never skip" nil)
+ (const :tag "Help balance" electric-pair-default-skip-self)
+ (const :tag "Always skip" t)
+ function))
(defcustom electric-pair-inhibit-predicate
#'electric-pair-default-inhibit
"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-default-inhibit)
(const :tag "Always pair" ignore)
function))
-(defun electric-pair-default-inhibit (char)
+(defcustom electric-pair-preserve-balance t
+ "Non-nil if default pairing and skipping should help balance parentheses.
+
+The default values of `electric-pair-inhibit-predicate' and
+`electric-pair-skip-self' check this variable before delegating to other
+predicates reponsible for making decisions on whether to pair/skip some
+characters based on the actual state of the buffer's parenthesis and
+quotes."
+ :version "24.4"
+ :type 'boolean)
+
+(defcustom electric-pair-delete-adjacent-pairs t
+ "If non-nil, backspacing an open paren also deletes adjacent closer.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-open-newline-between-pairs t
+ "If non-nil, a newline between adjacent parentheses opens an extra one.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes" t)
+ (const :tag "No" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace t
+ "If non-nil skip whitespace when skipping over closing parens.
+
+The specific kind of whitespace skipped is given by the variable
+`electric-pair-skip-whitespace-chars'.
+
+The symbol `chomp' specifies that the skipped-over whitespace
+should be deleted.
+
+Can also be a function of no arguments, in which case that function's
+return value is considered instead."
+ :version "24.4"
+ :type '(choice
+ (const :tag "Yes, jump over whitespace" t)
+ (const :tag "Yes, and delete whitespace" 'chomp)
+ (const :tag "No, no whitespace skipping" nil)
+ function))
+
+(defcustom electric-pair-skip-whitespace-chars (list ?\t ?\s ?\n)
+ "Whitespace characters considered by `electric-pair-skip-whitespace'."
+ :version "24.4"
+ :type '(choice (set (const :tag "Space" ?\s)
+ (const :tag "Tab" ?\t)
+ (const :tag "Newline" ?\n))
+ (list character)))
+
+(defun electric-pair--skip-whitespace ()
+ "Skip whitespace forward, not crossing comment or string boundaries."
+ (let ((saved (point))
+ (string-or-comment (nth 8 (syntax-ppss))))
+ (skip-chars-forward (apply #'string electric-pair-skip-whitespace-chars))
+ (unless (eq string-or-comment (nth 8 (syntax-ppss)))
+ (goto-char saved))))
+
+(defvar electric-pair-text-syntax-table prog-mode-syntax-table
+ "Syntax table used when pairing inside comments and strings.
+
+`electric-pair-mode' considers this syntax table only when point in inside
+quotes or comments. If lookup fails here, `electric-pair-text-pairs' will
+be considered.")
+
+(defun electric-pair-backward-delete-char (n &optional killflag untabify)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char' or, if UNTABIFY is
+non-nil, `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (let* ((prev (char-before))
+ (next (char-after))
+ (syntax-info (electric-pair-syntax-info prev))
+ (syntax (car syntax-info))
+ (pair (cadr syntax-info)))
+ (when (and (if (functionp electric-pair-delete-adjacent-pairs)
+ (funcall electric-pair-delete-adjacent-pairs)
+ electric-pair-delete-adjacent-pairs)
+ next
+ (memq syntax '(?\( ?\" ?\$))
+ (eq pair next))
+ (delete-char 1 killflag))
+ (if untabify
+ (backward-delete-char-untabify n killflag)
+ (backward-delete-char n killflag))))
+
+(defun electric-pair-backward-delete-char-untabify (n &optional killflag)
+ "Delete characters backward, and maybe also two adjacent paired delimiters.
+
+Remaining behaviour is given by `backward-delete-char-untabify'."
+ (interactive "*p\nP")
+ (electric-pair-backward-delete-char n killflag t))
+
+(defun electric-pair-conservative-inhibit (char)
(or
;; I find it more often preferable not to pair when the
;; same char is next.
@@ -363,14 +496,40 @@ closer."
;; I also find it often preferable not to pair next to a word.
(eq (char-syntax (following-char)) ?w)))
-(defun electric-pair-syntax (command-event)
- (let ((x (assq command-event electric-pair-pairs)))
+(defun electric-pair-syntax-info (command-event)
+ "Calculate a list (SYNTAX PAIR UNCONDITIONAL STRING-OR-COMMENT-START).
+
+SYNTAX is COMMAND-EVENT's syntax character. PAIR is
+COMMAND-EVENT's pair. UNCONDITIONAL indicates the variables
+`electric-pair-pairs' or `electric-pair-text-pairs' were used to
+lookup syntax. STRING-OR-COMMENT-START indicates that point is
+inside a comment of string."
+ (let* ((pre-string-or-comment (nth 8 (save-excursion
+ (syntax-ppss (1- (point))))))
+ (post-string-or-comment (nth 8 (syntax-ppss (point))))
+ (string-or-comment (and post-string-or-comment
+ pre-string-or-comment))
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (table-syntax-and-pair (with-syntax-table table
+ (list (char-syntax command-event)
+ (or (matching-paren command-event)
+ command-event))))
+ (fallback (if string-or-comment
+ (append electric-pair-text-pairs
+ electric-pair-pairs)
+ electric-pair-pairs))
+ (direct (assq command-event fallback))
+ (reverse (rassq command-event fallback)))
(cond
- (x (if (eq (car x) (cdr x)) ?\" ?\())
- ((rassq command-event electric-pair-pairs) ?\))
- ((nth 8 (syntax-ppss))
- (with-syntax-table text-mode-syntax-table (char-syntax command-event)))
- (t (char-syntax command-event)))))
+ ((memq (car table-syntax-and-pair)
+ '(?\" ?\( ?\) ?\$))
+ (append table-syntax-and-pair (list nil string-or-comment)))
+ (direct (if (eq (car direct) (cdr direct))
+ (list ?\" command-event t string-or-comment)
+ (list ?\( (cdr direct) t string-or-comment)))
+ (reverse (list ?\) (car reverse) t string-or-comment)))))
(defun electric-pair--insert (char)
(let ((last-command-event char)
@@ -378,56 +537,286 @@ closer."
(electric-pair-mode nil))
(self-insert-command 1)))
+(defun electric-pair--syntax-ppss (&optional pos where)
+ "Like `syntax-ppss', but sometimes fallback to `parse-partial-sexp'.
+
+WHERE is list defaulting to '(string comment) and indicates
+when to fallback to `parse-partial-sexp'."
+ (let* ((pos (or pos (point)))
+ (where (or where '(string comment)))
+ (quick-ppss (syntax-ppss))
+ (quick-ppss-at-pos (syntax-ppss pos)))
+ (if (or (and (nth 3 quick-ppss) (memq 'string where))
+ (and (nth 4 quick-ppss) (memq 'comment where)))
+ (with-syntax-table electric-pair-text-syntax-table
+ (parse-partial-sexp (1+ (nth 8 quick-ppss)) pos))
+ ;; HACK! cc-mode apparently has some `syntax-ppss' bugs
+ (if (memq major-mode '(c-mode c++ mode))
+ (parse-partial-sexp (point-min) pos)
+ quick-ppss-at-pos))))
+
+;; Balancing means controlling pairing and skipping of parentheses so
+;; that, if possible, the buffer ends up at least as balanced as
+;; before, if not more. The algorithm is slightly complex because some
+;; situations like "()))" need pairing to occur at the end but not at
+;; the beginning. Balancing should also happen independently for
+;; different types of parentheses, so that having your {}'s unbalanced
+;; doesn't keep `electric-pair-mode' from balancing your ()'s and your
+;; []'s.
+(defun electric-pair--balance-info (direction string-or-comment)
+ "Examine lists forward or backward according to DIRECTIONS's sign.
+
+STRING-OR-COMMENT is info suitable for running `parse-partial-sexp'.
+
+Return a cons of two descritions (MATCHED-P . PAIR) for the
+innermost and outermost lists that enclose point. The outermost
+list enclosing point is either the first top-level or first
+mismatched list found by uplisting.
+
+If the outermost list is matched, don't rely on its PAIR. If
+point is not enclosed by any lists, return ((T) (T))."
+ (let* (innermost
+ outermost
+ (table (if string-or-comment
+ electric-pair-text-syntax-table
+ (syntax-table)))
+ (at-top-level-or-equivalent-fn
+ ;; called when `scan-sexps' ran perfectly, when when it
+ ;; found a parenthesis pointing in the direction of
+ ;; travel. Also when travel started inside a comment and
+ ;; exited it
+ #'(lambda ()
+ (setq outermost (list t))
+ (unless innermost
+ (setq innermost (list t)))))
+ (ended-prematurely-fn
+ ;; called when `scan-sexps' crashed against a parenthesis
+ ;; pointing opposite the direction of travel. After
+ ;; traversing that character, the idea is to travel one sexp
+ ;; in the opposite direction looking for a matching
+ ;; delimiter.
+ #'(lambda ()
+ (let* ((pos (point))
+ (matched
+ (save-excursion
+ (cond ((< direction 0)
+ (condition-case nil
+ (eq (char-after pos)
+ (with-syntax-table table
+ (matching-paren
+ (char-before
+ (scan-sexps (point) 1)))))
+ (scan-error nil)))
+ (t
+ ;; In this case, no need to use
+ ;; `scan-sexps', we can use some
+ ;; `electric-pair--syntax-ppss' in this
+ ;; case (which uses the quicker
+ ;; `syntax-ppss' in some cases)
+ (let* ((ppss (electric-pair--syntax-ppss
+ (1- (point))))
+ (start (car (last (nth 9 ppss))))
+ (opener (char-after start)))
+ (and start
+ (eq (char-before pos)
+ (or (with-syntax-table table
+ (matching-paren opener))
+ opener))))))))
+ (actual-pair (if (> direction 0)
+ (char-before (point))
+ (char-after (point)))))
+ (unless innermost
+ (setq innermost (cons matched actual-pair)))
+ (unless matched
+ (setq outermost (cons matched actual-pair)))))))
+ (save-excursion
+ (while (not outermost)
+ (condition-case err
+ (with-syntax-table table
+ (scan-sexps (point) (if (> direction 0)
+ (point-max)
+ (- (point-max))))
+ (funcall at-top-level-or-equivalent-fn))
+ (scan-error
+ (cond ((or
+ ;; some error happened and it is not of the "ended
+ ;; prematurely" kind"...
+ (not (string-match "ends prematurely" (nth 1 err)))
+ ;; ... or we were in a comment and just came out of
+ ;; it.
+ (and string-or-comment
+ (not (nth 8 (syntax-ppss)))))
+ (funcall at-top-level-or-equivalent-fn))
+ (t
+ ;; exit the sexp
+ (goto-char (nth 3 err))
+ (funcall ended-prematurely-fn)))))))
+ (cons innermost outermost)))
+
+(defun electric-pair--looking-at-unterminated-string-p (char)
+ "Say if following string starts with CHAR and is unterminated."
+ ;; FIXME: ugly/naive
+ (save-excursion
+ (skip-chars-forward (format "^%c" char))
+ (while (not (zerop (% (save-excursion (skip-syntax-backward "\\")) 2)))
+ (unless (eobp)
+ (forward-char 1)
+ (skip-chars-forward (format "^%c" char))))
+ (and (not (eobp))
+ (condition-case err
+ (progn (forward-sexp) nil)
+ (scan-error t)))))
+
+(defun electric-pair--inside-string-p (char)
+ "Say if point is inside a string started by CHAR.
+
+A comments text is parsed with `electric-pair-text-syntax-table'.
+Also consider strings within comments, but not strings within
+strings."
+ ;; FIXME: could also consider strings within strings by examining
+ ;; delimiters.
+ (let* ((ppss (electric-pair--syntax-ppss (point) '(comment))))
+ (memq (nth 3 ppss) (list t char))))
+
+(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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq ?\( syntax)
+ (let* ((pair-data
+ (electric-pair--balance-info 1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (cond ((car outermost)
+ nil)
+ (t
+ (eq (cdr outermost) pair)))))
+ ((eq syntax ?\")
+ (electric-pair--looking-at-unterminated-string-p 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, finally restoring the situation as if nothing
+happened."
+ (pcase (electric-pair-syntax-info char)
+ (`(,syntax ,pair ,_ ,s-or-c)
+ (unwind-protect
+ (progn
+ (delete-char -1)
+ (cond ((eq syntax ?\))
+ (let* ((pair-data
+ (electric-pair--balance-info
+ -1 s-or-c))
+ (innermost (car pair-data))
+ (outermost (cdr pair-data)))
+ (and
+ (cond ((car outermost)
+ (car innermost))
+ ((car innermost)
+ (not (eq (cdr outermost) pair)))))))
+ ((eq syntax ?\")
+ (electric-pair--inside-string-p char))))
+ (insert-char char)))))
+
+(defun electric-pair-default-skip-self (char)
+ (if electric-pair-preserve-balance
+ (electric-pair-skip-if-helps-balance char)
+ t))
+
+(defun electric-pair-default-inhibit (char)
+ (if electric-pair-preserve-balance
+ (electric-pair-inhibit-if-helps-balance char)
+ (electric-pair-conservative-inhibit 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)))
- (closer (if (eq syntax ?\()
- (cdr (or (assq last-command-event electric-pair-pairs)
- (aref (syntax-table) last-command-event)))
- last-command-event)))
- (cond
- ((null pos) nil)
- ;; Wrap a pair around the active region.
- ((and (memq syntax '(?\( ?\" ?\$)) (use-region-p))
- ;; FIXME: To do this right, we'd need a post-self-insert-function
- ;; so we could add-function around it and insert the closer after
- ;; all the rest of the hook has run.
- (if (>= (mark) (point))
- (goto-char (mark))
- ;; We already inserted the open-paren but at the end of the
- ;; region, so we have to remove it and start over.
- (delete-region (1- pos) (point))
- (save-excursion
- (goto-char (mark))
- (electric-pair--insert last-command-event)))
- ;; Since we're right after the closer now, we could tell the rest of
- ;; post-self-insert-hook that we inserted `closer', but then we'd get
- ;; blink-paren to kick in, which is annoying.
- ;;(setq last-command-event closer)
- (insert closer))
- ;; Backslash-escaped: no pairing, no skipping.
- ((save-excursion
- (goto-char (1- pos))
- (not (zerop (% (skip-syntax-backward "\\") 2))))
- nil)
- ;; Skip self.
- ((and (memq syntax '(?\) ?\" ?\$))
- 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
- ;; undo-log and in the intermediate state which might be visible to other
- ;; post-self-insert-hook. We'll just have to live with it for now.
- (delete-char 1))
- ;; Insert matching pair.
- ((not (or (not (memq syntax `(?\( ?\" ?\$)))
- overwrite-mode
- (funcall electric-pair-inhibit-predicate last-command-event)))
- (save-excursion (electric-pair--insert closer))))))
+ (skip-whitespace-info))
+ (pcase (electric-pair-syntax-info last-command-event)
+ (`(,syntax ,pair ,unconditional ,_)
+ (cond
+ ((null pos) nil)
+ ;; Wrap a pair around the active region.
+ ;;
+ ((and (memq syntax '(?\( ?\) ?\" ?\$)) (use-region-p))
+ ;; FIXME: To do this right, we'd need a post-self-insert-function
+ ;; so we could add-function around it and insert the closer after
+ ;; all the rest of the hook has run.
+ (if (or (eq syntax ?\")
+ (and (eq syntax ?\))
+ (>= (point) (mark)))
+ (and (not (eq syntax ?\)))
+ (>= (mark) (point))))
+ (save-excursion
+ (goto-char (mark))
+ (electric-pair--insert pair))
+ (delete-region pos (1- pos))
+ (electric-pair--insert pair)
+ (goto-char (mark))
+ (electric-pair--insert last-command-event)))
+ ;; Backslash-escaped: no pairing, no skipping.
+ ((save-excursion
+ (goto-char (1- pos))
+ (not (zerop (% (skip-syntax-backward "\\") 2))))
+ nil)
+ ;; Skip self.
+ ((and (memq syntax '(?\) ?\" ?\$))
+ (and (or unconditional
+ (if (functionp electric-pair-skip-self)
+ (funcall electric-pair-skip-self last-command-event)
+ electric-pair-skip-self))
+ (save-excursion
+ (when (setq skip-whitespace-info
+ (if (functionp electric-pair-skip-whitespace)
+ (funcall electric-pair-skip-whitespace)
+ electric-pair-skip-whitespace))
+ (electric-pair--skip-whitespace))
+ (eq (char-after) 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 undo-log and in the intermediate state which might
+ ;; be visible to other post-self-insert-hook. We'll just have to
+ ;; live with it for now.
+ (when skip-whitespace-info
+ (electric-pair--skip-whitespace))
+ (delete-region (1- pos) (if (eq skip-whitespace-info 'chomp)
+ (point)
+ pos))
+ (forward-char))
+ ;; Insert matching pair.
+ ((and (memq syntax `(?\( ?\" ?\$))
+ (not overwrite-mode)
+ (or unconditional
+ (not (funcall electric-pair-inhibit-predicate
+ last-command-event))))
+ (save-excursion (electric-pair--insert pair)))))
+ (t
+ (when (and (if (functionp electric-pair-open-newline-between-pairs)
+ (funcall electric-pair-open-newline-between-pairs)
+ electric-pair-open-newline-between-pairs)
+ (eq last-command-event ?\n)
+ (not (eobp))
+ (eq (save-excursion
+ (skip-chars-backward "\t\s")
+ (char-before (1- (point))))
+ (matching-paren (char-after))))
+ (save-excursion (newline 1 t)))))))
+
+(put 'electric-pair-post-self-insert-function 'priority 20)
(defun electric-pair-will-use-region ()
(and (use-region-p)
- (memq (electric-pair-syntax last-command-event) '(?\( ?\" ?\$))))
+ (memq (car (electric-pair-syntax-info last-command-event))
+ '(?\( ?\) ?\" ?\$))))
;;;###autoload
(define-minor-mode electric-pair-mode
@@ -438,29 +827,44 @@ the mode if ARG is omitted or nil.
Electric Pair mode is a global minor mode. When enabled, typing
an open parenthesis automatically inserts the corresponding
-closing parenthesis. \(Likewise for brackets, etc.)
-
-See options `electric-pair-pairs' and `electric-pair-skip-self'."
+closing parenthesis. \(Likewise for brackets, etc.)."
:global t :group 'electricity
(if electric-pair-mode
(progn
(add-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook)
(add-hook 'self-insert-uses-region-functions
#'electric-pair-will-use-region))
(remove-hook 'post-self-insert-hook
#'electric-pair-post-self-insert-function)
(remove-hook 'self-insert-uses-region-functions
- #'electric-pair-will-use-region)))
+ #'electric-pair-will-use-region)))
+
+(defvar electric-pair-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map [remap backward-delete-char-untabify]
+ 'electric-pair-backward-delete-char-untabify)
+ (define-key map [remap backward-delete-char]
+ 'electric-pair-backward-delete-char)
+ (define-key map [remap delete-backward-char]
+ 'electric-pair-backward-delete-char)
+ map)
+ "Keymap used by `electric-pair-mode'.")
;;; Electric newlines after/before/around some chars.
-(defvar electric-layout-rules '()
+(defvar electric-layout-rules nil
"List of rules saying where to automatically insert newlines.
-Each rule has the form (CHAR . WHERE) where CHAR is the char
-that was just inserted and WHERE specifies where to insert newlines
-and can be: nil, `before', `after', `around', or a function of no
-arguments that returns one of those symbols.")
+
+Each rule has the form (CHAR . WHERE) where CHAR is the char that
+was just inserted and WHERE specifies where to insert newlines
+and can be: nil, `before', `after', `around', `after-stay', or a
+function of no arguments that returns one of those symbols.
+
+The symbols specify where in relation to CHAR the newline
+character(s) should be inserted. `after-stay' means insert a
+newline after CHAR but stay in the same place.")
(defun electric-layout-post-self-insert-function ()
(let* ((rule (cdr (assq last-command-event electric-layout-rules)))
@@ -469,23 +873,32 @@ arguments that returns one of those symbols.")
(setq pos (electric--after-char-pos))
;; Not in a string or comment.
(not (nth 8 (save-excursion (syntax-ppss pos)))))
- (let ((end (copy-marker (point) t)))
+ (let ((end (copy-marker (point)))
+ (sym (if (functionp rule) (funcall rule) rule)))
+ (set-marker-insertion-type end (not (eq sym 'after-stay)))
(goto-char pos)
- (pcase (if (functionp rule) (funcall rule) rule)
+ (pcase sym
;; FIXME: we used `newline' down here which called
;; self-insert-command and ran post-self-insert-hook recursively.
;; It happened to make electric-indent-mode work automatically with
;; electric-layout-mode (at the cost of re-indenting lines
;; multiple times), but I'm not sure it's what we want.
+ ;;
+ ;; FIXME: check eolp before inserting \n?
(`before (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (`after (insert "\n")) ; FIXME: check eolp before inserting \n?
+ (unless (bolp) (insert "\n")))
+ (`after (insert "\n"))
+ (`after-stay (save-excursion
+ (let ((electric-layout-rules nil))
+ (newline 1 t))))
(`around (save-excursion
- (goto-char (1- pos)) (skip-chars-backward " \t")
- (unless (bolp) (insert "\n")))
- (insert "\n"))) ; FIXME: check eolp before inserting \n?
+ (goto-char (1- pos)) (skip-chars-backward " \t")
+ (unless (bolp) (insert "\n")))
+ (insert "\n"))) ; FIXME: check eolp before inserting \n?
(goto-char end)))))
+(put 'electric-layout-post-self-insert-function 'priority 40)
+
;;;###autoload
(define-minor-mode electric-layout-mode
"Automatically insert newlines around some chars.
@@ -494,11 +907,13 @@ positive, and disable it otherwise. If called from Lisp, enable
the mode if ARG is omitted or nil.
The variable `electric-layout-rules' says when and how to insert newlines."
:global t :group 'electricity
- (if electric-layout-mode
- (add-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)
- (remove-hook 'post-self-insert-hook
- #'electric-layout-post-self-insert-function)))
+ (cond (electric-layout-mode
+ (add-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function)
+ (electric--sort-post-self-insertion-hook))
+ (t
+ (remove-hook 'post-self-insert-hook
+ #'electric-layout-post-self-insert-function))))
(provide 'electric)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index b7bd33f..f1eae18 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -472,7 +472,13 @@ font-lock keywords will not be case sensitive."
(font-lock-mark-block-function . mark-defun)
(font-lock-syntactic-face-function
. lisp-font-lock-syntactic-face-function)))
- (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
+ (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
+ ;; electric
+ (when elisp
+ (setq-local electric-pair-text-pairs
+ (cons '(?\` . ?\') electric-pair-text-pairs)))
+ (setq-local electric-pair-skip-whitespace 'chomp)
+ (setq-local electric-pair-open-newline-between-pairs nil))
(defun lisp-outline-level ()
"Lisp mode `outline-level' function."
diff --git a/lisp/simple.el b/lisp/simple.el
index a654351..624d87f 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -610,7 +610,7 @@ In some text modes, where TAB inserts a tab, this command indents to the
column specified by the function `current-left-margin'."
(interactive "*")
(delete-horizontal-space t)
- (newline)
+ (newline 1 t)
(indent-according-to-mode))
(defun reindent-then-newline-and-indent ()
@@ -6448,10 +6448,14 @@ More precisely, a char with closeparen syntax is self-inserted.")
(point))))))
(funcall blink-paren-function)))
+(put 'blink-paren-post-self-insert-function 'priority 100)
+
(add-hook 'post-self-insert-hook #'blink-paren-post-self-insert-function
;; Most likely, this hook is nil, so this arg doesn't matter,
;; but I use it as a reminder that this function usually
- ;; likes to be run after others since it does `sit-for'.
+ ;; likes to be run after others since it does
+ ;; `sit-for'. That's also the reason it get a `priority' prop
+ ;; of 100.
'append)
\f
;; This executes C-g typed while Emacs is waiting for a command.
diff --git a/test/automated/electric-tests.el b/test/automated/electric-tests.el
new file mode 100644
index 0000000..aa4a063
--- /dev/null
+++ b/test/automated/electric-tests.el
@@ -0,0 +1,509 @@
+;;; electric-tests.el --- tests for electric.el
+
+;; Copyright (C) 2013 João Távora
+
+;; Author: João Távora <joaotavora@gmail.com>
+;; Keywords:
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'ert)
+(require 'ert-x)
+(require 'electric)
+(require 'cl-lib)
+
+(defun call-with-saved-electric-modes (fn)
+ (let ((saved-electric (if electric-pair-mode 1 -1))
+ (saved-layout (if electric-layout-mode 1 -1))
+ (saved-indent (if electric-indent-mode 1 -1)))
+ (electric-pair-mode -1)
+ (electric-layout-mode -1)
+ (electric-indent-mode -1)
+ (unwind-protect
+ (funcall fn)
+ (electric-pair-mode saved-electric)
+ (electric-indent-mode saved-indent)
+ (electric-layout-mode saved-layout))))
+
+(defmacro save-electric-modes (&rest body)
+ (declare (indent defun) (debug t))
+ `(call-with-saved-electric-modes #'(lambda () ,@body)))
+
+(defun electric-pair-test-for (fixture where char expected-string
+ expected-point mode bindings fixture-fn)
+ (with-temp-buffer
+ (funcall mode)
+ (insert fixture)
+ (save-electric-modes
+ (let ((last-command-event char))
+ (goto-char where)
+ (funcall fixture-fn)
+ (cl-progv
+ (mapcar #'car bindings)
+ (mapcar #'cdr bindings)
+ (self-insert-command 1))))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected-string))
+ (should (equal (point)
+ expected-point))))
+
+(eval-when-compile
+ (defun electric-pair-define-test-form (name fixture
+ char
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn)
+ (let* ((expected-string-and-point
+ (if skip-pair-string
+ (with-temp-buffer
+ (cl-progv
+ ;; FIXME: avoid `eval'
+ (mapcar #'car (eval bindings))
+ (mapcar #'cdr (eval bindings))
+ (funcall mode)
+ (insert fixture)
+ (goto-char (1+ pos))
+ (insert char)
+ (cond ((eq (aref skip-pair-string pos)
+ ?p)
+ (insert (cadr (electric-pair-syntax-info char)))
+ (backward-char 1))
+ ((eq (aref skip-pair-string pos)
+ ?s)
+ (delete-char -1)
+ (forward-char 1)))
+ (list
+ (buffer-substring-no-properties (point-min) (point-max))
+ (point))))
+ (list expected-string expected-point)))
+ (expected-string (car expected-string-and-point))
+ (expected-point (cadr expected-string-and-point))
+ (fixture (format "%s%s%s" prefix fixture suffix))
+ (expected-string (format "%s%s%s" prefix expected-string suffix))
+ (expected-point (+ (length prefix) expected-point))
+ (pos (+ (length prefix) pos)))
+ `(ert-deftest ,(intern (format "electric-pair-%s-at-point-%s-in-%s%s"
+ name
+ (1+ pos)
+ mode
+ extra-desc))
+ ()
+ ,(format "With \"%s\", try input %c at point %d. \
+Should %s \"%s\" and point at %d"
+ fixture
+ char
+ (1+ pos)
+ (if (string= fixture expected-string)
+ "stay"
+ "become")
+ (replace-regexp-in-string "\n" "\\\\n" expected-string)
+ expected-point)
+ (electric-pair-test-for ,fixture
+ ,(1+ pos)
+ ,char
+ ,expected-string
+ ,expected-point
+ ',mode
+ ,bindings
+ ,fixture-fn)))))
+
+(cl-defmacro define-electric-pair-test
+ (name fixture
+ input
+ &key
+ skip-pair-string
+ expected-string
+ expected-point
+ bindings
+ (modes '(quote (emacs-lisp-mode ruby-mode c++-mode)))
+ (test-in-comments t)
+ (test-in-strings t)
+ (test-in-code t)
+ (fixture-fn #'(lambda ()
+ (electric-pair-mode 1))))
+ `(progn
+ ,@(cl-loop
+ for mode in (eval modes) ;FIXME: avoid `eval'
+ append
+ (cl-loop
+ for (prefix suffix extra-desc) in
+ (append (if test-in-comments
+ `((,(with-temp-buffer
+ (funcall mode)
+ (insert "z")
+ (comment-region (point-min) (point-max))
+ (buffer-substring-no-properties (point-min)
+ (1- (point-max))))
+ ""
+ "-in-comments")))
+ (if test-in-strings
+ `(("\"" "\"" "-in-strings")))
+ (if test-in-code
+ `(("" "" ""))))
+ append
+ (cl-loop
+ for char across input
+ for pos from 0
+ unless (eq char ?-)
+ collect (electric-pair-define-test-form
+ name
+ fixture
+ (aref input pos)
+ pos
+ expected-string
+ expected-point
+ skip-pair-string
+ prefix
+ suffix
+ extra-desc
+ mode
+ bindings
+ fixture-fn))))))
+\f
+;;; Basic pairings and skippings
+;;;
+(define-electric-pair-test balanced-situation
+ " (()) " "(((((((" :skip-pair-string "ppppppp"
+ :modes '(ruby-mode))
+
+(define-electric-pair-test too-many-openings
+ " ((()) " "(((((((" :skip-pair-string "ppppppp")
+
+(define-electric-pair-test too-many-closings
+ " (())) " "(((((((" :skip-pair-string "------p")
+
+(define-electric-pair-test too-many-closings-2
+ "() ) " "---(---" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-3
+ ")() " "(------" :skip-pair-string "-------")
+
+(define-electric-pair-test balanced-autoskipping
+ " (()) " "---))--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test too-many-openings-autoskipping
+ " ((()) " "----))-" :skip-pair-string "-------")
+
+(define-electric-pair-test too-many-closings-autoskipping
+ " (())) " "---)))-" :skip-pair-string "---sss-")
+
+\f
+;;; Mixed parens
+;;;
+(define-electric-pair-test mixed-paren-1
+ " ()] " "-(-(---" :skip-pair-string "-p-p---")
+
+(define-electric-pair-test mixed-paren-2
+ " [() " "-(-()--" :skip-pair-string "-p-ps--")
+
+(define-electric-pair-test mixed-paren-3
+ " (]) " "-(-()--" :skip-pair-string "---ps--")
+
+(define-electric-pair-test mixed-paren-4
+ " ()] " "---)]--" :skip-pair-string "---ss--")
+
+(define-electric-pair-test mixed-paren-5
+ " [() " "----(--" :skip-pair-string "----p--")
+
+(define-electric-pair-test find-matching-different-paren-type
+ " ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test find-matching-different-paren-type-inside-list
+ "( ()]) " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test ignore-different-unmatching-paren-type
+ "( ()]) " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test autopair-keep-least-amount-of-mixed-unbalance
+ "( ()] " "-(-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test dont-autopair-to-resolve-mixed-unbalance
+ "( ()] " "-[-----" :skip-pair-string "-------")
+
+(define-electric-pair-test autopair-so-as-not-to-worsen-unbalance-situation
+ "( (]) " "-[-----" :skip-pair-string "-p-----")
+
+(define-electric-pair-test skip-over-partially-balanced
+ " [([]) " "-----)---" :skip-pair-string "-----s---")
+
+(define-electric-pair-test only-skip-over-at-least-partially-balanced-stuff
+ " [([()) " "-----))--" :skip-pair-string "-----s---")
+
+
+
+\f
+;;; Quotes
+;;;
+(define-electric-pair-test pair-some-quotes-skip-others
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test skip-single-quotes-in-ruby-mode
+ " '' " "--'-" :skip-pair-string "--s-"
+ :modes '(ruby-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone
+ " \"' " "-\"'-" :skip-pair-string "----"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-2
+ " \"\\\"' " "-\"--'-" :skip-pair-string "------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test leave-unbalanced-quotes-alone-3
+ " foo\\''" "'------" :skip-pair-string "-------"
+ :modes '(ruby-mode)
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+(define-electric-pair-test inhibit-only-if-next-is-mismatched
+ "\"foo\"\"bar" "\""
+ :expected-string "\"\"\"foo\"\"bar"
+ :expected-point 2
+ :test-in-strings nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,prog-mode-syntax-table)))
+
+\f
+;;; More quotes, but now don't bind `electric-pair-text-syntax-table'
+;;; to `prog-mode-syntax-table'. Use the defaults for
+;;; `electric-pair-pairs' and `electric-pair-text-pairs'.
+;;;
+(define-electric-pair-test pairing-skipping-quotes-in-code
+ " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
+ :test-in-strings nil
+ :test-in-comments nil)
+
+(define-electric-pair-test skipping-quotes-in-comments
+ " \"\" " "--\"-----" :skip-pair-string "--s------"
+ :test-in-strings nil)
+
+\f
+;;; Skipping over whitespace
+;;;
+(define-electric-pair-test whitespace-jumping
+ " ( ) " "--))))---" :expected-string " ( ) " :expected-point 8
+ :bindings '((electric-pair-skip-whitespace . t)))
+
+(define-electric-pair-test whitespace-chomping
+ " ( ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp)))
+
+(define-electric-pair-test whitespace-chomping-2
+ " ( \n\t\t\n ) " "--)------" :expected-string " () " :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp))
+ :test-in-comments nil)
+
+(define-electric-pair-test whitespace-chomping-dont-cross-comments
+ " ( \n\t\t\n ) " "--)------" :expected-string " () \n\t\t\n ) "
+ :expected-point 4
+ :bindings '((electric-pair-skip-whitespace . chomp))
+ :test-in-strings nil
+ :test-in-code nil
+ :test-in-comments t)
+
+\f
+;;; Pairing arbitrary characters
+;;;
+(define-electric-pair-test angle-brackets-everywhere
+ "<>" "<>" :skip-pair-string "ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(define-electric-pair-test angle-brackets-everywhere-2
+ "(<>" "-<>" :skip-pair-string "-ps"
+ :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
+
+(defvar electric-pair-test-angle-brackets-table
+ (let ((table (make-syntax-table prog-mode-syntax-table)))
+ (modify-syntax-entry ?\< "(>" table)
+ (modify-syntax-entry ?\> ")<`" table)
+ table))
+
+(define-electric-pair-test angle-brackets-pair
+ "<>" "<" :expected-string "<><>" :expected-point 2
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test angle-brackets-skip
+ "<>" "->" :expected-string "<>" :expected-point 3
+ :test-in-code nil
+ :bindings `((electric-pair-text-syntax-table
+ . ,electric-pair-test-angle-brackets-table)))
+
+(define-electric-pair-test pair-backtick-and-quote-in-comments
+ ";; " "---`" :expected-string ";; `'" :expected-point 5
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-comments
+ ";; `foo'" "-------'" :expected-string ";; `foo'" :expected-point 9
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test pair-backtick-and-quote-in-strings
+ "\"\"" "-`" :expected-string "\"`'\"" :expected-point 3
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings
+ "\"`'\"" "--'" :expected-string "\"`'\"" :expected-point 4
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+(define-electric-pair-test skip-backtick-and-quote-in-strings-2
+ " \"`'\"" "----'" :expected-string " \"`'\"" :expected-point 6
+ :test-in-comments nil
+ :test-in-strings nil
+ :modes '(emacs-lisp-mode)
+ :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
+
+\f
+;;; `js-mode' has `electric-layout-rules' for '{ and '}
+;;;
+(define-electric-pair-test js-mode-braces
+ "" "{" :expected-string "{}" :expected-point 2
+ :modes '(js-mode)
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout
+ "" "{" :expected-string "{\n\n}" :expected-point 3
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-layout-mode 1)
+ (electric-pair-mode 1)))
+
+(define-electric-pair-test js-mode-braces-with-layout-and-indent
+ "" "{" :expected-string "{\n \n}" :expected-point 7
+ :modes '(js-mode)
+ :test-in-comments nil
+ :test-in-strings nil
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (electric-layout-mode 1)))
+
+\f
+;;; Backspacing
+;;; TODO: better tests
+;;;
+(ert-deftest electric-pair-backspace-1 ()
+ (save-electric-modes
+ (with-temp-buffer
+ (insert "()")
+ (goto-char 2)
+ (electric-pair-backward-delete-char 1)
+ (should (equal "" (buffer-string))))))
+
+\f
+;;; Electric newlines between pairs
+;;; TODO: better tests
+(ert-deftest electric-pair-open-extra-newline ()
+ (save-electric-modes
+ (with-temp-buffer
+ (c-mode)
+ (electric-pair-mode 1)
+ (electric-indent-mode 1)
+ (insert "int main {}")
+ (backward-char 1)
+ (let ((c-basic-offset 4))
+ (newline 1 t)
+ (should (equal "int main {\n \n}"
+ (buffer-string)))
+ (should (equal (point) (- (point-max) 2)))))))
+
+
+\f
+;;; Autowrapping
+;;;
+(define-electric-pair-test autowrapping-1
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-2
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-3
+ "foo" ")" :expected-string "(foo)" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-4
+ "foo" "(" :expected-string "(foo)" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(define-electric-pair-test autowrapping-5
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 2
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (mark-sexp 1)))
+
+(define-electric-pair-test autowrapping-6
+ "foo" "\"" :expected-string "\"foo\"" :expected-point 6
+ :fixture-fn #'(lambda ()
+ (electric-pair-mode 1)
+ (goto-char (point-max))
+ (skip-chars-backward "\"")
+ (mark-sexp -1)))
+
+(provide 'electric-pair-tests)
+;;; electric-pair-tests.el ends here
^ permalink raw reply related [flat|nested] 36+ messages in thread
* Re: [patch] make electric-pair-mode smarter/more useful
2013-12-23 14:41 ` João Távora
@ 2013-12-24 14:29 ` Bozhidar Batsov
0 siblings, 0 replies; 36+ messages in thread
From: Bozhidar Batsov @ 2013-12-24 14:29 UTC (permalink / raw
To: João Távora; +Cc: emacs-devel, Stefan Monnier, Dmitry Gutov
[-- Attachment #1: Type: text/plain, Size: 58559 bytes --]
Not sure if Stefan saw your message here, but he mentioned in the "feature
freeze" thread that your patch is approved and can be installed. Have a
look at it.
On 23 December 2013 16:41, João Távora <joaotavora@gmail.com> wrote:
>
> Stefan,
>
> Don't want to be pushy, but is there anything holding up this patch?
>
> Here's yet another version that fixes some more details:
>
> - fixed default values for `electric-pair-inhibit-predicate' and
> `electric-pair-skip-self'.
>
> - electric-pair-mode's docstring doesn't mention related customization
> variables because there are now too many of them.
>
> - emacs-lisp/lisp-mode.el locally sets
> `electric-pair-open-newline-between-pairs' to t.
>
> - the newline call in `newline-and-indent` now uses interactive set to
> t
>
> Thanks
> João
>
> diff --git a/lisp/ChangeLog b/lisp/ChangeLog
> index 12889de..0564484 100644
> --- a/lisp/ChangeLog
> +++ b/lisp/ChangeLog
> @@ -1,3 +1,25 @@
> +2013-12-99 João Távora <joaotavora@gmail.com>
> +
> + * electric.el (electric-pair-mode): More flexible engine for skip-
> + and inhibit predicates, new options for pairing-related
> + functionality.
> + (electric-pair-preserve-balance): Pair/skip parentheses and quotes
> + if that keeps or improves their balance in buffers.
> + (electric-pair-delete-adjacent-pairs): Delete the pair when
> + backspacing over adjacent matched delimiters.
> + (electric-pair-open-extra-newline): Open extra newline when
> + inserting newlines between adjacent matched delimiters.
> + (electric--sort-post-self-insertion-hook): Sort
> + post-self-insert-hook according to priority values when
> + minor-modes are activated.
> + * simple.el (newline-and-indent): Call newline with interactive
> + set to t.
> + (blink-paren-post-self-insert-function): Set priority to 100.
> + * emacs-lisp/lisp-mode.el (lisp-mode-variables): Use
> + electric-pair-text-pairs to pair backtick-and-quote in strings and
> + comments. Locally set electric-pair-skip-whitespace to 'chomp and
> + electric-pair-open-newline-between-pairs to nil.
> +
> 2013-12-23 Chong Yidong <cyd@gnu.org>
>
> * subr.el (set-transient-map): Rename from
> diff --git a/lisp/electric.el b/lisp/electric.el
> index 91b99b4..ca4616b 100644
> --- a/lisp/electric.el
> +++ b/lisp/electric.el
> @@ -187,6 +187,17 @@ Returns nil when we can't find this char."
> (eq (char-before) last-command-event)))))
> pos)))
>
> +(defun electric--sort-post-self-insertion-hook ()
> + "Ensure order of electric functions in `post-self-insertion-hook'.
> +
> +Hooks in this variable interact in non-trivial ways, so a
> +relative order must be maintained within it."
> + (setq-default post-self-insert-hook
> + (sort (default-value 'post-self-insert-hook)
> + #'(lambda (fn1 fn2)
> + (< (or (get fn1 'priority) 0)
> + (or (get fn2 'priority) 0))))))
> +
> ;;; Electric indentation.
>
> ;; Autoloading variables is generally undesirable, but major modes
> @@ -267,6 +278,8 @@ mode set `electric-indent-inhibit', but this can be
> used as a workaround.")
> (> pos (line-beginning-position)))
> (indent-according-to-mode)))))
>
> +(put 'electric-indent-post-self-insert-function 'priority 60)
> +
> (defun electric-indent-just-newline (arg)
> "Insert just a newline, without any auto-indentation."
> (interactive "*P")
> @@ -295,20 +308,9 @@ insert a character from `electric-indent-chars'."
> #'electric-indent-post-self-insert-function))
> (when (eq (lookup-key global-map [?\C-j]) 'newline-and-indent)
> (define-key global-map [?\C-j] 'electric-indent-just-newline))
> - ;; post-self-insert-hooks interact in non-trivial ways.
> - ;; It turns out that electric-indent-mode generally works better if
> run
> - ;; late, but still before blink-paren.
> (add-hook 'post-self-insert-hook
> - #'electric-indent-post-self-insert-function
> - 'append)
> - ;; FIXME: Ugly!
> - (let ((bp (memq #'blink-paren-post-self-insert-function
> - (default-value 'post-self-insert-hook))))
> - (when (memq #'electric-indent-post-self-insert-function bp)
> - (setcar bp #'electric-indent-post-self-insert-function)
> - (setcdr bp (cons #'blink-paren-post-self-insert-function
> - (delq #'electric-indent-post-self-insert-function
> - (cdr bp))))))))
> + #'electric-indent-post-self-insert-function)
> + (electric--sort-post-self-insertion-hook)))
>
> ;;;###autoload
> (define-minor-mode electric-indent-local-mode
> @@ -327,32 +329,163 @@ insert a character from `electric-indent-chars'."
>
> (defcustom electric-pair-pairs
> '((?\" . ?\"))
> - "Alist of pairs that should be used regardless of major mode."
> + "Alist of pairs that should be used regardless of major mode.
> +
> +Pairs of delimiters in this list are a fallback in case they have
> +no syntax relevant to `electric-pair-mode' in the mode's syntax
> +table.
> +
> +See also the variable `electric-pair-text-pairs'."
> :version "24.1"
> :type '(repeat (cons character character)))
>
> -(defcustom electric-pair-skip-self t
> +(defcustom electric-pair-text-pairs
> + '((?\" . ?\" ))
> + "Alist of pairs that should always be used in comments and strings.
> +
> +Pairs of delimiters in this list are a fallback in case they have
> +no syntax relevant to `electric-pair-mode' in the syntax table
> +defined in `electric-pair-text-syntax-table'"
> + :version "24.4"
> + :type '(repeat (cons character character)))
> +
> +(defcustom electric-pair-skip-self #'electric-pair-default-skip-self
> "If non-nil, skip char instead of inserting a second closing paren.
> +
> 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."
> +
> +This can be convenient for people who find it easier to hit ) than C-f.
> +
> +Can also be a function of one argument (the closer char just
> +inserted), in which case that function's return value is
> +considered instead."
> :version "24.1"
> - :type 'boolean)
> + :type '(choice
> + (const :tag "Never skip" nil)
> + (const :tag "Help balance" electric-pair-default-skip-self)
> + (const :tag "Always skip" t)
> + function))
>
> (defcustom electric-pair-inhibit-predicate
> #'electric-pair-default-inhibit
> "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-default-inhibit)
> (const :tag "Always pair" ignore)
> function))
>
> -(defun electric-pair-default-inhibit (char)
> +(defcustom electric-pair-preserve-balance t
> + "Non-nil if default pairing and skipping should help balance
> parentheses.
> +
> +The default values of `electric-pair-inhibit-predicate' and
> +`electric-pair-skip-self' check this variable before delegating to other
> +predicates reponsible for making decisions on whether to pair/skip some
> +characters based on the actual state of the buffer's parenthesis and
> +quotes."
> + :version "24.4"
> + :type 'boolean)
> +
> +(defcustom electric-pair-delete-adjacent-pairs t
> + "If non-nil, backspacing an open paren also deletes adjacent closer.
> +
> +Can also be a function of no arguments, in which case that function's
> +return value is considered instead."
> + :version "24.4"
> + :type '(choice
> + (const :tag "Yes" t)
> + (const :tag "No" nil)
> + function))
> +
> +(defcustom electric-pair-open-newline-between-pairs t
> + "If non-nil, a newline between adjacent parentheses opens an extra one.
> +
> +Can also be a function of no arguments, in which case that function's
> +return value is considered instead."
> + :version "24.4"
> + :type '(choice
> + (const :tag "Yes" t)
> + (const :tag "No" nil)
> + function))
> +
> +(defcustom electric-pair-skip-whitespace t
> + "If non-nil skip whitespace when skipping over closing parens.
> +
> +The specific kind of whitespace skipped is given by the variable
> +`electric-pair-skip-whitespace-chars'.
> +
> +The symbol `chomp' specifies that the skipped-over whitespace
> +should be deleted.
> +
> +Can also be a function of no arguments, in which case that function's
> +return value is considered instead."
> + :version "24.4"
> + :type '(choice
> + (const :tag "Yes, jump over whitespace" t)
> + (const :tag "Yes, and delete whitespace" 'chomp)
> + (const :tag "No, no whitespace skipping" nil)
> + function))
> +
> +(defcustom electric-pair-skip-whitespace-chars (list ?\t ?\s ?\n)
> + "Whitespace characters considered by `electric-pair-skip-whitespace'."
> + :version "24.4"
> + :type '(choice (set (const :tag "Space" ?\s)
> + (const :tag "Tab" ?\t)
> + (const :tag "Newline" ?\n))
> + (list character)))
> +
> +(defun electric-pair--skip-whitespace ()
> + "Skip whitespace forward, not crossing comment or string boundaries."
> + (let ((saved (point))
> + (string-or-comment (nth 8 (syntax-ppss))))
> + (skip-chars-forward (apply #'string
> electric-pair-skip-whitespace-chars))
> + (unless (eq string-or-comment (nth 8 (syntax-ppss)))
> + (goto-char saved))))
> +
> +(defvar electric-pair-text-syntax-table prog-mode-syntax-table
> + "Syntax table used when pairing inside comments and strings.
> +
> +`electric-pair-mode' considers this syntax table only when point in inside
> +quotes or comments. If lookup fails here, `electric-pair-text-pairs' will
> +be considered.")
> +
> +(defun electric-pair-backward-delete-char (n &optional killflag untabify)
> + "Delete characters backward, and maybe also two adjacent paired
> delimiters.
> +
> +Remaining behaviour is given by `backward-delete-char' or, if UNTABIFY is
> +non-nil, `backward-delete-char-untabify'."
> + (interactive "*p\nP")
> + (let* ((prev (char-before))
> + (next (char-after))
> + (syntax-info (electric-pair-syntax-info prev))
> + (syntax (car syntax-info))
> + (pair (cadr syntax-info)))
> + (when (and (if (functionp electric-pair-delete-adjacent-pairs)
> + (funcall electric-pair-delete-adjacent-pairs)
> + electric-pair-delete-adjacent-pairs)
> + next
> + (memq syntax '(?\( ?\" ?\$))
> + (eq pair next))
> + (delete-char 1 killflag))
> + (if untabify
> + (backward-delete-char-untabify n killflag)
> + (backward-delete-char n killflag))))
> +
> +(defun electric-pair-backward-delete-char-untabify (n &optional killflag)
> + "Delete characters backward, and maybe also two adjacent paired
> delimiters.
> +
> +Remaining behaviour is given by `backward-delete-char-untabify'."
> + (interactive "*p\nP")
> + (electric-pair-backward-delete-char n killflag t))
> +
> +(defun electric-pair-conservative-inhibit (char)
> (or
> ;; I find it more often preferable not to pair when the
> ;; same char is next.
> @@ -363,14 +496,40 @@ closer."
> ;; I also find it often preferable not to pair next to a word.
> (eq (char-syntax (following-char)) ?w)))
>
> -(defun electric-pair-syntax (command-event)
> - (let ((x (assq command-event electric-pair-pairs)))
> +(defun electric-pair-syntax-info (command-event)
> + "Calculate a list (SYNTAX PAIR UNCONDITIONAL STRING-OR-COMMENT-START).
> +
> +SYNTAX is COMMAND-EVENT's syntax character. PAIR is
> +COMMAND-EVENT's pair. UNCONDITIONAL indicates the variables
> +`electric-pair-pairs' or `electric-pair-text-pairs' were used to
> +lookup syntax. STRING-OR-COMMENT-START indicates that point is
> +inside a comment of string."
> + (let* ((pre-string-or-comment (nth 8 (save-excursion
> + (syntax-ppss (1- (point))))))
> + (post-string-or-comment (nth 8 (syntax-ppss (point))))
> + (string-or-comment (and post-string-or-comment
> + pre-string-or-comment))
> + (table (if string-or-comment
> + electric-pair-text-syntax-table
> + (syntax-table)))
> + (table-syntax-and-pair (with-syntax-table table
> + (list (char-syntax command-event)
> + (or (matching-paren command-event)
> + command-event))))
> + (fallback (if string-or-comment
> + (append electric-pair-text-pairs
> + electric-pair-pairs)
> + electric-pair-pairs))
> + (direct (assq command-event fallback))
> + (reverse (rassq command-event fallback)))
> (cond
> - (x (if (eq (car x) (cdr x)) ?\" ?\())
> - ((rassq command-event electric-pair-pairs) ?\))
> - ((nth 8 (syntax-ppss))
> - (with-syntax-table text-mode-syntax-table (char-syntax
> command-event)))
> - (t (char-syntax command-event)))))
> + ((memq (car table-syntax-and-pair)
> + '(?\" ?\( ?\) ?\$))
> + (append table-syntax-and-pair (list nil string-or-comment)))
> + (direct (if (eq (car direct) (cdr direct))
> + (list ?\" command-event t string-or-comment)
> + (list ?\( (cdr direct) t string-or-comment)))
> + (reverse (list ?\) (car reverse) t string-or-comment)))))
>
> (defun electric-pair--insert (char)
> (let ((last-command-event char)
> @@ -378,56 +537,286 @@ closer."
> (electric-pair-mode nil))
> (self-insert-command 1)))
>
> +(defun electric-pair--syntax-ppss (&optional pos where)
> + "Like `syntax-ppss', but sometimes fallback to `parse-partial-sexp'.
> +
> +WHERE is list defaulting to '(string comment) and indicates
> +when to fallback to `parse-partial-sexp'."
> + (let* ((pos (or pos (point)))
> + (where (or where '(string comment)))
> + (quick-ppss (syntax-ppss))
> + (quick-ppss-at-pos (syntax-ppss pos)))
> + (if (or (and (nth 3 quick-ppss) (memq 'string where))
> + (and (nth 4 quick-ppss) (memq 'comment where)))
> + (with-syntax-table electric-pair-text-syntax-table
> + (parse-partial-sexp (1+ (nth 8 quick-ppss)) pos))
> + ;; HACK! cc-mode apparently has some `syntax-ppss' bugs
> + (if (memq major-mode '(c-mode c++ mode))
> + (parse-partial-sexp (point-min) pos)
> + quick-ppss-at-pos))))
> +
> +;; Balancing means controlling pairing and skipping of parentheses so
> +;; that, if possible, the buffer ends up at least as balanced as
> +;; before, if not more. The algorithm is slightly complex because some
> +;; situations like "()))" need pairing to occur at the end but not at
> +;; the beginning. Balancing should also happen independently for
> +;; different types of parentheses, so that having your {}'s unbalanced
> +;; doesn't keep `electric-pair-mode' from balancing your ()'s and your
> +;; []'s.
> +(defun electric-pair--balance-info (direction string-or-comment)
> + "Examine lists forward or backward according to DIRECTIONS's sign.
> +
> +STRING-OR-COMMENT is info suitable for running `parse-partial-sexp'.
> +
> +Return a cons of two descritions (MATCHED-P . PAIR) for the
> +innermost and outermost lists that enclose point. The outermost
> +list enclosing point is either the first top-level or first
> +mismatched list found by uplisting.
> +
> +If the outermost list is matched, don't rely on its PAIR. If
> +point is not enclosed by any lists, return ((T) (T))."
> + (let* (innermost
> + outermost
> + (table (if string-or-comment
> + electric-pair-text-syntax-table
> + (syntax-table)))
> + (at-top-level-or-equivalent-fn
> + ;; called when `scan-sexps' ran perfectly, when when it
> + ;; found a parenthesis pointing in the direction of
> + ;; travel. Also when travel started inside a comment and
> + ;; exited it
> + #'(lambda ()
> + (setq outermost (list t))
> + (unless innermost
> + (setq innermost (list t)))))
> + (ended-prematurely-fn
> + ;; called when `scan-sexps' crashed against a parenthesis
> + ;; pointing opposite the direction of travel. After
> + ;; traversing that character, the idea is to travel one sexp
> + ;; in the opposite direction looking for a matching
> + ;; delimiter.
> + #'(lambda ()
> + (let* ((pos (point))
> + (matched
> + (save-excursion
> + (cond ((< direction 0)
> + (condition-case nil
> + (eq (char-after pos)
> + (with-syntax-table table
> + (matching-paren
> + (char-before
> + (scan-sexps (point) 1)))))
> + (scan-error nil)))
> + (t
> + ;; In this case, no need to use
> + ;; `scan-sexps', we can use some
> + ;; `electric-pair--syntax-ppss' in this
> + ;; case (which uses the quicker
> + ;; `syntax-ppss' in some cases)
> + (let* ((ppss (electric-pair--syntax-ppss
> + (1- (point))))
> + (start (car (last (nth 9 ppss))))
> + (opener (char-after start)))
> + (and start
> + (eq (char-before pos)
> + (or (with-syntax-table table
> + (matching-paren opener))
> + opener))))))))
> + (actual-pair (if (> direction 0)
> + (char-before (point))
> + (char-after (point)))))
> + (unless innermost
> + (setq innermost (cons matched actual-pair)))
> + (unless matched
> + (setq outermost (cons matched actual-pair)))))))
> + (save-excursion
> + (while (not outermost)
> + (condition-case err
> + (with-syntax-table table
> + (scan-sexps (point) (if (> direction 0)
> + (point-max)
> + (- (point-max))))
> + (funcall at-top-level-or-equivalent-fn))
> + (scan-error
> + (cond ((or
> + ;; some error happened and it is not of the "ended
> + ;; prematurely" kind"...
> + (not (string-match "ends prematurely" (nth 1 err)))
> + ;; ... or we were in a comment and just came out of
> + ;; it.
> + (and string-or-comment
> + (not (nth 8 (syntax-ppss)))))
> + (funcall at-top-level-or-equivalent-fn))
> + (t
> + ;; exit the sexp
> + (goto-char (nth 3 err))
> + (funcall ended-prematurely-fn)))))))
> + (cons innermost outermost)))
> +
> +(defun electric-pair--looking-at-unterminated-string-p (char)
> + "Say if following string starts with CHAR and is unterminated."
> + ;; FIXME: ugly/naive
> + (save-excursion
> + (skip-chars-forward (format "^%c" char))
> + (while (not (zerop (% (save-excursion (skip-syntax-backward "\\"))
> 2)))
> + (unless (eobp)
> + (forward-char 1)
> + (skip-chars-forward (format "^%c" char))))
> + (and (not (eobp))
> + (condition-case err
> + (progn (forward-sexp) nil)
> + (scan-error t)))))
> +
> +(defun electric-pair--inside-string-p (char)
> + "Say if point is inside a string started by CHAR.
> +
> +A comments text is parsed with `electric-pair-text-syntax-table'.
> +Also consider strings within comments, but not strings within
> +strings."
> + ;; FIXME: could also consider strings within strings by examining
> + ;; delimiters.
> + (let* ((ppss (electric-pair--syntax-ppss (point) '(comment))))
> + (memq (nth 3 ppss) (list t char))))
> +
> +(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, finally restoring the situation as if nothing
> +happened."
> + (pcase (electric-pair-syntax-info char)
> + (`(,syntax ,pair ,_ ,s-or-c)
> + (unwind-protect
> + (progn
> + (delete-char -1)
> + (cond ((eq ?\( syntax)
> + (let* ((pair-data
> + (electric-pair--balance-info 1 s-or-c))
> + (innermost (car pair-data))
> + (outermost (cdr pair-data)))
> + (cond ((car outermost)
> + nil)
> + (t
> + (eq (cdr outermost) pair)))))
> + ((eq syntax ?\")
> + (electric-pair--looking-at-unterminated-string-p
> 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, finally restoring the situation as if nothing
> +happened."
> + (pcase (electric-pair-syntax-info char)
> + (`(,syntax ,pair ,_ ,s-or-c)
> + (unwind-protect
> + (progn
> + (delete-char -1)
> + (cond ((eq syntax ?\))
> + (let* ((pair-data
> + (electric-pair--balance-info
> + -1 s-or-c))
> + (innermost (car pair-data))
> + (outermost (cdr pair-data)))
> + (and
> + (cond ((car outermost)
> + (car innermost))
> + ((car innermost)
> + (not (eq (cdr outermost) pair)))))))
> + ((eq syntax ?\")
> + (electric-pair--inside-string-p char))))
> + (insert-char char)))))
> +
> +(defun electric-pair-default-skip-self (char)
> + (if electric-pair-preserve-balance
> + (electric-pair-skip-if-helps-balance char)
> + t))
> +
> +(defun electric-pair-default-inhibit (char)
> + (if electric-pair-preserve-balance
> + (electric-pair-inhibit-if-helps-balance char)
> + (electric-pair-conservative-inhibit 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)))
> - (closer (if (eq syntax ?\()
> - (cdr (or (assq last-command-event
> electric-pair-pairs)
> - (aref (syntax-table) last-command-event)))
> - last-command-event)))
> - (cond
> - ((null pos) nil)
> - ;; Wrap a pair around the active region.
> - ((and (memq syntax '(?\( ?\" ?\$)) (use-region-p))
> - ;; FIXME: To do this right, we'd need a post-self-insert-function
> - ;; so we could add-function around it and insert the closer after
> - ;; all the rest of the hook has run.
> - (if (>= (mark) (point))
> - (goto-char (mark))
> - ;; We already inserted the open-paren but at the end of the
> - ;; region, so we have to remove it and start over.
> - (delete-region (1- pos) (point))
> - (save-excursion
> - (goto-char (mark))
> - (electric-pair--insert last-command-event)))
> - ;; Since we're right after the closer now, we could tell the rest of
> - ;; post-self-insert-hook that we inserted `closer', but then we'd
> get
> - ;; blink-paren to kick in, which is annoying.
> - ;;(setq last-command-event closer)
> - (insert closer))
> - ;; Backslash-escaped: no pairing, no skipping.
> - ((save-excursion
> - (goto-char (1- pos))
> - (not (zerop (% (skip-syntax-backward "\\") 2))))
> - nil)
> - ;; Skip self.
> - ((and (memq syntax '(?\) ?\" ?\$))
> - 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
> - ;; undo-log and in the intermediate state which might be visible to
> other
> - ;; post-self-insert-hook. We'll just have to live with it for now.
> - (delete-char 1))
> - ;; Insert matching pair.
> - ((not (or (not (memq syntax `(?\( ?\" ?\$)))
> - overwrite-mode
> - (funcall electric-pair-inhibit-predicate
> last-command-event)))
> - (save-excursion (electric-pair--insert closer))))))
> + (skip-whitespace-info))
> + (pcase (electric-pair-syntax-info last-command-event)
> + (`(,syntax ,pair ,unconditional ,_)
> + (cond
> + ((null pos) nil)
> + ;; Wrap a pair around the active region.
> + ;;
> + ((and (memq syntax '(?\( ?\) ?\" ?\$)) (use-region-p))
> + ;; FIXME: To do this right, we'd need a post-self-insert-function
> + ;; so we could add-function around it and insert the closer after
> + ;; all the rest of the hook has run.
> + (if (or (eq syntax ?\")
> + (and (eq syntax ?\))
> + (>= (point) (mark)))
> + (and (not (eq syntax ?\)))
> + (>= (mark) (point))))
> + (save-excursion
> + (goto-char (mark))
> + (electric-pair--insert pair))
> + (delete-region pos (1- pos))
> + (electric-pair--insert pair)
> + (goto-char (mark))
> + (electric-pair--insert last-command-event)))
> + ;; Backslash-escaped: no pairing, no skipping.
> + ((save-excursion
> + (goto-char (1- pos))
> + (not (zerop (% (skip-syntax-backward "\\") 2))))
> + nil)
> + ;; Skip self.
> + ((and (memq syntax '(?\) ?\" ?\$))
> + (and (or unconditional
> + (if (functionp electric-pair-skip-self)
> + (funcall electric-pair-skip-self
> last-command-event)
> + electric-pair-skip-self))
> + (save-excursion
> + (when (setq skip-whitespace-info
> + (if (functionp
> electric-pair-skip-whitespace)
> + (funcall
> electric-pair-skip-whitespace)
> + electric-pair-skip-whitespace))
> + (electric-pair--skip-whitespace))
> + (eq (char-after) 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 undo-log and in the intermediate state which might
> + ;; be visible to other post-self-insert-hook. We'll just have to
> + ;; live with it for now.
> + (when skip-whitespace-info
> + (electric-pair--skip-whitespace))
> + (delete-region (1- pos) (if (eq skip-whitespace-info 'chomp)
> + (point)
> + pos))
> + (forward-char))
> + ;; Insert matching pair.
> + ((and (memq syntax `(?\( ?\" ?\$))
> + (not overwrite-mode)
> + (or unconditional
> + (not (funcall electric-pair-inhibit-predicate
> + last-command-event))))
> + (save-excursion (electric-pair--insert pair)))))
> + (t
> + (when (and (if (functionp electric-pair-open-newline-between-pairs)
> + (funcall electric-pair-open-newline-between-pairs)
> + electric-pair-open-newline-between-pairs)
> + (eq last-command-event ?\n)
> + (not (eobp))
> + (eq (save-excursion
> + (skip-chars-backward "\t\s")
> + (char-before (1- (point))))
> + (matching-paren (char-after))))
> + (save-excursion (newline 1 t)))))))
> +
> +(put 'electric-pair-post-self-insert-function 'priority 20)
>
> (defun electric-pair-will-use-region ()
> (and (use-region-p)
> - (memq (electric-pair-syntax last-command-event) '(?\( ?\" ?\$))))
> + (memq (car (electric-pair-syntax-info last-command-event))
> + '(?\( ?\) ?\" ?\$))))
>
> ;;;###autoload
> (define-minor-mode electric-pair-mode
> @@ -438,29 +827,44 @@ the mode if ARG is omitted or nil.
>
> Electric Pair mode is a global minor mode. When enabled, typing
> an open parenthesis automatically inserts the corresponding
> -closing parenthesis. \(Likewise for brackets, etc.)
> -
> -See options `electric-pair-pairs' and `electric-pair-skip-self'."
> +closing parenthesis. \(Likewise for brackets, etc.)."
> :global t :group 'electricity
> (if electric-pair-mode
> (progn
> (add-hook 'post-self-insert-hook
> #'electric-pair-post-self-insert-function)
> + (electric--sort-post-self-insertion-hook)
> (add-hook 'self-insert-uses-region-functions
> #'electric-pair-will-use-region))
> (remove-hook 'post-self-insert-hook
> #'electric-pair-post-self-insert-function)
> (remove-hook 'self-insert-uses-region-functions
> - #'electric-pair-will-use-region)))
> + #'electric-pair-will-use-region)))
> +
> +(defvar electric-pair-mode-map
> + (let ((map (make-sparse-keymap)))
> + (define-key map [remap backward-delete-char-untabify]
> + 'electric-pair-backward-delete-char-untabify)
> + (define-key map [remap backward-delete-char]
> + 'electric-pair-backward-delete-char)
> + (define-key map [remap delete-backward-char]
> + 'electric-pair-backward-delete-char)
> + map)
> + "Keymap used by `electric-pair-mode'.")
>
> ;;; Electric newlines after/before/around some chars.
>
> -(defvar electric-layout-rules '()
> +(defvar electric-layout-rules nil
> "List of rules saying where to automatically insert newlines.
> -Each rule has the form (CHAR . WHERE) where CHAR is the char
> -that was just inserted and WHERE specifies where to insert newlines
> -and can be: nil, `before', `after', `around', or a function of no
> -arguments that returns one of those symbols.")
> +
> +Each rule has the form (CHAR . WHERE) where CHAR is the char that
> +was just inserted and WHERE specifies where to insert newlines
> +and can be: nil, `before', `after', `around', `after-stay', or a
> +function of no arguments that returns one of those symbols.
> +
> +The symbols specify where in relation to CHAR the newline
> +character(s) should be inserted. `after-stay' means insert a
> +newline after CHAR but stay in the same place.")
>
> (defun electric-layout-post-self-insert-function ()
> (let* ((rule (cdr (assq last-command-event electric-layout-rules)))
> @@ -469,23 +873,32 @@ arguments that returns one of those symbols.")
> (setq pos (electric--after-char-pos))
> ;; Not in a string or comment.
> (not (nth 8 (save-excursion (syntax-ppss pos)))))
> - (let ((end (copy-marker (point) t)))
> + (let ((end (copy-marker (point)))
> + (sym (if (functionp rule) (funcall rule) rule)))
> + (set-marker-insertion-type end (not (eq sym 'after-stay)))
> (goto-char pos)
> - (pcase (if (functionp rule) (funcall rule) rule)
> + (pcase sym
> ;; FIXME: we used `newline' down here which called
> ;; self-insert-command and ran post-self-insert-hook
> recursively.
> ;; It happened to make electric-indent-mode work automatically
> with
> ;; electric-layout-mode (at the cost of re-indenting lines
> ;; multiple times), but I'm not sure it's what we want.
> + ;;
> + ;; FIXME: check eolp before inserting \n?
> (`before (goto-char (1- pos)) (skip-chars-backward " \t")
> - (unless (bolp) (insert "\n")))
> - (`after (insert "\n")) ; FIXME: check eolp before
> inserting \n?
> + (unless (bolp) (insert "\n")))
> + (`after (insert "\n"))
> + (`after-stay (save-excursion
> + (let ((electric-layout-rules nil))
> + (newline 1 t))))
> (`around (save-excursion
> - (goto-char (1- pos)) (skip-chars-backward " \t")
> - (unless (bolp) (insert "\n")))
> - (insert "\n"))) ; FIXME: check eolp before
> inserting \n?
> + (goto-char (1- pos)) (skip-chars-backward " \t")
> + (unless (bolp) (insert "\n")))
> + (insert "\n"))) ; FIXME: check eolp before
> inserting \n?
> (goto-char end)))))
>
> +(put 'electric-layout-post-self-insert-function 'priority 40)
> +
> ;;;###autoload
> (define-minor-mode electric-layout-mode
> "Automatically insert newlines around some chars.
> @@ -494,11 +907,13 @@ positive, and disable it otherwise. If called from
> Lisp, enable
> the mode if ARG is omitted or nil.
> The variable `electric-layout-rules' says when and how to insert
> newlines."
> :global t :group 'electricity
> - (if electric-layout-mode
> - (add-hook 'post-self-insert-hook
> - #'electric-layout-post-self-insert-function)
> - (remove-hook 'post-self-insert-hook
> - #'electric-layout-post-self-insert-function)))
> + (cond (electric-layout-mode
> + (add-hook 'post-self-insert-hook
> + #'electric-layout-post-self-insert-function)
> + (electric--sort-post-self-insertion-hook))
> + (t
> + (remove-hook 'post-self-insert-hook
> + #'electric-layout-post-self-insert-function))))
>
> (provide 'electric)
>
> diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
> index b7bd33f..f1eae18 100644
> --- a/lisp/emacs-lisp/lisp-mode.el
> +++ b/lisp/emacs-lisp/lisp-mode.el
> @@ -472,7 +472,13 @@ font-lock keywords will not be case sensitive."
> (font-lock-mark-block-function . mark-defun)
> (font-lock-syntactic-face-function
> . lisp-font-lock-syntactic-face-function)))
> - (setq-local prettify-symbols-alist lisp--prettify-symbols-alist))
> + (setq-local prettify-symbols-alist lisp--prettify-symbols-alist)
> + ;; electric
> + (when elisp
> + (setq-local electric-pair-text-pairs
> + (cons '(?\` . ?\') electric-pair-text-pairs)))
> + (setq-local electric-pair-skip-whitespace 'chomp)
> + (setq-local electric-pair-open-newline-between-pairs nil))
>
> (defun lisp-outline-level ()
> "Lisp mode `outline-level' function."
> diff --git a/lisp/simple.el b/lisp/simple.el
> index a654351..624d87f 100644
> --- a/lisp/simple.el
> +++ b/lisp/simple.el
> @@ -610,7 +610,7 @@ In some text modes, where TAB inserts a tab, this
> command indents to the
> column specified by the function `current-left-margin'."
> (interactive "*")
> (delete-horizontal-space t)
> - (newline)
> + (newline 1 t)
> (indent-according-to-mode))
>
> (defun reindent-then-newline-and-indent ()
> @@ -6448,10 +6448,14 @@ More precisely, a char with closeparen syntax is
> self-inserted.")
> (point))))))
> (funcall blink-paren-function)))
>
> +(put 'blink-paren-post-self-insert-function 'priority 100)
> +
> (add-hook 'post-self-insert-hook #'blink-paren-post-self-insert-function
> ;; Most likely, this hook is nil, so this arg doesn't matter,
> ;; but I use it as a reminder that this function usually
> - ;; likes to be run after others since it does `sit-for'.
> + ;; likes to be run after others since it does
> + ;; `sit-for'. That's also the reason it get a `priority' prop
> + ;; of 100.
> 'append)
>
> ;; This executes C-g typed while Emacs is waiting for a command.
> diff --git a/test/automated/electric-tests.el
> b/test/automated/electric-tests.el
> new file mode 100644
> index 0000000..aa4a063
> --- /dev/null
> +++ b/test/automated/electric-tests.el
> @@ -0,0 +1,509 @@
> +;;; electric-tests.el --- tests for electric.el
> +
> +;; Copyright (C) 2013 João Távora
> +
> +;; Author: João Távora <joaotavora@gmail.com>
> +;; Keywords:
> +
> +;; 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 <http://www.gnu.org/licenses/>.
> +
> +;;; Commentary:
> +
> +;;
> +
> +;;; Code:
> +(require 'ert)
> +(require 'ert-x)
> +(require 'electric)
> +(require 'cl-lib)
> +
> +(defun call-with-saved-electric-modes (fn)
> + (let ((saved-electric (if electric-pair-mode 1 -1))
> + (saved-layout (if electric-layout-mode 1 -1))
> + (saved-indent (if electric-indent-mode 1 -1)))
> + (electric-pair-mode -1)
> + (electric-layout-mode -1)
> + (electric-indent-mode -1)
> + (unwind-protect
> + (funcall fn)
> + (electric-pair-mode saved-electric)
> + (electric-indent-mode saved-indent)
> + (electric-layout-mode saved-layout))))
> +
> +(defmacro save-electric-modes (&rest body)
> + (declare (indent defun) (debug t))
> + `(call-with-saved-electric-modes #'(lambda () ,@body)))
> +
> +(defun electric-pair-test-for (fixture where char expected-string
> + expected-point mode bindings
> fixture-fn)
> + (with-temp-buffer
> + (funcall mode)
> + (insert fixture)
> + (save-electric-modes
> + (let ((last-command-event char))
> + (goto-char where)
> + (funcall fixture-fn)
> + (cl-progv
> + (mapcar #'car bindings)
> + (mapcar #'cdr bindings)
> + (self-insert-command 1))))
> + (should (equal (buffer-substring-no-properties (point-min)
> (point-max))
> + expected-string))
> + (should (equal (point)
> + expected-point))))
> +
> +(eval-when-compile
> + (defun electric-pair-define-test-form (name fixture
> + char
> + pos
> + expected-string
> + expected-point
> + skip-pair-string
> + prefix
> + suffix
> + extra-desc
> + mode
> + bindings
> + fixture-fn)
> + (let* ((expected-string-and-point
> + (if skip-pair-string
> + (with-temp-buffer
> + (cl-progv
> + ;; FIXME: avoid `eval'
> + (mapcar #'car (eval bindings))
> + (mapcar #'cdr (eval bindings))
> + (funcall mode)
> + (insert fixture)
> + (goto-char (1+ pos))
> + (insert char)
> + (cond ((eq (aref skip-pair-string pos)
> + ?p)
> + (insert (cadr (electric-pair-syntax-info
> char)))
> + (backward-char 1))
> + ((eq (aref skip-pair-string pos)
> + ?s)
> + (delete-char -1)
> + (forward-char 1)))
> + (list
> + (buffer-substring-no-properties (point-min)
> (point-max))
> + (point))))
> + (list expected-string expected-point)))
> + (expected-string (car expected-string-and-point))
> + (expected-point (cadr expected-string-and-point))
> + (fixture (format "%s%s%s" prefix fixture suffix))
> + (expected-string (format "%s%s%s" prefix expected-string
> suffix))
> + (expected-point (+ (length prefix) expected-point))
> + (pos (+ (length prefix) pos)))
> + `(ert-deftest ,(intern (format
> "electric-pair-%s-at-point-%s-in-%s%s"
> + name
> + (1+ pos)
> + mode
> + extra-desc))
> + ()
> + ,(format "With \"%s\", try input %c at point %d. \
> +Should %s \"%s\" and point at %d"
> + fixture
> + char
> + (1+ pos)
> + (if (string= fixture expected-string)
> + "stay"
> + "become")
> + (replace-regexp-in-string "\n" "\\\\n" expected-string)
> + expected-point)
> + (electric-pair-test-for ,fixture
> + ,(1+ pos)
> + ,char
> + ,expected-string
> + ,expected-point
> + ',mode
> + ,bindings
> + ,fixture-fn)))))
> +
> +(cl-defmacro define-electric-pair-test
> + (name fixture
> + input
> + &key
> + skip-pair-string
> + expected-string
> + expected-point
> + bindings
> + (modes '(quote (emacs-lisp-mode ruby-mode c++-mode)))
> + (test-in-comments t)
> + (test-in-strings t)
> + (test-in-code t)
> + (fixture-fn #'(lambda ()
> + (electric-pair-mode 1))))
> + `(progn
> + ,@(cl-loop
> + for mode in (eval modes) ;FIXME: avoid `eval'
> + append
> + (cl-loop
> + for (prefix suffix extra-desc) in
> + (append (if test-in-comments
> + `((,(with-temp-buffer
> + (funcall mode)
> + (insert "z")
> + (comment-region (point-min) (point-max))
> + (buffer-substring-no-properties (point-min)
> + (1-
> (point-max))))
> + ""
> + "-in-comments")))
> + (if test-in-strings
> + `(("\"" "\"" "-in-strings")))
> + (if test-in-code
> + `(("" "" ""))))
> + append
> + (cl-loop
> + for char across input
> + for pos from 0
> + unless (eq char ?-)
> + collect (electric-pair-define-test-form
> + name
> + fixture
> + (aref input pos)
> + pos
> + expected-string
> + expected-point
> + skip-pair-string
> + prefix
> + suffix
> + extra-desc
> + mode
> + bindings
> + fixture-fn))))))
> +
> +;;; Basic pairings and skippings
> +;;;
> +(define-electric-pair-test balanced-situation
> + " (()) " "(((((((" :skip-pair-string "ppppppp"
> + :modes '(ruby-mode))
> +
> +(define-electric-pair-test too-many-openings
> + " ((()) " "(((((((" :skip-pair-string "ppppppp")
> +
> +(define-electric-pair-test too-many-closings
> + " (())) " "(((((((" :skip-pair-string "------p")
> +
> +(define-electric-pair-test too-many-closings-2
> + "() ) " "---(---" :skip-pair-string "-------")
> +
> +(define-electric-pair-test too-many-closings-3
> + ")() " "(------" :skip-pair-string "-------")
> +
> +(define-electric-pair-test balanced-autoskipping
> + " (()) " "---))--" :skip-pair-string "---ss--")
> +
> +(define-electric-pair-test too-many-openings-autoskipping
> + " ((()) " "----))-" :skip-pair-string "-------")
> +
> +(define-electric-pair-test too-many-closings-autoskipping
> + " (())) " "---)))-" :skip-pair-string "---sss-")
> +
> +
> +;;; Mixed parens
> +;;;
> +(define-electric-pair-test mixed-paren-1
> + " ()] " "-(-(---" :skip-pair-string "-p-p---")
> +
> +(define-electric-pair-test mixed-paren-2
> + " [() " "-(-()--" :skip-pair-string "-p-ps--")
> +
> +(define-electric-pair-test mixed-paren-3
> + " (]) " "-(-()--" :skip-pair-string "---ps--")
> +
> +(define-electric-pair-test mixed-paren-4
> + " ()] " "---)]--" :skip-pair-string "---ss--")
> +
> +(define-electric-pair-test mixed-paren-5
> + " [() " "----(--" :skip-pair-string "----p--")
> +
> +(define-electric-pair-test find-matching-different-paren-type
> + " ()] " "-[-----" :skip-pair-string "-------")
> +
> +(define-electric-pair-test find-matching-different-paren-type-inside-list
> + "( ()]) " "-[-----" :skip-pair-string "-------")
> +
> +(define-electric-pair-test ignore-different-unmatching-paren-type
> + "( ()]) " "-(-----" :skip-pair-string "-p-----")
> +
> +(define-electric-pair-test autopair-keep-least-amount-of-mixed-unbalance
> + "( ()] " "-(-----" :skip-pair-string "-p-----")
> +
> +(define-electric-pair-test dont-autopair-to-resolve-mixed-unbalance
> + "( ()] " "-[-----" :skip-pair-string "-------")
> +
> +(define-electric-pair-test
> autopair-so-as-not-to-worsen-unbalance-situation
> + "( (]) " "-[-----" :skip-pair-string "-p-----")
> +
> +(define-electric-pair-test skip-over-partially-balanced
> + " [([]) " "-----)---" :skip-pair-string "-----s---")
> +
> +(define-electric-pair-test
> only-skip-over-at-least-partially-balanced-stuff
> + " [([()) " "-----))--" :skip-pair-string "-----s---")
> +
> +
> +
> +
> +;;; Quotes
> +;;;
> +(define-electric-pair-test pair-some-quotes-skip-others
> + " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
> + :test-in-strings nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,prog-mode-syntax-table)))
> +
> +(define-electric-pair-test skip-single-quotes-in-ruby-mode
> + " '' " "--'-" :skip-pair-string "--s-"
> + :modes '(ruby-mode)
> + :test-in-comments nil
> + :test-in-strings nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,prog-mode-syntax-table)))
> +
> +(define-electric-pair-test leave-unbalanced-quotes-alone
> + " \"' " "-\"'-" :skip-pair-string "----"
> + :modes '(ruby-mode)
> + :test-in-strings nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,prog-mode-syntax-table)))
> +
> +(define-electric-pair-test leave-unbalanced-quotes-alone-2
> + " \"\\\"' " "-\"--'-" :skip-pair-string "------"
> + :modes '(ruby-mode)
> + :test-in-strings nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,prog-mode-syntax-table)))
> +
> +(define-electric-pair-test leave-unbalanced-quotes-alone-3
> + " foo\\''" "'------" :skip-pair-string "-------"
> + :modes '(ruby-mode)
> + :test-in-strings nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,prog-mode-syntax-table)))
> +
> +(define-electric-pair-test inhibit-only-if-next-is-mismatched
> + "\"foo\"\"bar" "\""
> + :expected-string "\"\"\"foo\"\"bar"
> + :expected-point 2
> + :test-in-strings nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,prog-mode-syntax-table)))
> +
> +
> +;;; More quotes, but now don't bind `electric-pair-text-syntax-table'
> +;;; to `prog-mode-syntax-table'. Use the defaults for
> +;;; `electric-pair-pairs' and `electric-pair-text-pairs'.
> +;;;
> +(define-electric-pair-test pairing-skipping-quotes-in-code
> + " \"\" " "-\"\"-----" :skip-pair-string "-ps------"
> + :test-in-strings nil
> + :test-in-comments nil)
> +
> +(define-electric-pair-test skipping-quotes-in-comments
> + " \"\" " "--\"-----" :skip-pair-string "--s------"
> + :test-in-strings nil)
> +
> +
> +;;; Skipping over whitespace
> +;;;
> +(define-electric-pair-test whitespace-jumping
> + " ( ) " "--))))---" :expected-string " ( ) " :expected-point 8
> + :bindings '((electric-pair-skip-whitespace . t)))
> +
> +(define-electric-pair-test whitespace-chomping
> + " ( ) " "--)------" :expected-string " () " :expected-point 4
> + :bindings '((electric-pair-skip-whitespace . chomp)))
> +
> +(define-electric-pair-test whitespace-chomping-2
> + " ( \n\t\t\n ) " "--)------" :expected-string " () " :expected-point
> 4
> + :bindings '((electric-pair-skip-whitespace . chomp))
> + :test-in-comments nil)
> +
> +(define-electric-pair-test whitespace-chomping-dont-cross-comments
> + " ( \n\t\t\n ) " "--)------" :expected-string " () \n\t\t\n ) "
> + :expected-point 4
> + :bindings '((electric-pair-skip-whitespace . chomp))
> + :test-in-strings nil
> + :test-in-code nil
> + :test-in-comments t)
> +
> +
> +;;; Pairing arbitrary characters
> +;;;
> +(define-electric-pair-test angle-brackets-everywhere
> + "<>" "<>" :skip-pair-string "ps"
> + :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
> +
> +(define-electric-pair-test angle-brackets-everywhere-2
> + "(<>" "-<>" :skip-pair-string "-ps"
> + :bindings '((electric-pair-pairs . ((?\< . ?\>)))))
> +
> +(defvar electric-pair-test-angle-brackets-table
> + (let ((table (make-syntax-table prog-mode-syntax-table)))
> + (modify-syntax-entry ?\< "(>" table)
> + (modify-syntax-entry ?\> ")<`" table)
> + table))
> +
> +(define-electric-pair-test angle-brackets-pair
> + "<>" "<" :expected-string "<><>" :expected-point 2
> + :test-in-code nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,electric-pair-test-angle-brackets-table)))
> +
> +(define-electric-pair-test angle-brackets-skip
> + "<>" "->" :expected-string "<>" :expected-point 3
> + :test-in-code nil
> + :bindings `((electric-pair-text-syntax-table
> + . ,electric-pair-test-angle-brackets-table)))
> +
> +(define-electric-pair-test pair-backtick-and-quote-in-comments
> + ";; " "---`" :expected-string ";; `'" :expected-point 5
> + :test-in-comments nil
> + :test-in-strings nil
> + :modes '(emacs-lisp-mode)
> + :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
> +
> +(define-electric-pair-test skip-backtick-and-quote-in-comments
> + ";; `foo'" "-------'" :expected-string ";; `foo'" :expected-point 9
> + :test-in-comments nil
> + :test-in-strings nil
> + :modes '(emacs-lisp-mode)
> + :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
> +
> +(define-electric-pair-test pair-backtick-and-quote-in-strings
> + "\"\"" "-`" :expected-string "\"`'\"" :expected-point 3
> + :test-in-comments nil
> + :test-in-strings nil
> + :modes '(emacs-lisp-mode)
> + :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
> +
> +(define-electric-pair-test skip-backtick-and-quote-in-strings
> + "\"`'\"" "--'" :expected-string "\"`'\"" :expected-point 4
> + :test-in-comments nil
> + :test-in-strings nil
> + :modes '(emacs-lisp-mode)
> + :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
> +
> +(define-electric-pair-test skip-backtick-and-quote-in-strings-2
> + " \"`'\"" "----'" :expected-string " \"`'\"" :expected-point 6
> + :test-in-comments nil
> + :test-in-strings nil
> + :modes '(emacs-lisp-mode)
> + :bindings '((electric-pair-text-pairs . ((?\` . ?\')))))
> +
> +
> +;;; `js-mode' has `electric-layout-rules' for '{ and '}
> +;;;
> +(define-electric-pair-test js-mode-braces
> + "" "{" :expected-string "{}" :expected-point 2
> + :modes '(js-mode)
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)))
> +
> +(define-electric-pair-test js-mode-braces-with-layout
> + "" "{" :expected-string "{\n\n}" :expected-point 3
> + :modes '(js-mode)
> + :test-in-comments nil
> + :test-in-strings nil
> + :fixture-fn #'(lambda ()
> + (electric-layout-mode 1)
> + (electric-pair-mode 1)))
> +
> +(define-electric-pair-test js-mode-braces-with-layout-and-indent
> + "" "{" :expected-string "{\n \n}" :expected-point 7
> + :modes '(js-mode)
> + :test-in-comments nil
> + :test-in-strings nil
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (electric-indent-mode 1)
> + (electric-layout-mode 1)))
> +
> +
> +;;; Backspacing
> +;;; TODO: better tests
> +;;;
> +(ert-deftest electric-pair-backspace-1 ()
> + (save-electric-modes
> + (with-temp-buffer
> + (insert "()")
> + (goto-char 2)
> + (electric-pair-backward-delete-char 1)
> + (should (equal "" (buffer-string))))))
> +
> +
> +;;; Electric newlines between pairs
> +;;; TODO: better tests
> +(ert-deftest electric-pair-open-extra-newline ()
> + (save-electric-modes
> + (with-temp-buffer
> + (c-mode)
> + (electric-pair-mode 1)
> + (electric-indent-mode 1)
> + (insert "int main {}")
> + (backward-char 1)
> + (let ((c-basic-offset 4))
> + (newline 1 t)
> + (should (equal "int main {\n \n}"
> + (buffer-string)))
> + (should (equal (point) (- (point-max) 2)))))))
> +
> +
> +
> +;;; Autowrapping
> +;;;
> +(define-electric-pair-test autowrapping-1
> + "foo" "(" :expected-string "(foo)" :expected-point 2
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (mark-sexp 1)))
> +
> +(define-electric-pair-test autowrapping-2
> + "foo" ")" :expected-string "(foo)" :expected-point 6
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (mark-sexp 1)))
> +
> +(define-electric-pair-test autowrapping-3
> + "foo" ")" :expected-string "(foo)" :expected-point 6
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (goto-char (point-max))
> + (skip-chars-backward "\"")
> + (mark-sexp -1)))
> +
> +(define-electric-pair-test autowrapping-4
> + "foo" "(" :expected-string "(foo)" :expected-point 2
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (goto-char (point-max))
> + (skip-chars-backward "\"")
> + (mark-sexp -1)))
> +
> +(define-electric-pair-test autowrapping-5
> + "foo" "\"" :expected-string "\"foo\"" :expected-point 2
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (mark-sexp 1)))
> +
> +(define-electric-pair-test autowrapping-6
> + "foo" "\"" :expected-string "\"foo\"" :expected-point 6
> + :fixture-fn #'(lambda ()
> + (electric-pair-mode 1)
> + (goto-char (point-max))
> + (skip-chars-backward "\"")
> + (mark-sexp -1)))
> +
> +(provide 'electric-pair-tests)
> +;;; electric-pair-tests.el ends here
>
>
[-- Attachment #2: Type: text/html, Size: 71195 bytes --]
^ permalink raw reply [flat|nested] 36+ messages in thread
end of thread, other threads:[~2013-12-24 14:29 UTC | newest]
Thread overview: 36+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2013-12-06 23:31 [patch] make electric-pair-mode smarter/more useful João Távora
2013-12-07 2:09 ` 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
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.