]> code.delx.au - gnu-emacs-elpa/blob - packages/adjust-parens/adjust-parens.el
912f7ea21d67f4a9bf0c490fe6406bfbc97b101f
[gnu-emacs-elpa] / packages / adjust-parens / adjust-parens.el
1 ;;; adjust-parens.el --- Indent and dedent Lisp code, automatically adjust close parens -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2013 Free Software Foundation, Inc.
4
5 ;; Author: Barry O'Reilly <gundaetiapo@gmail.com>
6 ;; Version: 1.0
7
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.
12
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.
17
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/>.
20
21 ;;; Commentary:
22 ;;
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.
26 ;;
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
35 ;; parens.
36 ;;
37 ;; To use:
38 ;; (require 'adjust-parens)
39 ;;
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)
43 ;;
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.
47 ;;
48 ;; Some examples follow. | indicates the position of point.
49 ;;
50 ;; (let ((x 10) (y (some-func 20))))
51 ;; |
52 ;;
53 ;; After one TAB:
54 ;;
55 ;; (let ((x 10) (y (some-func 20)))
56 ;; |)
57 ;;
58 ;; After three more TAB:
59 ;;
60 ;; (let ((x 10) (y (some-func 20
61 ;; |))))
62 ;;
63 ;; After two Shift-TAB to dedent:
64 ;;
65 ;; (let ((x 10) (y (some-func 20))
66 ;; |))
67 ;;
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:
72 ;;
73 ;; (defun func ()
74 ;; (save-excursion
75 ;; (other-func-1)
76 ;; |(other-func-2)
77 ;; (other-func-3)))
78 ;;
79 ;; After Shift-TAB:
80 ;;
81 ;; (defun func ()
82 ;; (save-excursion
83 ;; (other-func-1))
84 ;; |(other-func-2)
85 ;; (other-func-3))
86 ;;
87 ;; If you indent again with TAB, the sexps siblings aren't indented:
88 ;;
89 ;; (defun func ()
90 ;; (save-excursion
91 ;; (other-func-1)
92 ;; |(other-func-2))
93 ;; (other-func-3))
94 ;;
95 ;; Thus TAB and Shift-TAB are not exact inverse operations of each
96 ;; other, though they often seem to be.
97
98 ;;; Code:
99
100 ;; Future work:
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.
103 ;; - Write tests
104
105 (require 'cl)
106
107 (defun last-sexp-with-relative-depth (from-pos to-pos rel-depth)
108 "Parsing sexps from FROM-POS (inclusive) to TO-POS (exclusive),
109 return the position of the last sexp that had depth REL-DEPTH relative
110 to FROM-POS. Returns nil if REL-DEPTH is not reached.
111
112 Examples:
113 Region: a (b c (d)) e (f g (h i)) j
114
115 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 0)
116 Returns: position of j
117
118 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) -1)
119 Returns: position of (h i)
120
121 This function assumes FROM-POS is not in a string or comment."
122 (save-excursion
123 (goto-char from-pos)
124 (let (the-last-pos
125 (parse-state '(0 nil nil nil nil nil nil nil nil)))
126 (while (< (point) to-pos)
127 (setq parse-state
128 (parse-partial-sexp (point)
129 to-pos
130 nil
131 t ; Stop before sexp
132 parse-state))
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 (setq parse-state
139 (parse-partial-sexp (point)
140 (1+ (point))
141 nil
142 nil
143 parse-state)))
144 the-last-pos)))
145
146 (defun adjust-close-paren-for-indent ()
147 "Adjust a close parentheses of a sexp so as
148 lisp-indent-adjust-parens can indent that many levels.
149
150 If a close paren was moved, returns a two element list of positions:
151 where the close paren was moved from and the position following where
152 it moved to.
153
154 If there's no close parens to move, either return nil or allow
155 scan-error to propogate up."
156 (save-excursion
157 (let ((deleted-paren-pos
158 (save-excursion
159 (beginning-of-line)
160 (backward-sexp)
161 ;; Account for edge case when point has no sexp before it
162 (if (bobp)
163 nil
164 ;; If the sexp at point is a list,
165 ;; delete its closing paren
166 (when (eq (scan-lists (point) 1 0)
167 (scan-sexps (point) 1))
168 (forward-sexp)
169 (delete-char -1)
170 (point))))))
171 (when deleted-paren-pos
172 (let ((sexp-to-close
173 (last-sexp-with-relative-depth (point)
174 (progn (end-of-line)
175 (point))
176 0)))
177 (when sexp-to-close
178 (goto-char sexp-to-close)
179 (forward-sexp))
180 ;; Note: when no sexp-to-close found, line is empty. So put
181 ;; close paren after point.
182 (insert ")")
183 (list deleted-paren-pos (point)))))))
184
185 (defun adjust-close-paren-for-dedent ()
186 "Adjust a close parentheses of a sexp so as
187 lisp-dedent-adjust-parens can dedent that many levels.
188
189 If a close paren was moved, returns a two element list of positions:
190 where the close paren was moved from and the position following where
191 it moved to.
192
193 If there's no close parens to move, either return nil or allow
194 scan-error to propogate up."
195 (save-excursion
196 (let ((deleted-paren-pos
197 (save-excursion
198 (when (< (point)
199 (progn (up-list)
200 (point)))
201 (delete-char -1)
202 (point)))))
203 (when deleted-paren-pos
204 (let ((sexp-to-close
205 ;; Needs to work when dedenting in an empty list, in
206 ;; which case backward-sexp will signal scan-error and
207 ;; sexp-to-close will be nil.
208 (condition-case nil
209 (progn (backward-sexp)
210 (point))
211 (scan-error nil))))
212 ;; Move point to where to insert close paren
213 (if sexp-to-close
214 (forward-sexp)
215 (backward-up-list)
216 (forward-char 1))
217 (insert ")")
218 ;; The insertion makes deleted-paren-pos off by 1
219 (list (1+ deleted-paren-pos)
220 (point)))))))
221
222 (defun adjust-parens-p ()
223 "Whether to adjust parens."
224 (save-excursion
225 (let ((orig-pos (point)))
226 (back-to-indentation)
227 (and (not (use-region-p))
228 (<= orig-pos (point))))))
229
230 (defun adjust-parens-and-indent (adjust-function prefix-arg)
231 "Adjust close parens and indent the region over which the parens
232 moved."
233 (let ((region-of-change (list (point) (point))))
234 (cl-loop for i from 1 to (or prefix-arg 1)
235 with finished = nil
236 while (not finished)
237 do
238 (condition-case err
239 (let ((close-paren-movement
240 (funcall adjust-function)))
241 (if close-paren-movement
242 (setq region-of-change
243 (list (min (car region-of-change)
244 (car close-paren-movement)
245 (cadr close-paren-movement))
246 (max (cadr region-of-change)
247 (car close-paren-movement)
248 (cadr close-paren-movement))))
249 (setq finished t)))
250 (scan-error (setq finished err))))
251 (apply 'indent-region region-of-change))
252 (back-to-indentation))
253
254 (defun lisp-indent-adjust-parens (&optional prefix-arg)
255 "Indent Lisp code to the next level while adjusting sexp balanced
256 expressions to be consistent.
257
258 This command can be bound to TAB instead of indent-for-tab-command. It
259 potentially calls the latter."
260 (interactive "P")
261 (if (adjust-parens-p)
262 (adjust-parens-and-indent 'adjust-close-paren-for-indent
263 prefix-arg)
264 (indent-for-tab-command prefix-arg)))
265
266 (defun lisp-dedent-adjust-parens (&optional prefix-arg)
267 "Dedent Lisp code to the previous level while adjusting sexp
268 balanced expressions to be consistent.
269
270 Binding to <backtab> (ie Shift-Tab) is a sensible choice."
271 (interactive "P")
272 (when (adjust-parens-p)
273 (adjust-parens-and-indent 'adjust-close-paren-for-dedent
274 prefix-arg)))
275
276 (add-hook 'emacs-lisp-mode-hook
277 (lambda ()
278 (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
279 (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)))
280
281 (provide 'adjust-parens)
282
283 ;;; adjust-parens.el ends here