aggressive-indent.el (22463B)
1 ;;; aggressive-indent.el --- Minor mode to aggressively keep your code always indented -*- lexical-binding:t -*- 2 3 ;; Copyright (C) 2014-2021 Free Software Foundation, Inc 4 5 ;; Author: Artur Malabarba <emacs@endlessparentheses.com> 6 ;; URL: https://github.com/Malabarba/aggressive-indent-mode 7 ;; Version: 1.10.0 8 ;; Package-Requires: ((emacs "24.3")) 9 ;; Keywords: indent lisp maint tools 10 ;; Prefix: aggressive-indent 11 ;; Separator: - 12 13 ;;; Commentary: 14 ;; 15 ;; `electric-indent-mode' is enough to keep your code nicely aligned when 16 ;; all you do is type. However, once you start shifting blocks around, 17 ;; transposing lines, or slurping and barfing sexps, indentation is bound 18 ;; to go wrong. 19 ;; 20 ;; `aggressive-indent-mode' is a minor mode that keeps your code always 21 ;; indented. It reindents after every change, making it more reliable 22 ;; than `electric-indent-mode'. 23 ;; 24 ;; ### Instructions ### 25 ;; 26 ;; This package is available fom Melpa, you may install it by calling 27 ;; 28 ;; M-x package-install RET aggressive-indent 29 ;; 30 ;; Then activate it with 31 ;; 32 ;; (add-hook 'emacs-lisp-mode-hook #'aggressive-indent-mode) 33 ;; (add-hook 'css-mode-hook #'aggressive-indent-mode) 34 ;; 35 ;; You can use this hook on any mode you want, `aggressive-indent' is not 36 ;; exclusive to emacs-lisp code. In fact, if you want to turn it on for 37 ;; every programming mode, you can do something like: 38 ;; 39 ;; (global-aggressive-indent-mode 1) 40 ;; (add-to-list 'aggressive-indent-excluded-modes 'html-mode) 41 ;; 42 ;; ### Manual Installation ### 43 ;; 44 ;; If you don't want to install from Melpa, you can download it manually, 45 ;; place it in your `load-path' and require it with 46 ;; 47 ;; (require 'aggressive-indent) 48 49 ;;; Instructions: 50 ;; 51 ;; INSTALLATION 52 ;; 53 ;; This package is available fom Melpa, you may install it by calling 54 ;; M-x package-install RET aggressive-indent. 55 ;; 56 ;; Then activate it with 57 ;; (add-hook 'emacs-lisp-mode-hook #'aggressive-indent-mode) 58 ;; 59 ;; You can also use an equivalent hook for another mode, 60 ;; `aggressive-indent' is not exclusive to emacs-lisp code. 61 ;; 62 ;; Alternatively, you can download it manually, place it in your 63 ;; `load-path' and require it with 64 ;; 65 ;; (require 'aggressive-indent) 66 67 ;;; License: 68 ;; 69 ;; This file is NOT part of GNU Emacs. 70 ;; 71 ;; This program is free software; you can redistribute it and/or 72 ;; modify it under the terms of the GNU General Public License 73 ;; as published by the Free Software Foundation; either version 3 74 ;; of the License, or (at your option) any later version. 75 ;; 76 ;; This program is distributed in the hope that it will be useful, 77 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 78 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 79 ;; GNU General Public License for more details. 80 ;; 81 82 ;;; Code: 83 84 (require 'cl-lib) 85 86 (defgroup aggressive-indent nil 87 "Customization group for aggressive-indent." 88 :prefix "aggressive-indent-" 89 :group 'electricity 90 :group 'indent) 91 92 (defun aggressive-indent-bug-report () 93 "Opens github issues page in a web browser. Please send any bugs you find. 94 Please include your Emacs and `aggressive-indent' versions." 95 (interactive) 96 (message "Your `aggressive-indent-version' is: %s, and your emacs version is: %s. 97 Please include this in your report!" 98 (eval-when-compile 99 (ignore-errors 100 (require 'lisp-mnt) 101 (lm-version))) 102 emacs-version) 103 (browse-url "https://github.com/Malabarba/aggressive-indent-mode/issues/new")) 104 105 (defvar aggressive-indent-mode) 106 107 ;;; Configuring indentation 108 (defcustom aggressive-indent-dont-electric-modes nil 109 "List of major-modes where `electric-indent' should be disabled." 110 :type '(choice 111 (const :tag "Never use `electric-indent-mode'." t) 112 (repeat :tag "List of major-modes to avoid `electric-indent-mode'." symbol)) 113 :package-version '(aggressive-indent . "0.3.1")) 114 115 (defcustom aggressive-indent-excluded-modes 116 '(elm-mode 117 haskell-mode 118 inf-ruby-mode 119 makefile-mode 120 makefile-gmake-mode 121 python-mode 122 sql-interactive-mode 123 text-mode 124 yaml-mode) 125 "Modes in which `aggressive-indent-mode' should not be activated. 126 This variable is only used if `global-aggressive-indent-mode' is 127 active. If the minor mode is turned on with the local command, 128 `aggressive-indent-mode', this variable is ignored." 129 :type '(repeat symbol) 130 :package-version '(aggressive-indent . "1.8.4")) 131 132 (defcustom aggressive-indent-protected-commands '(undo undo-tree-undo undo-tree-redo whitespace-cleanup) 133 "Commands after which indentation will NOT be performed. 134 Aggressive indentation could break things like `undo' by locking 135 the user in a loop, so this variable is used to control which 136 commands will NOT be followed by a re-indent." 137 :type '(repeat symbol) 138 :package-version '(aggressive-indent . "0.1")) 139 140 (defcustom aggressive-indent-protected-current-commands 141 '(query-replace-regexp query-replace) 142 "Like `aggressive-indent-protected-commands', but for the current command. 143 For instance, with the default value, this variable prevents 144 indentation during `query-replace' (but not after)." 145 :type '(repeat symbol) 146 :package-version '(aggressive-indent . "1.8.4")) 147 148 (defcustom aggressive-indent-comments-too nil 149 "If non-nil, aggressively indent in comments as well." 150 :type 'boolean 151 :package-version '(aggressive-indent . "0.3")) 152 153 (defcustom aggressive-indent-modes-to-prefer-defun 154 '(emacs-lisp-mode lisp-mode scheme-mode clojure-mode) 155 "List of major-modes in which indenting defun is preferred. 156 Add here any major modes with very good definitions of 157 `end-of-defun' and `beginning-of-defun', or modes which bug out 158 if you have `after-change-functions' (such as paredit). 159 160 If current major mode is derived from one of these, 161 `aggressive-indent' will call `aggressive-indent-indent-defun' 162 after every command. Otherwise, it will call 163 `aggressive-indent-indent-region-and-on' after every buffer 164 change." 165 :type '(repeat symbol) 166 :package-version '(aggressive-indent . "0.3")) 167 168 ;;; Preventing indentation 169 (defconst aggressive-indent--internal-dont-indent-if 170 '((memq last-command aggressive-indent-protected-commands) 171 (memq this-command aggressive-indent-protected-current-commands) 172 (region-active-p) 173 buffer-read-only 174 undo-in-progress 175 (null (buffer-modified-p)) 176 (and (boundp 'smerge-mode) smerge-mode) 177 (equal (buffer-name) "*ediff-merge*") 178 (let ((line (thing-at-point 'line))) 179 (and (stringp line) 180 ;; If the user is starting to type a comment. 181 (stringp comment-start) 182 (string-match (concat "\\`[[:blank:]]*" 183 (substring comment-start 0 1) 184 "[[:blank:]]*$") 185 line))) 186 (let ((sp (syntax-ppss))) 187 ;; Comments. 188 (or (and (not aggressive-indent-comments-too) (elt sp 4)) 189 ;; Strings. 190 (elt sp 3)))) 191 "List of forms which prevent indentation when they evaluate to non-nil. 192 This is for internal use only. For user customization, use 193 `aggressive-indent-dont-indent-if' instead.") 194 195 (eval-after-load 'yasnippet 196 '(when (boundp 'yas--active-field-overlay) 197 (add-to-list 'aggressive-indent--internal-dont-indent-if 198 '(and 199 (overlayp yas--active-field-overlay) 200 (overlay-end yas--active-field-overlay)) 201 'append))) 202 (eval-after-load 'company 203 '(when (boundp 'company-candidates) 204 (add-to-list 'aggressive-indent--internal-dont-indent-if 205 'company-candidates))) 206 (eval-after-load 'auto-complete 207 '(when (boundp 'ac-completing) 208 (add-to-list 'aggressive-indent--internal-dont-indent-if 209 'ac-completing))) 210 (eval-after-load 'multiple-cursors-core 211 '(when (boundp 'multiple-cursors-mode) 212 (add-to-list 'aggressive-indent--internal-dont-indent-if 213 'multiple-cursors-mode))) 214 (eval-after-load 'iedit 215 '(when (boundp 'iedit-mode) 216 (add-to-list 'aggressive-indent--internal-dont-indent-if 217 'iedit-mode))) 218 (eval-after-load 'evil 219 '(when (boundp 'iedit-mode) 220 (add-to-list 'aggressive-indent--internal-dont-indent-if 221 'iedit-mode))) 222 (eval-after-load 'coq 223 '(add-to-list 'aggressive-indent--internal-dont-indent-if 224 '(and (derived-mode-p 'coq-mode) 225 (not (string-match "\\.[[:space:]]*$" 226 (thing-at-point 'line)))))) 227 (eval-after-load 'ruby-mode 228 '(add-to-list 'aggressive-indent--internal-dont-indent-if 229 '(when (derived-mode-p 'ruby-mode) 230 (let ((line (thing-at-point 'line))) 231 (and (stringp line) 232 (string-match "\\b\\(begin\\|case\\|d\\(?:ef\\|o\\)\\|if\\) *$" line)))))) 233 234 (defcustom aggressive-indent-dont-indent-if '() 235 "List of variables and functions to prevent aggressive indenting. 236 This variable is a list where each element is a Lisp form. 237 As long as any one of these forms returns non-nil, 238 aggressive-indent will not perform any indentation. 239 240 See `aggressive-indent--internal-dont-indent-if' for usage examples. 241 242 Note that this is only used once, and only on the line where the 243 point is when we're about to start indenting. In order to 244 prevent indentation of further lines, see 245 `aggressive-indent-stop-here-hook'." 246 :type '(repeat sexp) 247 :package-version '(aggressive-indent . "0.2")) 248 249 (defcustom aggressive-indent-stop-here-hook nil 250 "A hook that runs on each line before it is indented. 251 If any function on this hook returns non-nil, it immediately 252 prevents indentation of the current line and any further 253 lines. 254 255 Note that aggressive-indent does indentation in two stages. The 256 first stage indents the entire edited region, while the second 257 stage keeps indenting further lines until its own logic decide to 258 stop. This hook only affects the second stage. That is, it 259 effectly lets you add your own predicates to the logic that 260 decides when to stop. 261 262 In order to prevent indentation before the first stage, see 263 `aggressive-indent-dont-indent-if' instead." 264 :type 'hook) 265 266 (defvar aggressive-indent--error-message "One of the forms in `aggressive-indent-dont-indent-if' had the following error, I've disabled it until you fix it: %S" 267 "Error message thrown by `aggressive-indent-dont-indent-if'.") 268 269 (defvar aggressive-indent--has-errored nil 270 "Keep track of whether `aggressive-indent-dont-indent-if' is throwing. 271 This is used to prevent an infinite error loop on the user.") 272 273 (defun aggressive-indent--run-user-hooks () 274 "Safely run forms in `aggressive-indent-dont-indent-if'. 275 If any of them errors out, we only report it once until it stops 276 erroring again." 277 (and aggressive-indent-dont-indent-if 278 (condition-case er 279 (prog1 (eval (cons 'or aggressive-indent-dont-indent-if)) 280 (setq aggressive-indent--has-errored nil)) 281 (error (unless aggressive-indent--has-errored 282 (setq aggressive-indent--has-errored t) 283 (message aggressive-indent--error-message er)))))) 284 285 ;;; Indenting defun 286 (defcustom aggressive-indent-region-function #'indent-region 287 "Function called to indent a region. 288 It is called with two arguments, the region beginning and end." 289 :risky t 290 :type 'function) 291 292 ;;;###autoload 293 (defun aggressive-indent-indent-defun (&optional l r) 294 "Indent current defun. 295 Throw an error if parentheses are unbalanced. 296 If L and R are provided, use them for finding the start and end of defun." 297 (interactive) 298 (let ((p (point-marker))) 299 (set-marker-insertion-type p t) 300 (funcall aggressive-indent-region-function 301 (save-excursion 302 (when l (goto-char l)) 303 (beginning-of-defun 1) (point)) 304 (save-excursion 305 (when r (goto-char r)) 306 (end-of-defun 1) (point))) 307 (goto-char p))) 308 309 (defun aggressive-indent--softly-indent-defun (&optional l r) 310 "Indent current defun unobstrusively. 311 Like `aggressive-indent-indent-defun', but without errors or 312 messages. L and R passed to `aggressive-indent-indent-defun'." 313 (cl-letf (((symbol-function 'message) #'ignore)) 314 (ignore-errors (aggressive-indent-indent-defun l r)))) 315 316 ;;; Indenting region 317 (defun aggressive-indent--indent-current-balanced-line (column) 318 "Indent current balanced line, if it starts at COLUMN. 319 Balanced line means anything contained in a sexp that starts at 320 the current line, or starts at the same line that one of these 321 sexps ends. 322 323 Return non-nil only if the line's indentation actually changed." 324 (when (= (current-column) column) 325 (unless (= (point) 326 (progn (indent-according-to-mode) 327 (point))) 328 (let ((line-end (line-end-position))) 329 (forward-sexp 1) 330 (comment-forward (point-max)) 331 ;; We know previous sexp finished on a previous line when 332 ;; there's only be whitespace behind point. 333 (while (progn 334 (skip-chars-backward "[:blank:]") 335 (not (looking-at "^"))) 336 (forward-sexp 1) 337 (comment-forward (point-max))) 338 (when (looking-at "^") 339 (funcall aggressive-indent-region-function line-end (1- (point)))) 340 (skip-chars-forward "[:blank:]"))))) 341 342 (defun aggressive-indent--extend-end-to-whole-sexps (beg end) 343 "Return a point >= END, so that it covers whole sexps from BEG." 344 (save-excursion 345 (goto-char beg) 346 (while (and (< (point) end) 347 (not (eobp))) 348 (forward-sexp 1)) 349 (point))) 350 351 ;;;###autoload 352 (defun aggressive-indent-indent-region-and-on (l r) 353 "Indent region between L and R, and then some. 354 Call `aggressive-indent-region-function' between L and R, and 355 then keep indenting until nothing more happens." 356 (interactive "r") 357 (let ((p (point-marker))) 358 (set-marker-insertion-type p t) 359 (unwind-protect 360 (progn 361 (unless (= l r) 362 (when (= (char-before r) ?\n) 363 (cl-decf r))) 364 ;; If L is at the end of a line, skip that line. 365 (unless (= l r) 366 (when (= (char-after l) ?\n) 367 (cl-incf l))) 368 ;; Indent the affected region. 369 (goto-char r) 370 (unless (= l r) (funcall aggressive-indent-region-function l r)) 371 ;; And then we indent each following line until nothing happens. 372 (forward-line 1) 373 (skip-chars-forward "[:blank:]\n\r\xc") 374 (let ((base-column (current-column))) 375 (while (and (not (eobp)) 376 (not (run-hook-with-args-until-success 'aggressive-indent-stop-here-hook)) 377 (aggressive-indent--indent-current-balanced-line base-column))))) 378 (goto-char p)))) 379 380 (defun aggressive-indent--softly-indent-region-and-on (l r &rest _) 381 "Indent region between L and R, and a bit more. 382 Like `aggressive-indent-indent-region-and-on', but without errors 383 or messages." 384 (cl-letf (((symbol-function 'message) #'ignore)) 385 (ignore-errors (aggressive-indent-indent-region-and-on l r)))) 386 387 ;;; Tracking changes 388 (defvar aggressive-indent--changed-list nil 389 "List of (left right) limit of regions changed in the last command loop.") 390 (make-variable-buffer-local 'aggressive-indent--changed-list) 391 392 (defun aggressive-indent--process-changed-list-and-indent () 393 "Indent the regions in `aggressive-indent--changed-list'." 394 (unless (or (run-hook-wrapped 'aggressive-indent--internal-dont-indent-if #'eval) 395 (aggressive-indent--run-user-hooks)) 396 (let ((after-change-functions (remove 'aggressive-indent--keep-track-of-changes after-change-functions)) 397 (inhibit-point-motion-hooks t) 398 (indent-function 399 (if (cl-member-if #'derived-mode-p aggressive-indent-modes-to-prefer-defun) 400 #'aggressive-indent--softly-indent-defun #'aggressive-indent--softly-indent-region-and-on))) 401 ;; Take the 10 most recent changes. 402 (let ((cell (nthcdr 10 aggressive-indent--changed-list))) 403 (when cell (setcdr cell nil))) 404 ;; (message "----------") 405 (while aggressive-indent--changed-list 406 ;; (message "%S" (car aggressive-indent--changed-list)) 407 (apply indent-function (car aggressive-indent--changed-list)) 408 (setq aggressive-indent--changed-list 409 (cdr aggressive-indent--changed-list)))))) 410 411 (defun aggressive-indent--clear-change-list () 412 "Clear cache of all changed regions. " 413 (setq aggressive-indent--changed-list nil)) 414 415 (defcustom aggressive-indent-sit-for-time 0.05 416 "Time, in seconds, to wait before indenting. 417 If you feel aggressive-indent is causing Emacs to hang while 418 typing, try tweaking this number." 419 :type 'float) 420 421 (defvar-local aggressive-indent--idle-timer nil 422 "Idle timer used for indentation") 423 424 ;; Ripped from Emacs 27.0 subr.el. 425 ;; See Github Issue#111 and Emacs bug#31692. 426 (defmacro aggressive-indent--while-no-input (&rest body) 427 "Execute BODY only as long as there's no pending input. 428 If input arrives, that ends the execution of BODY, 429 and `while-no-input' returns t. Quitting makes it return nil. 430 If BODY finishes, `while-no-input' returns whatever value BODY produced." 431 (declare (debug t) (indent 0)) 432 (let ((catch-sym (make-symbol "input"))) 433 `(with-local-quit 434 (catch ',catch-sym 435 (let ((throw-on-input ',catch-sym) 436 val) 437 (setq val (or (input-pending-p) 438 (progn ,@body))) 439 (cond 440 ;; When input arrives while throw-on-input is non-nil, 441 ;; kbd_buffer_store_buffered_event sets quit-flag to the 442 ;; value of throw-on-input. If, when BODY finishes, 443 ;; quit-flag still has the same value as throw-on-input, it 444 ;; means BODY never tested quit-flag, and therefore ran to 445 ;; completion even though input did arrive before it 446 ;; finished. In that case, we must manually simulate what 447 ;; 'throw' in process_quit_flag would do, and we must 448 ;; reset quit-flag, because leaving it set will cause us 449 ;; quit to top-level, which has undesirable consequences, 450 ;; such as discarding input etc. We return t in that case 451 ;; because input did arrive during execution of BODY. 452 ((eq quit-flag throw-on-input) 453 (setq quit-flag nil) 454 t) 455 ;; This is for when the user actually QUITs during 456 ;; execution of BODY. 457 (quit-flag 458 nil) 459 (t val))))))) 460 461 (defun aggressive-indent--maybe-cancel-timer () 462 "Cancel and remove the timer if it is set." 463 (when (timerp aggressive-indent--idle-timer) 464 (cancel-timer aggressive-indent--idle-timer) 465 (setq aggressive-indent--idle-timer nil))) 466 467 (defun aggressive-indent--indent-if-changed (buffer) 468 "Indent any region that changed in BUFFER in the last command loop." 469 (if (not (buffer-live-p buffer)) 470 (aggressive-indent--maybe-cancel-timer) 471 (with-current-buffer buffer 472 (when (and aggressive-indent-mode aggressive-indent--changed-list) 473 (save-excursion 474 (save-selected-window 475 (aggressive-indent--while-no-input 476 (aggressive-indent--process-changed-list-and-indent)))) 477 (aggressive-indent--maybe-cancel-timer))))) 478 479 (defun aggressive-indent--keep-track-of-changes (l r &rest _) 480 "Store the limits (L and R) of each change in the buffer." 481 (when aggressive-indent-mode 482 (push (list l r) aggressive-indent--changed-list) 483 (aggressive-indent--maybe-cancel-timer) 484 (setq aggressive-indent--idle-timer 485 (run-with-idle-timer aggressive-indent-sit-for-time t #'aggressive-indent--indent-if-changed (current-buffer))))) 486 487 ;;; Minor modes 488 ;;;###autoload 489 (define-minor-mode aggressive-indent-mode 490 nil nil " =>" 491 `((,(kbd "C-c C-q") . aggressive-indent-indent-defun) 492 ([backspace] 493 menu-item "maybe-delete-indentation" ignore :filter 494 (lambda (&optional _) 495 (when (and (looking-back "^[[:blank:]]+") 496 ;; Wherever we don't want to indent, we probably also 497 ;; want the default backspace behavior. 498 (not (run-hook-wrapped 'aggressive-indent--internal-dont-indent-if #'eval)) 499 (not (aggressive-indent--run-user-hooks))) 500 #'delete-indentation)))) 501 (if aggressive-indent-mode 502 (if (and global-aggressive-indent-mode 503 (or (cl-member-if #'derived-mode-p aggressive-indent-excluded-modes) 504 (equal indent-line-function #'indent-relative) 505 (derived-mode-p 'text-mode) 506 (eq major-mode 'fundamental-mode) 507 buffer-read-only)) 508 (aggressive-indent-mode -1) 509 ;; Should electric indent be ON or OFF? 510 (if (or (eq aggressive-indent-dont-electric-modes t) 511 (cl-member-if #'derived-mode-p aggressive-indent-dont-electric-modes)) 512 (aggressive-indent--local-electric nil) 513 (aggressive-indent--local-electric t)) 514 (add-hook 'after-change-functions #'aggressive-indent--keep-track-of-changes nil 'local) 515 (add-hook 'after-revert-hook #'aggressive-indent--clear-change-list nil 'local) 516 (add-hook 'before-save-hook #'aggressive-indent--process-changed-list-and-indent nil 'local) 517 (add-hook 'kill-buffer-hook #'aggressive-indent--maybe-cancel-timer nil 'local)) 518 ;; Clean the hooks 519 (aggressive-indent--maybe-cancel-timer) 520 (remove-hook 'after-change-functions #'aggressive-indent--keep-track-of-changes 'local) 521 (remove-hook 'after-revert-hook #'aggressive-indent--clear-change-list 'local) 522 (remove-hook 'before-save-hook #'aggressive-indent--process-changed-list-and-indent 'local) 523 (remove-hook 'post-command-hook #'aggressive-indent--softly-indent-defun 'local) 524 (remove-hook 'kill-buffer-hook #'aggressive-indent--maybe-cancel-timer 'local))) 525 526 (defun aggressive-indent--local-electric (on) 527 "Turn variable `electric-indent-mode' on or off locally, as per boolean ON." 528 (if (fboundp 'electric-indent-local-mode) 529 (electric-indent-local-mode (if on 1 -1)) 530 (set (make-local-variable 'electric-indent-mode) on))) 531 532 ;;;###autoload 533 (define-globalized-minor-mode global-aggressive-indent-mode 534 aggressive-indent-mode aggressive-indent-mode) 535 536 ;;;###autoload 537 (defalias 'aggressive-indent-global-mode 538 #'global-aggressive-indent-mode) 539 540 (provide 'aggressive-indent) 541 ;;; aggressive-indent.el ends here