1 ;;; adjust-parens.el --- Indent and dedent Lisp code, automatically adjust close parens -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2013 Free Software Foundation, Inc.
5 ;; Author: Barry O'Reilly <gundaetiapo@gmail.com>
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
23 ;; This package provides commands for indenting and dedenting Lisp
24 ;; code such that close parentheses and brackets are automatically
25 ;; adjusted to be consistent with the new level of indentation.
27 ;; When reading Lisp, the programmer pays attention to open parens and
28 ;; the close parens on the same line. But when a sexp spans more than
29 ;; one line, she deduces the close paren from indentation alone. Given
30 ;; that's how we read Lisp, this package aims to enable editing Lisp
31 ;; similarly: automatically adjust the close parens programmers ignore
32 ;; when reading. A result of this is an editing experience somewhat
33 ;; like python-mode, which also offers "indent" and "dedent" commands.
34 ;; There are differences because lisp-mode knows more due to existing
38 ;; (require 'adjust-parens)
39 ;; (add-hook 'emacs-lisp-mode-hook #'adjust-parens-mode)
40 ;; (add-hook 'clojure-mode-hook #'adjust-parens-mode)
43 ;; This binds two keys in Lisp Mode:
44 ;; (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
45 ;; (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)
47 ;; lisp-indent-adjust-parens potentially calls indent-for-tab-command
48 ;; (the usual binding for TAB in Lisp Mode). Thus it should not
49 ;; interfere with other TAB features like completion-at-point.
51 ;; Some examples follow. | indicates the position of point.
53 ;; (let ((x 10) (y (some-func 20))))
58 ;; (let ((x 10) (y (some-func 20)))
61 ;; After three more TAB:
63 ;; (let ((x 10) (y (some-func 20
66 ;; After two Shift-TAB to dedent:
68 ;; (let ((x 10) (y (some-func 20))
71 ;; When dedenting, the sexp may have sibling sexps on lines below. It
72 ;; makes little sense for those sexps to stay at the same indentation,
73 ;; because they cannot keep the same parent sexp without being moved
74 ;; completely. Thus they are dedented too. An example of this:
90 ;; If you indent again with TAB, the sexps siblings aren't indented:
98 ;; Thus TAB and Shift-TAB are not exact inverse operations of each
99 ;; other, though they often seem to be.
104 ;; - Consider taking a region as input in order to indent a sexp and
105 ;; its siblings in the region. Dedenting would not take a region.
109 (defun last-sexp-with-relative-depth (from-pos to-pos rel-depth)
110 "Parsing sexps from FROM-POS (inclusive) to TO-POS (exclusive),
111 return the position of the last sexp that had depth REL-DEPTH relative
112 to FROM-POS. Returns nil if REL-DEPTH is not reached.
117 Region: a (b c (d)) e (f g (h i)) j
119 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 0)
120 Returns: position of j
122 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 1)
123 Returns: position of (h i)
125 This function assumes FROM-POS is not in a string or comment."
128 (parse-state '(0 nil nil nil nil nil nil nil nil)))
129 (while (< (point) to-pos)
131 (parse-partial-sexp (point)
136 (and (not (eq (point) to-pos))
137 (eq (car parse-state) rel-depth)
138 (setq the-last-pos (point)))
139 ;; The previous parse may not advance. To advance and maintain
140 ;; correctness of depth, we parse over the next char.
141 (when (< (point) to-pos)
143 (parse-partial-sexp (point)
151 (defun adjust-parens-check-prior-sexp ()
152 "Returns true if there is a full sexp before point, else false.
155 (let ((pos1 (progn (backward-sexp)
157 (pos2 (progn (forward-sexp)
162 (defun adjust-close-paren-for-indent ()
163 "Adjust a close parentheses of a sexp so as
164 lisp-indent-adjust-parens can indent that many levels.
166 If a close paren was moved, returns a two element list of positions:
167 where the close paren was moved from and the position following where
170 If there's no close parens to move, either return nil or allow
171 scan-error to propogate up."
173 (let* ((deleted-paren-char nil)
177 ;; Account for edge case when point has no sexp before it
179 ;; This is primarily to avoid funny behavior when there
180 ;; is no sexp between bob and point.
181 (if (not (adjust-parens-check-prior-sexp))
183 ;; If the sexp at point is a list,
184 ;; delete its closing paren
185 (when (eq (scan-lists (point) 1 0)
186 (scan-sexps (point) 1))
188 (setq deleted-paren-char (char-before))
191 ;; Invariant: deleted-paren-pos nil iff deleted-paren-char nil
192 (when deleted-paren-pos
195 (last-sexp-with-relative-depth (point)
200 (goto-char sexp-to-close)
202 ;; Note: when no sexp-to-close found, line is empty. So put
203 ;; close paren after point.
204 (insert deleted-paren-char)
205 (list deleted-paren-pos (point)))))))
207 (defun adjust-close-paren-for-dedent ()
208 "Adjust a close parentheses of a sexp so as
209 lisp-dedent-adjust-parens can dedent that many levels.
211 If a close paren was moved, returns a two element list of positions:
212 where the close paren was moved from and the position following where
215 If there's no close parens to move, either return nil or allow
216 scan-error to propogate up."
218 (let* ((deleted-paren-char nil)
224 (setq deleted-paren-char (char-before))
227 ;; Invariant: deleted-paren-pos nil iff deleted-paren-char nil
228 (when deleted-paren-pos
230 ;; Needs to work when dedenting in an empty list, in
231 ;; which case backward-sexp will signal scan-error and
232 ;; sexp-to-close will be nil.
234 (progn (backward-sexp)
237 ;; Move point to where to insert close paren
242 (insert deleted-paren-char)
243 ;; The insertion makes deleted-paren-pos off by 1
244 (list (1+ deleted-paren-pos)
247 (defun adjust-parens-p ()
248 "Whether to adjust parens."
250 (let ((orig-pos (point)))
251 (back-to-indentation)
252 (and (= orig-pos (point))
254 ;; Current line indented?
255 (let ((indent (calculate-lisp-indent)))
262 (defun adjust-parens-and-indent (raw-parg
264 adjust-function-negative
266 "Adjust close parens and indent the region over which the parens
268 (if (adjust-parens-p)
269 (let* ((parg (prefix-numeric-value raw-parg))
270 (adjust-function (if (and parg (< parg 0))
271 adjust-function-negative
273 (region-of-change (list (point) (point))))
274 (cl-loop for i from 1 to (or (and parg (abs parg)) 1)
279 (let ((close-paren-movement
280 (funcall adjust-function)))
281 (if close-paren-movement
282 (setq region-of-change
283 (list (min (car region-of-change)
284 (car close-paren-movement)
285 (cadr close-paren-movement))
286 (max (cadr region-of-change)
287 (car close-paren-movement)
288 (cadr close-paren-movement))))
290 (scan-error (setq finished err))))
291 (apply 'indent-region region-of-change)
292 (back-to-indentation)
294 (funcall fallback-function raw-parg)))
296 (defcustom adjust-parens-fallback-indent-function 'indent-for-tab-command
297 "The function to call with prefix arg instead of
298 adjust-parens-and-indent when adjust-parens-p returns false."
300 :group 'adjust-parens)
301 (defun lisp-indent-adjust-parens (&optional raw-parg)
302 "Indent Lisp code to the next level while adjusting sexp balanced
303 expressions to be consistent.
305 Returns t if adjust-parens changed the buffer, else returns the
306 result of calling adjust-parens-fallback-indent-function.
308 This command can be bound to TAB instead of indent-for-tab-command. It
309 potentially calls the latter."
311 (adjust-parens-and-indent raw-parg
312 #'adjust-close-paren-for-indent
313 #'adjust-close-paren-for-dedent
314 adjust-parens-fallback-indent-function))
316 (defcustom adjust-parens-fallback-dedent-function 'indent-for-tab-command
317 "The function to call with prefix arg instead of
318 adjust-parens-and-indent when adjust-parens-p returns false."
320 :group 'adjust-parens)
321 (defun lisp-dedent-adjust-parens (&optional raw-parg)
322 "Dedent Lisp code to the previous level while adjusting sexp
323 balanced expressions to be consistent.
325 Returns t if adjust-parens changed the buffer, else returns the
326 result of calling adjust-parens-fallback-dedent-function.
328 Binding to <backtab> (ie Shift-Tab) is a sensible choice."
330 (adjust-parens-and-indent raw-parg
331 #'adjust-close-paren-for-dedent
332 #'adjust-close-paren-for-indent
333 adjust-parens-fallback-dedent-function))
335 (defgroup adjust-parens nil
336 "Indent and dedent Lisp code, automatically adjust close parens."
337 :prefix "adjust-parens-"
340 (defvar adjust-parens-mode-map (make-sparse-keymap)
341 "Keymap for `adjust-parens-mode'")
342 (define-key adjust-parens-mode-map (kbd "TAB") 'lisp-indent-adjust-parens)
343 (define-key adjust-parens-mode-map (kbd "<backtab>") 'lisp-dedent-adjust-parens)
345 (define-minor-mode adjust-parens-mode
346 "Indent and dedent Lisp code, automatically adjust close parens."
347 :group 'adjust-parens
348 :keymap adjust-parens-mode-map)
350 (provide 'adjust-parens)
352 ;;; adjust-parens.el ends here