]> code.delx.au - gnu-emacs-elpa/blob - packages/adjust-parens/adjust-parens.el
* Makefile: New file to provide 'make check' tests
[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.1
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
104 (require 'cl)
105
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.
110
111 May change point.
112
113 Examples:
114 Region: a (b c (d)) e (f g (h i)) j
115
116 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 0)
117 Returns: position of j
118
119 Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) -1)
120 Returns: position of (h i)
121
122 This function assumes FROM-POS is not in a string or comment."
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 (when (< (point) to-pos)
139 (setq parse-state
140 (parse-partial-sexp (point)
141 (1+ (point))
142 nil
143 nil
144 parse-state))))
145 the-last-pos))
146
147
148 (defun adjust-parens-check-prior-sexp ()
149 "Returns true if there is a full sexp before point, else false.
150
151 May change point."
152 (let ((pos1 (progn (backward-sexp)
153 (point)))
154 (pos2 (progn (forward-sexp)
155 (backward-sexp)
156 (point))))
157 (>= pos1 pos2)))
158
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.
162
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
165 it moved to.
166
167 If there's no close parens to move, either return nil or allow
168 scan-error to propogate up."
169 (save-excursion
170 (let ((deleted-paren-pos
171 (save-excursion
172 (beginning-of-line)
173 ;; Account for edge case when point has no sexp before it
174 ;;
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))
178 nil
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))
183 (forward-sexp)
184 (delete-char -1)
185 (point))))))
186 (when deleted-paren-pos
187 (let ((sexp-to-close
188 (save-excursion
189 (last-sexp-with-relative-depth (point)
190 (progn (end-of-line)
191 (point))
192 0))))
193 (when sexp-to-close
194 (goto-char sexp-to-close)
195 (forward-sexp))
196 ;; Note: when no sexp-to-close found, line is empty. So put
197 ;; close paren after point.
198 (insert ")")
199 (list deleted-paren-pos (point)))))))
200
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.
204
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
207 it moved to.
208
209 If there's no close parens to move, either return nil or allow
210 scan-error to propogate up."
211 (save-excursion
212 (let ((deleted-paren-pos
213 (save-excursion
214 (when (< (point)
215 (progn (up-list)
216 (point)))
217 (delete-char -1)
218 (point)))))
219 (when deleted-paren-pos
220 (let ((sexp-to-close
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.
224 (condition-case nil
225 (progn (backward-sexp)
226 (point))
227 (scan-error nil))))
228 ;; Move point to where to insert close paren
229 (if sexp-to-close
230 (forward-sexp)
231 (backward-up-list)
232 (forward-char 1))
233 (insert ")")
234 ;; The insertion makes deleted-paren-pos off by 1
235 (list (1+ deleted-paren-pos)
236 (point)))))))
237
238 (defun adjust-parens-p ()
239 "Whether to adjust parens."
240 (save-excursion
241 (let ((orig-pos (point)))
242 (back-to-indentation)
243 (and (not (use-region-p))
244 (<= orig-pos (point))))))
245
246 (defun adjust-parens-and-indent (adjust-function parg)
247 "Adjust close parens and indent the region over which the parens
248 moved."
249 (let ((region-of-change (list (point) (point))))
250 (cl-loop for i from 1 to (or parg 1)
251 with finished = nil
252 while (not finished)
253 do
254 (condition-case err
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))))
265 (setq finished t)))
266 (scan-error (setq finished err))))
267 (apply 'indent-region region-of-change))
268 (back-to-indentation))
269
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.
273
274 This command can be bound to TAB instead of indent-for-tab-command. It
275 potentially calls the latter."
276 (interactive "P")
277 (if (adjust-parens-p)
278 (adjust-parens-and-indent 'adjust-close-paren-for-indent
279 parg)
280 (indent-for-tab-command parg)))
281
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.
285
286 Binding to <backtab> (ie Shift-Tab) is a sensible choice."
287 (interactive "P")
288 (when (adjust-parens-p)
289 (adjust-parens-and-indent 'adjust-close-paren-for-dedent
290 parg)))
291
292 (add-hook 'emacs-lisp-mode-hook
293 (lambda ()
294 (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
295 (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)))
296
297 (provide 'adjust-parens)
298
299 ;;; adjust-parens.el ends here