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 are automatically adjusted to be
25 ;; 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)
40 ;; This binds two keys in Lisp Mode:
41 ;; (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
42 ;; (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)
44 ;; lisp-indent-adjust-parens potentially calls indent-for-tab-command
45 ;; (the usual binding for TAB in Lisp Mode). Thus it should not
46 ;; interfere with other TAB features like completion-at-point.
48 ;; Some examples follow. | indicates the position of point.
50 ;; (let ((x 10) (y (some-func 20))))
55 ;; (let ((x 10) (y (some-func 20)))
58 ;; After three more TAB:
60 ;; (let ((x 10) (y (some-func 20
63 ;; After two Shift-TAB to dedent:
65 ;; (let ((x 10) (y (some-func 20))
68 ;; When dedenting, the sexp may have sibling sexps on lines below. It
69 ;; makes little sense for those sexps to stay at the same indentation,
70 ;; because they cannot keep the same parent sexp without being moved
71 ;; completely. Thus they are dedented too. An example of this:
87 ;; If you indent again with TAB, the sexps siblings aren't indented:
95 ;; Thus TAB and Shift-TAB are not exact inverse operations of each
96 ;; other, though they often seem to be.
101 ;; - Consider taking a region as input in order to indent a sexp and
102 ;; its siblings in the region. Dedenting would not take a region.
106 (defun last-sexp-with-relative-depth (from-pos to-pos rel-depth)
107 "Parsing sexps from FROM-POS (inclusive) to TO-POS (exclusive),
108 return the position of the last sexp that had depth REL-DEPTH relative
109 to FROM-POS. Returns nil if REL-DEPTH is not reached.
114 Region: a (b c (d)) e (f g (h i)) j
116 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 0)
117 Returns: position of j
119 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) -1)
120 Returns: position of (h i)
122 This function assumes FROM-POS is not in a string or comment."
125 (parse-state '(0 nil nil nil nil nil nil nil nil)))
126 (while (< (point) to-pos)
128 (parse-partial-sexp (point)
133 (and (not (eq (point) to-pos))
134 (eq (car parse-state) rel-depth)
135 (setq the-last-pos (point)))
136 ;; The previous parse may not advance. To advance and maintain
137 ;; correctness of depth, we parse over the next char.
138 (when (< (point) to-pos)
140 (parse-partial-sexp (point)
148 (defun adjust-parens-check-prior-sexp ()
149 "Returns true if there is a full sexp before point, else false.
152 (let ((pos1 (progn (backward-sexp)
154 (pos2 (progn (forward-sexp)
159 (defun adjust-close-paren-for-indent ()
160 "Adjust a close parentheses of a sexp so as
161 lisp-indent-adjust-parens can indent that many levels.
163 If a close paren was moved, returns a two element list of positions:
164 where the close paren was moved from and the position following where
167 If there's no close parens to move, either return nil or allow
168 scan-error to propogate up."
170 (let ((deleted-paren-pos
173 ;; Account for edge case when point has no sexp before it
175 ;; This is primarily to avoid funny behavior when there
176 ;; is no sexp between bob and point.
177 (if (not (adjust-parens-check-prior-sexp))
179 ;; If the sexp at point is a list,
180 ;; delete its closing paren
181 (when (eq (scan-lists (point) 1 0)
182 (scan-sexps (point) 1))
186 (when deleted-paren-pos
189 (last-sexp-with-relative-depth (point)
194 (goto-char sexp-to-close)
196 ;; Note: when no sexp-to-close found, line is empty. So put
197 ;; close paren after point.
199 (list deleted-paren-pos (point)))))))
201 (defun adjust-close-paren-for-dedent ()
202 "Adjust a close parentheses of a sexp so as
203 lisp-dedent-adjust-parens can dedent that many levels.
205 If a close paren was moved, returns a two element list of positions:
206 where the close paren was moved from and the position following where
209 If there's no close parens to move, either return nil or allow
210 scan-error to propogate up."
212 (let ((deleted-paren-pos
219 (when deleted-paren-pos
221 ;; Needs to work when dedenting in an empty list, in
222 ;; which case backward-sexp will signal scan-error and
223 ;; sexp-to-close will be nil.
225 (progn (backward-sexp)
228 ;; Move point to where to insert close paren
234 ;; The insertion makes deleted-paren-pos off by 1
235 (list (1+ deleted-paren-pos)
238 (defun adjust-parens-p ()
239 "Whether to adjust parens."
241 (let ((orig-pos (point)))
242 (back-to-indentation)
243 (and (not (use-region-p))
244 (<= orig-pos (point))))))
246 (defun adjust-parens-and-indent (adjust-function parg)
247 "Adjust close parens and indent the region over which the parens
249 (let ((region-of-change (list (point) (point))))
250 (cl-loop for i from 1 to (or parg 1)
255 (let ((close-paren-movement
256 (funcall adjust-function)))
257 (if close-paren-movement
258 (setq region-of-change
259 (list (min (car region-of-change)
260 (car close-paren-movement)
261 (cadr close-paren-movement))
262 (max (cadr region-of-change)
263 (car close-paren-movement)
264 (cadr close-paren-movement))))
266 (scan-error (setq finished err))))
267 (apply 'indent-region region-of-change))
268 (back-to-indentation))
270 (defun lisp-indent-adjust-parens (&optional parg)
271 "Indent Lisp code to the next level while adjusting sexp balanced
272 expressions to be consistent.
274 This command can be bound to TAB instead of indent-for-tab-command. It
275 potentially calls the latter."
277 (if (adjust-parens-p)
278 (adjust-parens-and-indent 'adjust-close-paren-for-indent
280 (indent-for-tab-command parg)))
282 (defun lisp-dedent-adjust-parens (&optional parg)
283 "Dedent Lisp code to the previous level while adjusting sexp
284 balanced expressions to be consistent.
286 Binding to <backtab> (ie Shift-Tab) is a sensible choice."
288 (when (adjust-parens-p)
289 (adjust-parens-and-indent 'adjust-close-paren-for-dedent
292 (add-hook 'emacs-lisp-mode-hook
294 (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
295 (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)))
297 (provide 'adjust-parens)
299 ;;; adjust-parens.el ends here