]> code.delx.au - gnu-emacs-elpa/blob - packages/adjust-parens/adjust-parens.el
Merge commit '0cda39255827f283e7578cd469ae42daad9556a2' from js2-mode
[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: 3.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 and brackets are automatically
25 ;; adjusted to be 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 ;; (add-hook 'emacs-lisp-mode-hook #'adjust-parens-mode)
40 ;; (add-hook 'clojure-mode-hook #'adjust-parens-mode)
41 ;; ;; etc
42 ;;
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)
46 ;;
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.
50 ;;
51 ;; Some examples follow. | indicates the position of point.
52 ;;
53 ;; (let ((x 10) (y (some-func 20))))
54 ;; |
55 ;;
56 ;; After one TAB:
57 ;;
58 ;; (let ((x 10) (y (some-func 20)))
59 ;; |)
60 ;;
61 ;; After three more TAB:
62 ;;
63 ;; (let ((x 10) (y (some-func 20
64 ;; |))))
65 ;;
66 ;; After two Shift-TAB to dedent:
67 ;;
68 ;; (let ((x 10) (y (some-func 20))
69 ;; |))
70 ;;
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:
75 ;;
76 ;; (defun func ()
77 ;; (save-excursion
78 ;; (other-func-1)
79 ;; |(other-func-2)
80 ;; (other-func-3)))
81 ;;
82 ;; After Shift-TAB:
83 ;;
84 ;; (defun func ()
85 ;; (save-excursion
86 ;; (other-func-1))
87 ;; |(other-func-2)
88 ;; (other-func-3))
89 ;;
90 ;; If you indent again with TAB, the sexps siblings aren't indented:
91 ;;
92 ;; (defun func ()
93 ;; (save-excursion
94 ;; (other-func-1)
95 ;; |(other-func-2))
96 ;; (other-func-3))
97 ;;
98 ;; Thus TAB and Shift-TAB are not exact inverse operations of each
99 ;; other, though they often seem to be.
100
101 ;;; Code:
102
103 ;; Future work:
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.
106
107 (require 'cl-lib)
108
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.
113
114 May change point.
115
116 Examples:
117 Region: a (b c (d)) e (f g (h i)) j
118
119 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 0)
120 Returns: position of j
121
122 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 1)
123 Returns: position of (h i)
124
125 This function assumes FROM-POS is not in a string or comment."
126 (goto-char from-pos)
127 (let (the-last-pos
128 (parse-state '(0 nil nil nil nil nil nil nil nil)))
129 (while (< (point) to-pos)
130 (setq parse-state
131 (parse-partial-sexp (point)
132 to-pos
133 nil
134 t ; Stop before sexp
135 parse-state))
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)
142 (setq parse-state
143 (parse-partial-sexp (point)
144 (1+ (point))
145 nil
146 nil
147 parse-state))))
148 the-last-pos))
149
150
151 (defun adjust-parens-check-prior-sexp ()
152 "Returns true if there is a full sexp before point, else false.
153
154 May change point."
155 (let ((pos1 (progn (backward-sexp)
156 (point)))
157 (pos2 (progn (forward-sexp)
158 (backward-sexp)
159 (point))))
160 (>= pos1 pos2)))
161
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.
165
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
168 it moved to.
169
170 If there's no close parens to move, either return nil or allow
171 scan-error to propogate up."
172 (save-excursion
173 (let* ((deleted-paren-char nil)
174 (deleted-paren-pos
175 (save-excursion
176 (beginning-of-line)
177 ;; Account for edge case when point has no sexp before it
178 ;;
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))
182 nil
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))
187 (forward-sexp)
188 (setq deleted-paren-char (char-before))
189 (delete-char -1)
190 (point))))))
191 ;; Invariant: deleted-paren-pos nil iff deleted-paren-char nil
192 (when deleted-paren-pos
193 (let ((sexp-to-close
194 (save-excursion
195 (last-sexp-with-relative-depth (point)
196 (progn (end-of-line)
197 (point))
198 0))))
199 (when sexp-to-close
200 (goto-char sexp-to-close)
201 (forward-sexp))
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)))))))
206
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.
210
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
213 it moved to.
214
215 If there's no close parens to move, either return nil or allow
216 scan-error to propogate up."
217 (save-excursion
218 (let* ((deleted-paren-char nil)
219 (deleted-paren-pos
220 (save-excursion
221 (when (< (point)
222 (progn (up-list)
223 (point)))
224 (setq deleted-paren-char (char-before))
225 (delete-char -1)
226 (point)))))
227 ;; Invariant: deleted-paren-pos nil iff deleted-paren-char nil
228 (when deleted-paren-pos
229 (let ((sexp-to-close
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.
233 (condition-case nil
234 (progn (backward-sexp)
235 (point))
236 (scan-error nil))))
237 ;; Move point to where to insert close paren
238 (if sexp-to-close
239 (forward-sexp)
240 (backward-up-list)
241 (forward-char 1))
242 (insert deleted-paren-char)
243 ;; The insertion makes deleted-paren-pos off by 1
244 (list (1+ deleted-paren-pos)
245 (point)))))))
246
247 (defun adjust-parens-p ()
248 "Whether to adjust parens."
249 (save-excursion
250 (let ((orig-pos (point)))
251 (back-to-indentation)
252 (and (= orig-pos (point))
253 (not (use-region-p))
254 ;; Current line indented?
255 (let ((indent (calculate-lisp-indent)))
256 (and indent
257 (= (current-column)
258 (if (listp indent)
259 (car indent)
260 indent))))))))
261
262 (defun adjust-parens-and-indent (raw-parg
263 adjust-function
264 adjust-function-negative
265 fallback-function)
266 "Adjust close parens and indent the region over which the parens
267 moved."
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
272 adjust-function))
273 (region-of-change (list (point) (point))))
274 (cl-loop for i from 1 to (or (and parg (abs parg)) 1)
275 with finished = nil
276 while (not finished)
277 do
278 (condition-case err
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))))
289 (setq finished t)))
290 (scan-error (setq finished err))))
291 (apply 'indent-region region-of-change)
292 (back-to-indentation)
293 t)
294 (funcall fallback-function raw-parg)))
295
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."
299 :type 'function
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.
304
305 Returns t if adjust-parens changed the buffer, else returns the
306 result of calling adjust-parens-fallback-indent-function.
307
308 This command can be bound to TAB instead of indent-for-tab-command. It
309 potentially calls the latter."
310 (interactive "P")
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))
315
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."
319 :type 'function
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.
324
325 Returns t if adjust-parens changed the buffer, else returns the
326 result of calling adjust-parens-fallback-dedent-function.
327
328 Binding to <backtab> (ie Shift-Tab) is a sensible choice."
329 (interactive "P")
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))
334
335 (defgroup adjust-parens nil
336 "Indent and dedent Lisp code, automatically adjust close parens."
337 :prefix "adjust-parens-"
338 :group 'convenience)
339
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)
344
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)
349
350 (provide 'adjust-parens)
351
352 ;;; adjust-parens.el ends here