Extend electric-layout-mode to handle more complex layouts (bug#33794)

Entries in electric-layout-rules can specify multiple
newline-related actions which are executed in order of appearance.

Also, have it play nice with electric-pair-mode when inserting a
newlines, particularly with electric-pair-open-newline-between-pairs.

Entries in electric-layout-rules can also be functions.  Among other
things, the logic behind electric-pair-open-newline-between-pairs
could now be moved to electric-layout-mode, but this commit doesn't do
that yet.

This change was motivated by bug#33794 and is an alternative solution
to the problem reported in that bug.

* lisp/electric.el (electric-layout-rules): Adjust docstring.
(electric-layout-post-self-insert-function): Call
electric-layout-post-self-insert-function-1.
(electric-layout-post-self-insert-function-1): Rename from
electric-layout-post-self-insert-function.  Redesign.
(electric-layout-local-mode): New minor mode.

* test/lisp/electric-tests.el (electric-layout-int-main-kernel-style)
(electric-layout-int-main-allman-style)
(electric-modes-in-c-mode-with-self-insert-command)
(electric-pair-mode-newline-between-parens)
(electric-layout-mode-newline-between-parens-without-e-p-m)
(electric-layout-mode-newline-between-parens-without-e-p-m-2): New
tests.
(plainer-c-mode): New helper.
This commit is contained in:
João Távora 2018-12-21 18:00:08 +00:00
parent 0515b223c2
commit 949295ae1a
2 changed files with 196 additions and 33 deletions

View file

@ -363,45 +363,91 @@ use `electric-indent-local-mode'."
(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', `after-stay', or a
function of no arguments that returns one of those symbols.
Each rule has the form (CHAR . WHERE), the rule matching if the
character just inserted was CHAR. WHERE specifies where to
insert newlines, and can be:
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.")
* one of the symbols `before', `after', `around', `after-stay',
or nil.
* a list of the preceding symbols, processed in order of
appearance to insert multiple newlines;
* a function of no arguments that returns one of the previous
values.
Each symbol specifies where, in relation to the position POS of
the character inserted, the newline character(s) should be
inserted. `after-stay' means insert a newline after POS but stay
in the same place.
Instead of the (CHAR . WHERE) form, a rule can also be just a
function of a single argument, the character just inserted. It
should return a value compatible with WHERE if the rule matches,
or nil if it doesn't match.
If multiple rules match, only first one is executed.")
(defun electric-layout-post-self-insert-function ()
(let* ((rule (cdr (assq last-command-event electric-layout-rules)))
pos)
(when electric-layout-mode
(electric-layout-post-self-insert-function-1)))
;; for edebug's sake, a separate function
(defun electric-layout-post-self-insert-function-1 ()
(let* (pos
probe
(rules electric-layout-rules)
(rule
(catch 'done
(while (setq probe (pop rules))
(cond ((and (consp probe)
(eq (car probe) last-command-event))
(throw 'done (cdr probe)))
((functionp probe)
(let ((res
(save-excursion
(goto-char
(or pos (setq pos (electric--after-char-pos))))
(funcall probe last-command-event))))
(when res (throw 'done res)))))))))
(when (and rule
(setq pos (electric--after-char-pos))
(or pos (setq pos (electric--after-char-pos)))
;; Not in a string or comment.
(not (nth 8 (save-excursion (syntax-ppss pos)))))
(let ((end (point-marker))
(sym (if (functionp rule) (funcall rule) rule)))
(set-marker-insertion-type end (not (eq sym 'after-stay)))
(goto-char pos)
(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"))
('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 end)))))
(goto-char pos)
(when (functionp rule) (setq rule (funcall rule)))
(dolist (sym (if (symbolp rule) (list rule) rule))
(let* ((nl-after
(lambda ()
;; FIXME: we use `newline', which calls
;; `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: when `newline'ing, we exceptionally
;; prevent a specific behaviour of
;; `eletric-pair-mode', that of opening an extra
;; newline between newly inserted matching paris.
;; In theory that behaviour should be provided by
;; `electric-layout-mode' instead, which should be
;; possible given the current API.
;;
;; FIXME: check eolp before inserting \n?
(let ((electric-layout-mode nil)
(electric-pair-open-newline-between-pairs nil))
(newline 1 t))))
(nl-before (lambda ()
(save-excursion
(goto-char (1- pos)) (skip-chars-backward " \t")
(unless (bolp) (funcall nl-after))))))
(pcase sym
('before (funcall nl-before))
('after (funcall nl-after))
('after-stay (save-excursion (funcall nl-after)))
('around (funcall nl-before) (funcall nl-after))))))))
(put 'electric-layout-post-self-insert-function 'priority 40)
@ -419,6 +465,19 @@ The variable `electric-layout-rules' says when and how to insert newlines."
(remove-hook 'post-self-insert-hook
#'electric-layout-post-self-insert-function))))
;;;###autoload
(define-minor-mode electric-layout-local-mode
"Toggle `electric-layout-mode' only in this buffer."
:variable (buffer-local-value 'electric-layout-mode (current-buffer))
(cond
((eq electric-layout-mode (default-value 'electric-layout-mode))
(kill-local-variable 'electric-layout-mode))
((not (default-value 'electric-layout-mode))
;; Locally enabled, but globally disabled.
(electric-layout-mode 1) ; Setup the hooks.
(setq-default electric-layout-mode nil) ; But keep it globally disabled.
)))
;;; Electric quoting.
(defcustom electric-quote-comment t

View file

@ -812,5 +812,109 @@ baz\"\""
:bindings '((comment-start . "<!--") (comment-use-syntax . t))
:test-in-comments nil :test-in-strings nil)
;;; tests for `electric-layout-mode'
(ert-deftest electric-layout-int-main-kernel-style ()
(ert-with-test-buffer ()
(c-mode)
(electric-layout-local-mode 1)
(electric-pair-local-mode 1)
(electric-indent-local-mode 1)
(setq-local electric-layout-rules
'((?\{ . (after-stay after))))
(insert "int main () ")
(let ((last-command-event ?\{))
(call-interactively (key-binding `[,last-command-event])))
(should (equal (buffer-string) "int main () {\n \n}"))))
(ert-deftest electric-layout-int-main-allman-style ()
(ert-with-test-buffer ()
(c-mode)
(electric-layout-local-mode 1)
(electric-pair-local-mode 1)
(electric-indent-local-mode 1)
(setq-local electric-layout-rules
'((?\{ . (before after-stay after))))
(insert "int main () ")
(let ((last-command-event ?\{))
(call-interactively (key-binding `[,last-command-event])))
(should (equal (buffer-string) "int main ()\n{\n \n}"))))
(define-derived-mode plainer-c-mode c-mode "pC"
"A plainer/saner C-mode with no internal electric machinery."
(c-toggle-electric-state -1)
(setq-local electric-indent-local-mode-hook nil)
(setq-local electric-indent-mode-hook nil)
(electric-indent-local-mode 1)
(dolist (key '(?\" ?\' ?\{ ?\} ?\( ?\) ?\[ ?\]))
(local-set-key (vector key) 'self-insert-command)))
(ert-deftest electric-modes-in-c-mode-with-self-insert-command ()
(ert-with-test-buffer ()
(plainer-c-mode)
(electric-layout-local-mode 1)
(electric-pair-local-mode 1)
(electric-indent-local-mode 1)
(setq-local electric-layout-rules
'((?\{ . (before after-stay after))))
(insert "int main () ")
(let ((last-command-event ?\{))
(call-interactively (key-binding `[,last-command-event])))
(should (equal (buffer-string) "int main ()\n{\n \n}"))))
(ert-deftest electric-pair-mode-newline-between-parens ()
(ert-with-test-buffer ()
(plainer-c-mode)
(electric-layout-local-mode -1) ;; ensure e-l-m mode is off
(electric-pair-local-mode 1)
(insert-before-markers "int main () {}")
(backward-char 1)
(let ((last-command-event ? ))
(call-interactively (key-binding `[,last-command-event])))
(should (equal (buffer-string) "int main () {\n \n}"))))
(ert-deftest electric-layout-mode-newline-between-parens-without-e-p-m ()
(ert-with-test-buffer ()
(plainer-c-mode)
(electric-layout-local-mode 1)
(electric-pair-local-mode -1) ;; ensure e-p-m mode is off
(electric-indent-local-mode 1)
(setq-local electric-layout-rules
'((?\n
.
(lambda ()
(when (eq (save-excursion
(skip-chars-backward "\t\s")
(char-before (1- (point))))
(matching-paren (char-after)))
'(after-stay))))))
(insert "int main () {}")
(backward-char 1)
(let ((last-command-event ? ))
(call-interactively (key-binding `[,last-command-event])))
(should (equal (buffer-string) "int main () {\n \n}"))))
(ert-deftest electric-layout-mode-newline-between-parens-without-e-p-m-2 ()
(ert-with-test-buffer ()
(plainer-c-mode)
(electric-layout-local-mode 1)
(electric-pair-local-mode -1) ;; ensure e-p-m mode is off
(electric-indent-local-mode 1)
(setq-local electric-layout-rules
'((lambda (char)
(when (and
(eq char ?\n)
(eq (save-excursion
(skip-chars-backward "\t\s")
(char-before (1- (point))))
(matching-paren (char-after))))
'(after-stay)))))
(insert "int main () {}")
(backward-char 1)
(let ((last-command-event ? ))
(call-interactively (key-binding `[,last-command-event])))
(should (equal (buffer-string) "int main () {\n \n}"))))
(provide 'electric-tests)
;;; electric-tests.el ends here