(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)
(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
:bindings '((comment-start . "<!--") (comment-use-syntax . t))
:test-in-comments nil :test-in-strings nil)
+\f
+;;; 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 ?\r))
+ (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 ?\r))
+ (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 ?\r))
+ (call-interactively (key-binding `[,last-command-event])))
+ (should (equal (buffer-string) "int main () {\n \n}"))))
+
(provide 'electric-tests)
;;; electric-tests.el ends here