Commit 3b8d5131 authored by João Távora's avatar João Távora
Browse files

Make Electric Pair mode smarter/more useful:

* lisp/electric.el: Pairing/skipping helps preserve
  balance. Autobackspacing behaviour. Opens extra newlines between
  pairs. Skip whitespace before closing delimiters.

* lisp/emacs-lisp/lisp-mode.el (lisp-mode-variables): Use new
  features.

* test/automated/electric-tests.lisp: New file.

* doc/emacs/programs.texi: Describe new features.

* lisp/simple.el: Pass non-nil interactive arg to newline call inside
  newline-and-indent.
parent fbcc63a3
2013-12-26 João Távora <joaotavora@gmail.com>
* emacs.texi (Matching): Describe new features of Electric Pair
mode.
2013-12-25 Chong Yidong <cyd@gnu.org>
* glossary.texi (Glossary): Define MULE in modern terms.
......
......@@ -844,8 +844,34 @@ show-paren-mode}.
Electric Pair mode, a global minor mode, provides a way to easily
insert matching delimiters. Whenever you insert an opening delimiter,
the matching closing delimiter is automatically inserted as well,
leaving point between the two. To toggle Electric Pair mode, type
@kbd{M-x electric-pair-mode}.
leaving point between the two. Conversely, when you insert a closing
delimiter over an existing one, no inserting takes places and that
position is simply skipped over. These variables control additional
features of Electric Pair mode:
@itemize @bullet
@item
@code{electric-pair-preserve-balance}, when non-@code{nil}, makes the
default pairing logic balance out the number of opening and closing
delimiters.
@item
@code{electric-pair-delete-adjacent-pairs}, when non-@code{nil}, makes
backspacing between two adjacent delimiters also automatically delete
the closing delimiter.
@item
@code{electric-pair-open-newline-between-pairs}, when non-@code{nil},
makes inserting inserting a newline between two adjacent pairs also
automatically open and extra newline after point.
@item
@code{electric-skip-whitespace}, when non-@code{nil}, causes the minor
mode to skip whitespace forward before deciding whether to skip over
the closing delimiter.
@end itemize
To toggle Electric Pair mode, type @kbd{M-x electric-pair-mode}.
@node Comments
@section Manipulating Comments
......
2013-12-26 João Távora <joaotavora@gmail.com>
* NEWS: Describe new features of Electric Pair mode.
2013-12-23 Teodor Zlatanov <tzz@lifelogs.com>
* NEWS: Updated for `gnutls-verify-error', cfengine-mode, and
......
......@@ -439,6 +439,42 @@ and `desktop-restore-forces-onscreen' offer further customization.
** Eldoc Mode works properly in the minibuffer.
** Electric Pair mode
*** New `electric-pair-preserve-balance' enabled by default.
Pairing/skipping only kicks in when that help the balance of
parentheses and quotes, i.e. the buffer should end up at least as
balanced as before.
You can further control this behaviour by adjusting the predicates
stored in `electric-pair-inhibit-predicate' and
`electric-pair-skip-self'.
*** New `electric-pair-delete-adjacent-pairs' enabled by default.
In `electric-pair-mode', the commands `backward-delete-char' and
`backward-delete-char-untabify' are now bound to electric variants
that delete the closer when invoked between adjacent pairs.
*** New `electric-pair-open-newline-between-pairs' enabled by default.
In `electric-pair-mode', inserting a newline between adjacent pairs
opens an extra newline after point, which is indented if
`electric-indent-mode' is also set.
*** New `electric-pair-skip-whitespace' enabled by default.
Controls if skipping over closing delimiters should jump over any
whitespace slack. Setting it to `chomp' makes it delete this
whitespace. See also the variable
`electric-pair-skip-whitespace-chars'.
*** New variables control the pairing in strings and comments.
You can customize `electric-pair-text-pairs' and
`electric-pair-text-syntax-table' to tweak pairing behaviour inside
strings and comments.
** EPA
*** New option `epa-mail-aliases'.
......
2013-12-26 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-26 Fabián Ezequiel Gallina <fgallina@gnu.org>
* progmodes/python.el: Use lexical-binding.
......
......@@ -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,297 @@ 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)