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