summaryrefslogtreecommitdiff
path: root/paredit.el
blob: abc29e9e74b4cffc894f8bf0157fc4a5dc5c4eb0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
;;; -*- mode: emacs-lisp -*-

;;;;;; paredit: Parenthesis editing minor mode
;;;;;; Version 1

;;; Taylor Campbell wrote this code; he places it in the public domain.

;;; Add this to your .emacs after adding paredit.el to /path/to/elisp/:
;;;
;;;   (add-to-list 'load-path "/path/to/elisp/")
;;;   (require 'paredit)
;;;   (add-hook '...-mode-hook (lambda () (paredit-mode 1)))
;;;
;;; Usually the ... will be lisp or scheme or both.

;;; This assumes Unix-style LF line endings.

(defconst paredit-version 1)

(defvar paredit-mode-map
  (let ((keymap (make-sparse-keymap)))
    (define-key keymap "(" 'paredit-open-list)
    (define-key keymap ")" 'paredit-close-list)
    (define-key keymap "\"" 'paredit-doublequote)
    (define-key keymap (kbd "M-\"") 'paredit-insert-doublequote)

    (define-key keymap (kbd "RET") 'newline-and-indent)
    (define-key keymap (kbd "DEL") 'paredit-backspace)
    
    (define-key keymap (kbd "C-k") 'paredit-kill)

    ;; In case something broke and you really, really need to insert a
    ;; literal parenthesis.  Don't use these often.
    (define-key keymap (kbd "C-M-(")
      (lambda () (interactive) (insert "(")))
    (define-key keymap (kbd "C-M-)")
      (lambda () (interactive) (insert ")")))

    ;; C-up & C-down are by default useless paragraph commands, while
    ;; C-M-up & C-M-down are BACKWARD-UP-LIST & BACKWARD-DOWN-LIST.
    (define-key keymap (kbd "<C-up>") 'up-list)
    (define-key keymap (kbd "<C-down>") 'down-list)

    (define-key keymap (kbd "M-(") 'forward-wrap-sexp)
    (define-key keymap (kbd "M-)") 'backward-wrap-sexp)
    (define-key keymap (kbd "M-/") 'splice-sexp)
    (define-key keymap (kbd "M-\\") 'join-sexps)

    (define-key keymap (kbd "C-)") 'forward-slurp-sexp)
    (define-key keymap (kbd "C-}") 'forward-barf-sexp)
    (define-key keymap (kbd "C-(") 'backward-slurp-sexp)
    (define-key keymap (kbd "C-{") 'backward-barf-sexp)
    
    keymap)
  "Keymap for paredit minor mode.")

(define-minor-mode paredit-mode
  "Minor mode for pseudo-structurally editing Lisp code."
  :lighter " Paredit")



;;; ----------------
;;; Basic editing commands

(defun paredit-open-list ()
  "Inserts a balanced parenthesis pair.
If in string, comment, or character literal, inserts a single opening
parenthesis."
  (interactive)
  (if (or (paredit-in-comment-p)
          (paredit-in-string-p)
          (eq (char-before) ?\\ ))
      (insert "(")
    (insert-parentheses 0)))

(defun paredit-close-list ()
  "Moves past one closing parenthesis and reindents.
If in a string, comment, or character literal, inserts a single closing
parenthesis."
  (interactive)
  (if (or (paredit-in-comment-p)
          (paredit-in-string-p)
          (eq (char-before) ?\\ ))
      (insert ")")
    (move-past-close-and-reindent)))

(defun paredit-doublequote ()
  "Inserts a pair of double-quotes.
Inside a comment or character literal, inserts a literal double-quote.
Inside a string, moves to the end of the string."
  (interactive)
  (cond ((or (paredit-in-comment-p)
             (eq (char-before) ?\\ ))
         ;; Special case for when we're on character literals, just to
         ;; be convenient.
         (insert "\""))
        ((paredit-in-string-p)
         (while (not (eq (char-after) ?\" ))
           (forward-char)
           (if (eq (char-after) ?\\ )   ; Skip escaped characters.
               (forward-char)))
         (forward-char))
        (t (insert-pair 0 ?\" ?\"))))

(defun paredit-insert-doublequote (&optional n)
  "Inserts a single literal double-quote.
Inside a string, inserts an escaped double-quote: \\\"
Outside of a string, comment, or character literal, displays a message
to the user and inserts a single literal double-quote nevertheless."
  (interactive "p")
  (let ((string
         (cond ((or (paredit-in-comment-p)
                    (eq (char-before) ?\\ ))
                "\"")                   ; plain doublequote
               ((paredit-in-string-p)
                "\\\"")                 ; escaped doublequote
               (t (message "Inserting naked doublequote%s..."
                           (if (> n 1) "s" ""))
                  "\""))))              ; plain doublequote
    (while (< 0 n)
      (insert string)
      (setq n (1- n)))))

(defun paredit-backspace ()
  "Deletes a character backward or moves backward over a delimiter.
If at the start of an empty string literal, deletes the whole string,
including both delimiters.  If at the start of a non-empty string
literal, moves back outside of the string literal.  If anywhere else in
a string literal, deletes a single character.
If on a closing parenthesis, moves backward one character.
If on an opening parenthesis followed by a closing parenthesis, deletes
both parentheses.
If on any other opening parenthesis, does nothing.
Anywhere else, deletes a character backward."
  (interactive)
  (cond ((paredit-in-comment-p)
         (backward-delete-char 1))
        ((paredit-in-string-p)
         (if (and (eq (char-after) ?\" )
                  (eq (char-before) ?\" )
                  (not (eq (char-before (1- (point)))
                           ?\\ )))
             (progn (backward-char)
                    (kill-sexp))
           (backward-delete-char 1)))
        ((and (or (eq (char-before) ?\) )
                  (eq (char-before) ?\" ))
              (not (eq (char-before (1- (point)))
                       ?\\ )))
         (backward-char))
        ;++ This should test more thoroughly, e.g. for (   ).
        ((and (eq (char-before) ?\( )
              (eq (char-after)  ?\) ))
         (backward-char)
         (kill-sexp))
        ;; Delete it, unless it's an opening parenthesis not preceded
        ;; by a backslash (i.e. not a character literal).
        ((or (not (eq (char-before) ?\( ))
             (eq (char-before (1- (point)))
                 ?\\ ))
         (backward-delete-char-untabify 1))))

(defun paredit-kill ()
  "Kills a line or S-expression.
If an S-expression starts on the same line as the point, kills that
S-expression; otherwise, behaves as `kill-line'."
  (interactive)
  (cond ((or (eq (char-after) ?\n )
             (paredit-in-comment-p)
             (save-excursion
               (skip-chars-forward " \t\n" (point-at-eol))
               (or (eq (point) (point-at-eol))
                   (eq (char-after) ?\; ))))
         (kill-line))
        ((paredit-in-string-p)
         (if (eq (char-after) ?\n )
             ;; Delete the newline only if we're at the end of the
             ;; line.  (The ordinary Emacs behaviour is to do this also
             ;; if there's only whitespace following, but I hate that
             ;; behaviour.)
             (delete-char)
           (while (not (or (eq (char-after) ?\n )
                           (eq (char-after) ?\" )))
             (cond ((eq (char-after) ?\\ )
                    (delete-char)
                    ;; The one after the backslash is escaped, so eat
                    ;; it (most importantly if it's a doublequote),
                    ;; unless it's a newline.
                    (if (not (eq (char-after (1+ point))
                                 ?\n ))
                        (delete-char)))
                   (t (delete-char))))))
        (t (kill-sexp))))



;;; ----------------
;;; Wrappage, splicage, & joinage

(defun forward-wrap-sexp (&optional n)
  "Wraps the following S-expression in a list.
If a prefix argument N is given, N S-expressions are contained in the
list."
  (interactive "p")
  (insert-parentheses (or n 1))
  (save-excursion (beginning-of-defun)
                  (indent-sexp)))

(defun backward-wrap-sexp (&optional n)
  "Wraps the preceding S-expression in a list.
If a prefix argument N is given, N S-expressions are contained in the
list."
  (interactive "p")
  (insert-parentheses (- (or n 1)))
  (save-excursion (beginning-of-defun)
                  (indent-sexp)))

(defun splice-sexp ()
  "Splices the list the point is on by removing its delimiters."
  (interactive)
  (save-excursion
    (backward-up-list)                  ; Go up to the beginning...
    (save-excursion
      (forward-sexp)                    ; Go forward an expression, to
      (backward-delete-char 1))         ;   delete the end delimiter.
    (delete-char 1)                     ; ...to delete the open char.
    (beginning-of-defun)                ; Reindent, now that the
    (indent-sexp)))                     ;   structure has changed.

(defun join-sexps ()
  "Joins two adjacent S-expressions into one S-expression."
  (interactive)
  (save-excursion
    (backward-sexp)                     ; Go to the end of the
    (forward-sexp)                      ;   preceding expression.
    (backward-delete-char 1)            ; Delete the closing delimiter.
    (forward-sexp)                      ; Go to the start of the
    (backward-sexp)                     ;   following expression.
    (delete-char 1)                     ; Delete the opening delimiter.
    (beginning-of-defun)                ; Reindent the whole defun, now
    (indent-sexp)))                     ;   that its structure changed.



;;; ----------------
;;; Slurpage & barfage

;;; This shouldn't be here.

(defmacro ignore-errors (&rest body)
  `(condition-case nil (progn ,@body)
     (error nil)))

(defun forward-slurp-sexp (&optional n)
  "Adds the S-expression following the current list into that list by
moving the closing delimiter.
If a prefix argument N is given, N S-expressions are slurped into the
current list."
  (interactive "p")
  (save-excursion
    (up-list)                           ; Up to the end of the list to
    (let ((close (char-before)))        ;   save and delete the closing
      (backward-delete-char 1)          ;   delimiter.
      (ignore-errors (forward-sexp n))  ; Go to the end of the last exp
      (insert close))                   ;   to insert that delimiter.
    (beginning-of-defun)                ; Reindent the form, now that
    (indent-sexp)))                     ;   the structure has changed.

(defun forward-barf-sexp (&optional n)
  "Removes the last S-expression in the current list from that list by
moving the closing delimiter.
If a prefix argument N is given, the last N S-expressions are barfed
out of the current list."
  (interactive "p")
  (save-excursion
    (up-list)                           ; Up to the end of the list to
    (let ((close (char-before)))        ;   save and delete the closing
      (backward-delete-char 1)          ;   delimiter.
      (ignore-errors (backward-sexp n)) ; Go back to where we want to
      (skip-chars-backward " \t\n")     ;   insert the delimiter.
      (if (eq (point) (point-min))
          (error "Barfing all subexpressions with no open-paren?")
        (insert close)))
    (beginning-of-defun)                ; Reindent: structure has
    (indent-sexp)))                     ;   changed.

(defun backward-slurp-sexp (&optional n)
  "Adds the S-expression preceding the current list into that list by
moving the closing delimiter.
If a prefix argument N is given, N S-expressions are slurped into the
current list."
  (interactive "p")
  (save-excursion
    (backward-up-list)
    (let ((open (char-after)))
      (delete-char 1)
      (ignore-errors (backward-sexp n))
      (insert open))
    (beginning-of-defun)
    (indent-sexp)))

(defun backward-barf-sexp (&optional n)
  "Removes the first S-expression in the current list from that list by
moving the closing delimiter.
If a prefix argument N is given, the first N S-expressions are barfed
out of the current list."
  (interactive "p")
  (save-excursion
    (backward-up-list)
    (let ((open (char-after)))
      (delete-char 1)
      (ignore-errors (forward-sexp n))
      (skip-chars-forward " \t\n")
      (if (eq (point) (point-max))
          (error "Barfing all subexpressions with no close-paren?")
        (insert open)))
    (beginning-of-defun)
    (indent-sexp)))



;;; ----------------
;;; Two utility functions

(defun paredit-in-comment-p ()
  "True if the point is within a Lisp line comment."
  ;++ Make this work on block comments?
  (save-excursion
    ;; The third T argument to SEARCH-BACKWARD says to return NIL,
    ;; not to signal an error, if no match is found.
    (and (search-backward ";" (point-at-bol) t)
         (not (eq (char-before) ?\\ ))
         t)))

;;; Taken roughly from thingatpt.el.

(defun paredit-in-string-p ()
  "True if the point is within a double-quote-delimited string."
  (let ((orig (point)))
    (save-excursion
      (beginning-of-defun)
      (eq (nth 3 (parse-partial-sexp (point) orig))
          ?\" ))))



(provide 'paredit)