]> code.delx.au - gnu-emacs-elpa/blob - packages/adjust-parens/adjust-parens.el
Merge branch 'master' of github.com:leoliu/ggtags
[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.3
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 ;;
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-lib)
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-char nil)
171 (deleted-paren-pos
172 (save-excursion
173 (beginning-of-line)
174 ;; Account for edge case when point has no sexp before it
175 ;;
176 ;; This is primarily to avoid funny behavior when there
177 ;; is no sexp between bob and point.
178 (if (not (adjust-parens-check-prior-sexp))
179 nil
180 ;; If the sexp at point is a list,
181 ;; delete its closing paren
182 (when (eq (scan-lists (point) 1 0)
183 (scan-sexps (point) 1))
184 (forward-sexp)
185 (setq deleted-paren-char (char-before))
186 (delete-char -1)
187 (point))))))
188 ;; Invariant: deleted-paren-pos nil iff deleted-paren-char nil
189 (when deleted-paren-pos
190 (let ((sexp-to-close
191 (save-excursion
192 (last-sexp-with-relative-depth (point)
193 (progn (end-of-line)
194 (point))
195 0))))
196 (when sexp-to-close
197 (goto-char sexp-to-close)
198 (forward-sexp))
199 ;; Note: when no sexp-to-close found, line is empty. So put
200 ;; close paren after point.
201 (insert deleted-paren-char)
202 (list deleted-paren-pos (point)))))))
203
204 (defun adjust-close-paren-for-dedent ()
205 "Adjust a close parentheses of a sexp so as
206 lisp-dedent-adjust-parens can dedent that many levels.
207
208 If a close paren was moved, returns a two element list of positions:
209 where the close paren was moved from and the position following where
210 it moved to.
211
212 If there's no close parens to move, either return nil or allow
213 scan-error to propogate up."
214 (save-excursion
215 (let* ((deleted-paren-char nil)
216 (deleted-paren-pos
217 (save-excursion
218 (when (< (point)
219 (progn (up-list)
220 (point)))
221 (setq deleted-paren-char (char-before))
222 (delete-char -1)
223 (point)))))
224 ;; Invariant: deleted-paren-pos nil iff deleted-paren-char nil
225 (when deleted-paren-pos
226 (let ((sexp-to-close
227 ;; Needs to work when dedenting in an empty list, in
228 ;; which case backward-sexp will signal scan-error and
229 ;; sexp-to-close will be nil.
230 (condition-case nil
231 (progn (backward-sexp)
232 (point))
233 (scan-error nil))))
234 ;; Move point to where to insert close paren
235 (if sexp-to-close
236 (forward-sexp)
237 (backward-up-list)
238 (forward-char 1))
239 (insert deleted-paren-char)
240 ;; The insertion makes deleted-paren-pos off by 1
241 (list (1+ deleted-paren-pos)
242 (point)))))))
243
244 (defun adjust-parens-p ()
245 "Whether to adjust parens."
246 (save-excursion
247 (let ((orig-pos (point)))
248 (back-to-indentation)
249 (and (not (use-region-p))
250 (<= orig-pos (point))))))
251
252 (defun adjust-parens-and-indent (adjust-function parg)
253 "Adjust close parens and indent the region over which the parens
254 moved."
255 (let ((region-of-change (list (point) (point))))
256 (cl-loop for i from 1 to (or parg 1)
257 with finished = nil
258 while (not finished)
259 do
260 (condition-case err
261 (let ((close-paren-movement
262 (funcall adjust-function)))
263 (if close-paren-movement
264 (setq region-of-change
265 (list (min (car region-of-change)
266 (car close-paren-movement)
267 (cadr close-paren-movement))
268 (max (cadr region-of-change)
269 (car close-paren-movement)
270 (cadr close-paren-movement))))
271 (setq finished t)))
272 (scan-error (setq finished err))))
273 (apply 'indent-region region-of-change))
274 (back-to-indentation))
275
276 (defun lisp-indent-adjust-parens (&optional parg)
277 "Indent Lisp code to the next level while adjusting sexp balanced
278 expressions to be consistent.
279
280 This command can be bound to TAB instead of indent-for-tab-command. It
281 potentially calls the latter."
282 (interactive "P")
283 (if (adjust-parens-p)
284 (adjust-parens-and-indent 'adjust-close-paren-for-indent
285 parg)
286 (indent-for-tab-command parg)))
287
288 (defun lisp-dedent-adjust-parens (&optional parg)
289 "Dedent Lisp code to the previous level while adjusting sexp
290 balanced expressions to be consistent.
291
292 Binding to <backtab> (ie Shift-Tab) is a sensible choice."
293 (interactive "P")
294 (when (adjust-parens-p)
295 (adjust-parens-and-indent 'adjust-close-paren-for-dedent
296 parg)))
297
298 (add-hook 'emacs-lisp-mode-hook
299 (lambda ()
300 (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
301 (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)))
302
303 (provide 'adjust-parens)
304
305 ;;; adjust-parens.el ends here