* [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-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: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 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 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-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-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: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: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
* 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 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-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 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-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 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 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
* 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 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
[parent not found: <CALDnm52AoShN891-L9=Cbng98UtYPEntzO+n_XDMmEL+UV0r-A@mail.gmail.com>]
* 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
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.