]> code.delx.au - gnu-emacs-elpa/blob - packages/el-search/el-search.el
el-search: add autoload cookies
[gnu-emacs-elpa] / packages / el-search / el-search.el
1 ;;; el-search.el --- Expression based incremental search for emacs-lisp-mode -*- lexical-binding: t -*-
2
3 ;; Copyright (C) 2015 Free Software Foundation, Inc
4
5 ;; Author: Michael Heerdegen <michael_heerdegen@web.de>
6 ;; Maintainer: Michael Heerdegen <michael_heerdegen@web.de>
7 ;; Created: 29 Jul 2015
8 ;; Keywords: lisp
9 ;; Compatibility: GNU Emacs 25
10 ;; Version: 0.0.2
11 ;; Package-Requires: ((emacs "25"))
12
13
14 ;; This file is not part of GNU Emacs.
15
16 ;; GNU Emacs is free software: you can redistribute it and/or modify
17 ;; it under the terms of the GNU General Public License as published by
18 ;; the Free Software Foundation, either version 3 of the License, or
19 ;; (at your option) any later version.
20
21 ;; GNU Emacs is distributed in the hope that it will be useful,
22 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ;; GNU General Public License for more details.
25
26 ;; You should have received a copy of the GNU General Public License
27 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
28
29
30 ;;; Commentary:
31
32 ;; The first version for trying! Feedback and improvement suggestions
33 ;; welcome.
34
35
36 ;; Introduction
37 ;; ============
38 ;;
39 ;;
40 ;; The main user entry point is the command `el-search-pattern'. It
41 ;; prompts for a `pcase' pattern and searches the current buffer for
42 ;; expressions that are matched by it when read. Point is put at the
43 ;; beginning of the expression found (unlike isearch).
44 ;;
45 ;; It doesn't matter how the code is actually formatted. Comments are
46 ;; ignored by the search, and strings are treated as objects, their
47 ;; contents are not being searched.
48 ;;
49 ;; Example 1: if you enter
50 ;;
51 ;; 97
52 ;;
53 ;; at the prompt, this will find any occurrence of the number 97 in
54 ;; the code, but not 977 or (+ 90 7) or "My string containing 97".
55 ;; But it will find anything `eq' to 97 after reading, e.g. #x61 or
56 ;; ?a.
57 ;;
58 ;;
59 ;; Example 2: If you enter the pattern
60 ;;
61 ;; `(defvar ,_)
62 ;;
63 ;; you search for all defvar forms that don't specify an init value.
64 ;;
65 ;; The following will search for defvar forms with a docstring whose
66 ;; first line is longer than 70 characters:
67 ;;
68 ;; `(defvar ,_ ,_
69 ;; ,(and s (guard (< 70 (length (car (split-string s "\n")))))))
70 ;;
71 ;;
72 ;;
73 ;; Convenience
74 ;; ===========
75 ;;
76 ;; For expression input, the minibuffer prompts here uses
77 ;; `emacs-lisp-mode'.
78 ;;
79 ;; When reading a search pattern in the minibuffer, the input is
80 ;; automatically wrapped into `(and expr ,(read input)). So, if you
81 ;; want to search a buffer for symbols that are defined in "cl-lib",
82 ;; you can use this pattern
83 ;;
84 ;; (guard (and (symbolp expr)
85 ;; (when-let ((file (symbol-file expr)))
86 ;; (string-match-p "cl-lib\\.elc?$" file))))
87 ;;
88 ;; without binding the variable `expr'.
89 ;;
90 ;;
91 ;; Replacing
92 ;; =========
93 ;;
94 ;; You can replace expressions with command `el-search-query-replace'.
95 ;; You are queried for a (pcase) pattern and a replacement expression.
96 ;; For each match of the pattern, the replacement expression is
97 ;; evaluated with the bindings created by the pcase matching in
98 ;; effect, and printed to produce the replacement string.
99 ;;
100 ;; Example: In some buffer you want to swap the two expressions at the
101 ;; places of the first two arguments in all calls of function `foo',
102 ;; so that e.g.
103 ;;
104 ;; (foo 'a (* 2 (+ 3 4)) t)
105 ;;
106 ;; becomes
107 ;;
108 ;; (foo (* 2 (+ 3 4)) 'a t).
109 ;;
110 ;; This will do it:
111 ;;
112 ;; M-x el-search-query-replace RET
113 ;; `(foo ,a ,b . ,rest) RET
114 ;; `(foo ,b ,a . ,rest) RET
115 ;;
116 ;; Type y to replace a match and go to the next one, r to replace
117 ;; without moving, SPC to go to the next match and ! to replace all
118 ;; remaining matches automatically. q quits. n is like SPC, so that
119 ;; y and n work like in isearch (meaning "yes" and "no") if you are
120 ;; used to that.
121 ;;
122 ;;
123 ;;
124 ;; Suggested key bindings
125 ;; ======================
126 ;;
127 ;; (define-key emacs-lisp-mode-map [(control ?S)] #'el-search-pattern)
128 ;; (define-key emacs-lisp-mode-map [(control ?%)] #'el-search-query-replace)
129 ;;
130 ;; (define-key isearch-mode-map [(control ?S)] #'el-search-search-from-isearch)
131 ;; (define-key isearch-mode-map [(control ?%)] #'el-search-replace-from-isearch)
132 ;;
133 ;; The bindings in `isearch-mode-map' let you conveniently switch to
134 ;; elisp searching from isearch.
135 ;;
136 ;;
137 ;; Bugs, Known Limitations
138 ;;
139 ;;
140 ;; - Replacing: in some cases the reader syntax of forms
141 ;; is changing due to reading+printing. "Some" because we can treat
142 ;; that problem in most cases.
143 ;;
144 ;; - Similarly: Comments are normally preserved (where it makes
145 ;; sense). But when replacing like `(foo ,a ,b) -> `(foo ,b ,a)
146 ;;
147 ;; in a content like
148 ;;
149 ;; (foo
150 ;; a
151 ;; ;;a comment
152 ;; b)
153 ;;
154 ;; the comment will be lost.
155 ;;
156 ;;
157 ;;
158 ;; TODO:
159 ;;
160 ;; - improve docstrings
161 ;;
162 ;; - Implement sessions; add multi-file support based on iterators. A
163 ;; file list is read in (or the user can specify an iterator as a
164 ;; variable). The state in the current buffer is just (buffer
165 ;; . marker). Or should this be abstracted into an own lib? Could be
166 ;; named "files-session" or so.
167
168
169
170 ;;; Code:
171
172 ;;;; Requirements
173
174 (eval-when-compile
175 (require 'subr-x))
176
177 (require 'cl-lib)
178 (require 'elisp-mode)
179 (require 'thingatpt)
180
181
182 ;;;; Configuration stuff
183
184 (defgroup el-search nil
185 "Expression based search and replace for `emacs-lisp-mode'."
186 :group 'lisp)
187
188 (defcustom el-search-this-expression-identifier 'expr
189 "Name of the identifier referring to the whole expression.
190 The default value is `expr'. You can use this variable in the
191 search prompt to refer to value of the currently searched
192 expression."
193 :type 'symbol)
194
195 (defface el-search-match '((((background dark)) (:background "#0000A0"))
196 (t (:background "DarkSlateGray1")))
197 "Face for highlighting the current match.")
198
199
200 ;;;; Helpers
201
202 (defun el-search--print (expr)
203 (let ((print-quoted t)
204 (print-length nil)
205 (print-level nil))
206 (prin1-to-string expr)))
207
208 (defvar el-search-read-expression-map
209 (let ((map (make-sparse-keymap)))
210 (set-keymap-parent map read-expression-map)
211 (define-key map [(control ?g)] #'abort-recursive-edit)
212 (define-key map [up] nil)
213 (define-key map [down] nil)
214 (define-key map [(control meta backspace)] #'backward-kill-sexp)
215 (define-key map [(control ?S)] #'exit-minibuffer)
216 map)
217 "Map for reading input with `el-search-read-expression'.")
218
219 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
220 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
221 "Read expression for `my-eval-expression'."
222 (minibuffer-with-setup-hook
223 (lambda ()
224 (emacs-lisp-mode)
225 (use-local-map el-search-read-expression-map)
226 (setq font-lock-mode t)
227 (funcall font-lock-function 1)
228 (backward-sexp)
229 (indent-sexp)
230 (goto-char (point-max)))
231 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
232 (or hist 'read-expression-history) default)))
233
234 (defun el-search--read-pattern (prompt &optional default initial-contents read)
235 (el-search-read-expression
236 prompt initial-contents 'el-search-history
237 (or default (when-let ((this-sexp (sexp-at-point)))
238 (concat "'" (el-search--print this-sexp))))
239 read))
240
241 (defun el-search--goto-next-sexp ()
242 "Move point to the beginning of the next sexp.
243 Don't move if already at beginning of a sexp."
244 (let ((not-done t) res)
245 (while not-done
246 (let ((stop-here nil) syntax-here)
247 (while (not stop-here)
248 (cond
249 ((eobp) (signal 'end-of-buffer nil))
250 ((looking-at (rx (and (* space) ";"))) (forward-line))
251 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
252 ((progn (setq syntax-here (syntax-ppss))
253 (or (nth 4 syntax-here) (nth 8 syntax-here)))
254 (if (nth 4 syntax-here) (forward-line) (search-forward "\"")))
255
256 ;; FIXME: can the rest be done more generically?
257 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
258 (not (looking-at "\\_<"))
259 (not (looking-back ",@" 2)))
260 (forward-symbol 1))
261 ((or (and (looking-at "'") (looking-back "#" 1))
262 (and (looking-at "@") (looking-back "," 1)))
263 (forward-char))
264 (t (setq stop-here t)))))
265 (condition-case nil
266 (progn
267 (setq res (save-excursion (read (current-buffer))))
268 (setq not-done nil))
269 (error (forward-char))))
270 res))
271
272 (defun el-search--match-p (pattern expression)
273 (funcall
274 `(lambda ()
275 (pcase ',expression
276 (,pattern t)
277 (_ nil)))))
278
279 (defun el-search-expression-contains-match-p (pattern expression)
280 "Whether some subexp of EXPRESSION is matched by PATTERN."
281 (or (el-search--match-p pattern expression)
282 (and (consp expression)
283 (if (cdr (last expression))
284 ;; a dotted list
285 (or (el-search-expression-contains-match-p pattern (car expression))
286 (el-search-expression-contains-match-p pattern (cdr expression)))
287 (cl-some (lambda (subexpr) (el-search-expression-contains-match-p pattern subexpr))
288 expression)))))
289
290 (defun el-search--maybe-wrap-pattern (pattern)
291 (if (el-search-expression-contains-match-p `',el-search-this-expression-identifier pattern)
292 `(and ,el-search-this-expression-identifier ,pattern)
293 pattern))
294
295 (defun el-search--search-pattern (pattern &optional noerror)
296 "Search elisp buffer with `pcase' PATTERN.
297 Set point to the beginning of the occurrence found and return
298 point. Optional second argument, if non-nil, means if fail just
299 return nil (no error)."
300 ;; For better performance we read complete top-level sexps and test
301 ;; for matches. We enter top-level expressions in the buffer text
302 ;; only when the test was successful.
303 (let ((match-beg nil) (opoint (point)) current-expr)
304 (if (catch 'no-match
305 (while (not match-beg)
306 (condition-case nil
307 (setq current-expr (el-search--goto-next-sexp))
308 (end-of-buffer
309 (goto-char opoint)
310 (throw 'no-match t)))
311 (if (and (zerop (car (syntax-ppss)))
312 (not (el-search-expression-contains-match-p pattern current-expr)))
313 ;; nothing here; skip to next top level form
314 (let ((end-of-next-sexp (scan-sexps (point) 2)))
315 (if (not end-of-next-sexp)
316 (throw 'no-match t)
317 (goto-char end-of-next-sexp)
318 (backward-sexp)))
319 (if (el-search--match-p pattern current-expr)
320 (setq match-beg (point)
321 opoint (point))
322 (forward-char)))))
323 (if noerror nil (signal 'end-of-buffer nil)))
324 match-beg))
325
326 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
327 ;; bound -> nil means till end of buffer
328 (save-excursion
329 (goto-char pos)
330 (condition-case nil
331 (while (or (not bound) (< (point) bound))
332 (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
333 (this-sexp (buffer-substring-no-properties (point) this-sexp-end)))
334 (funcall do-fun this-sexp this-sexp-end))
335 (forward-char)
336 (el-search--goto-next-sexp))
337 (end-of-buffer))
338 (when ret-fun (funcall ret-fun))))
339
340 (defun el-search--create-read-map (&optional pos)
341 (let ((mapping '()))
342 (el-search--do-subsexps
343 (or pos (point))
344 (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
345 (lambda () (nreverse mapping))
346 (save-excursion (thing-at-point--end-of-sexp) (point)))))
347
348 (defun el-search--repair-replacement-layout (printed mapping)
349 (with-temp-buffer
350 (insert printed)
351 (el-search--do-subsexps
352 (point-min)
353 (lambda (sexp sexp-end)
354 (when-let ((old (cdr (assoc (read sexp) mapping))))
355 (delete-region (point) sexp-end)
356 (when (string-match-p "\n" old)
357 (unless (looking-back "^[[:space:]]*" (line-beginning-position))
358 (insert "\n"))
359 (unless (looking-at "[[:space:]\)]*$")
360 (insert "\n")
361 (backward-char)))
362 (insert old)))
363 (lambda () (buffer-substring (point-min) (point-max))))))
364
365
366 ;;;; Highlighting
367
368 (defvar-local el-search-hl-overlay nil)
369
370 (defvar el-search-keep-hl nil)
371
372 (defun el-search-hl-sexp ()
373 (let ((bounds (list (point) (scan-sexps (point) 1))))
374 (if (overlayp el-search-hl-overlay)
375 (apply #'move-overlay el-search-hl-overlay bounds)
376 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
377 'face 'el-search-match)))
378 (add-hook 'post-command-hook (el-search-hl-post-command-fun (current-buffer)) t))
379
380 (defun el-search-hl-remove ()
381 (when (overlayp el-search-hl-overlay)
382 (delete-overlay el-search-hl-overlay)))
383
384 (defun el-search-hl-post-command-fun (buf)
385 (lambda ()
386 (when (buffer-live-p buf)
387 (unless (or el-search-keep-hl
388 (eq this-command 'el-search-query-replace)
389 (eq this-command 'el-search-pattern))
390 (with-current-buffer buf
391 (el-search-hl-remove)
392 (remove-hook 'post-command-hook #'el-search-hl-post-command-fun t))))))
393
394
395 ;;;; Core functions
396
397 (defvar el-search-history '()
398 "List of input strings.")
399
400 (defvar el-search-success nil)
401 (defvar el-search-current-pattern nil)
402
403 ;;;###autoload
404 (defun el-search-pattern (pattern)
405 "Do incremental elisp search forward."
406 (interactive (list (if (and (eq this-command last-command)
407 el-search-success)
408 el-search-current-pattern
409 (let ((pattern
410 (el-search--read-pattern "Find pcase pattern: "
411 (car el-search-history)
412 nil t)))
413 ;; A very common mistake: input "foo" instead of "'foo"
414 (when (and (symbolp pattern)
415 (not (eq pattern '_))
416 (or (not (boundp pattern))
417 (not (eq (symbol-value pattern) pattern))))
418 (error "Please don't forget the quote when searching for a symbol"))
419 (el-search--maybe-wrap-pattern pattern)))))
420 (setq el-search-current-pattern pattern)
421 (setq el-search-success nil)
422 (let ((opoint (point)))
423 (when (eq this-command last-command)
424 (forward-char))
425 (when (condition-case nil
426 (el-search--search-pattern pattern)
427 (end-of-buffer (message "No match")
428 (goto-char opoint)
429 (el-search-hl-remove)
430 (ding)
431 nil))
432 (setq el-search-success t)
433 (el-search-hl-sexp)
434 (message "%s" (substitute-command-keys "Type \\[el-search-pattern] to repeat")))))
435
436 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping)
437 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
438 (el-search-keep-hl t) (opoint (point)))
439 (unwind-protect
440 (while (and (not done) (el-search--search-pattern pattern t))
441 (setq opoint (point))
442 (unless replace-all (el-search-hl-sexp))
443 (let* ((read-mapping (el-search--create-read-map))
444 (region (list (point) (scan-sexps (point) 1)))
445 (substring (apply #'buffer-substring-no-properties region))
446 (expr (read substring))
447 (replaced-this nil)
448 (new-expr (funcall `(lambda () (pcase ',expr (,pattern ,replacement)))))
449 (to-insert (el-search--repair-replacement-layout
450 (el-search--print new-expr) (append mapping read-mapping)))
451 (do-replace (lambda ()
452 (atomic-change-group
453 (apply #'delete-region region)
454 (let ((inhibit-message t)
455 (opoint (point)))
456 (insert to-insert)
457 (indent-region opoint (point))
458 (goto-char opoint)
459 (el-search-hl-sexp)))
460 (cl-incf nbr-replaced)
461 (setq replaced-this t))))
462 (if replace-all
463 (funcall do-replace)
464 (while (not (pcase (if replaced-this
465 (read-char-choice "[SPC ! q]" '(?\ ?! ?q ?n))
466 (read-char-choice
467 (concat "Replace this occurence"
468 (if (or (string-match-p "\n" to-insert)
469 (< 40 (length to-insert)))
470 "" (format " with `%s'" to-insert))
471 "? [y SPC r ! q]" )
472 '(?y ?n ?r ?\ ?! ?q)))
473 (?r (funcall do-replace)
474 nil)
475 (?y (funcall do-replace)
476 t)
477 ((or ?\ ?n)
478 (unless replaced-this (cl-incf nbr-skipped))
479 t)
480 (?! (unless replaced-this
481 (funcall do-replace))
482 (setq replace-all t)
483 t)
484 (?q (setq done t)
485 t)))))
486 (unless (or done (eobp)) (forward-char 1)))))
487 (el-search-hl-remove)
488 (goto-char opoint)
489 (message "Replaced %d matches%s"
490 nbr-replaced
491 (if (zerop nbr-skipped) ""
492 (format " (%d skipped)" nbr-skipped)))))
493
494 (defun el-search-query-replace-read-args (&optional initial-contents)
495 (barf-if-buffer-read-only)
496 (let* ((from (el-search--read-pattern "Replace from: " nil initial-contents))
497 (to (el-search--read-pattern "Replace with result of evaluation of: " from)))
498 (list (el-search--maybe-wrap-pattern (read from)) (read to)
499 (with-temp-buffer
500 (insert to)
501 (el-search--create-read-map 1)))))
502
503 ;;;###autoload
504 (defun el-search-query-replace (from to &optional mapping)
505 "Replace some occurrences of FROM pattern with evaluated TO."
506 (interactive (el-search-query-replace-read-args))
507 (setq el-search-current-pattern from)
508 (barf-if-buffer-read-only)
509 (el-search-search-and-replace-pattern from to mapping))
510
511 (defun el-search--take-over-from-isearch ()
512 (let ((other-end isearch-other-end)
513 (input isearch-string))
514 (isearch-exit)
515 (when (and other-end (< other-end (point)))
516 (goto-char other-end))
517 input))
518
519 ;;;###autoload
520 (defun el-search-search-from-isearch ()
521 (interactive)
522 (el-search-pattern
523 (el-search--read-pattern
524 "Find pcase pattern: " nil (concat "'" (el-search--take-over-from-isearch)) t))
525 (setq this-command 'el-search-pattern))
526
527 ;;;###autoload
528 (defun el-search-replace-from-isearch ()
529 (interactive)
530 (let ((this-command 'el-search-query-replace))
531 (apply #'el-search-query-replace
532 (el-search-query-replace-read-args (concat "'" (el-search--take-over-from-isearch))))))
533
534
535
536 (provide 'el-search)
537 ;;; el-search.el ends here