1 ;;; el-search.el --- Expression based incremental search for emacs-lisp-mode -*- lexical-binding: t -*-
3 ;; Copyright (C) 2015 Free Software Foundation, Inc
5 ;; Author: Michael Heerdegen <michael_heerdegen@web.de>
6 ;; Maintainer: Michael Heerdegen <michael_heerdegen@web.de>
7 ;; Created: 29 Jul 2015
9 ;; Compatibility: GNU Emacs 25
11 ;; Package-Requires: ((emacs "25"))
14 ;; This file is not part of GNU Emacs.
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.
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.
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/>.
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).
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.
45 ;; Example 1: if you enter
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
55 ;; Example 2: If you enter the pattern
59 ;; you search for all defvar forms that don't specify an init value.
61 ;; The following will search for defvar forms with a docstring whose
62 ;; first line is longer than 70 characters:
65 ;; ,(and s (guard (< 70 (length (car (split-string s "\n")))))))
71 ;; For expression input, the minibuffer prompts here uses
74 ;; When reading a search pattern in the minibuffer, the input is
75 ;; automatically wrapped into `(and expr ,(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
79 ;; (guard (and (symbolp expr)
80 ;; (when-let ((file (symbol-file expr)))
81 ;; (string-match-p "cl-lib\\.elc?$" file))))
83 ;; without binding the variable `expr'.
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.
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',
99 ;; (foo 'a (* 2 (+ 3 4)) t)
103 ;; (foo (* 2 (+ 3 4)) 'a t).
107 ;; M-x el-search-query-replace RET
108 ;; `(foo ,a ,b . ,rest) RET
109 ;; `(foo ,b ,a . ,rest) RET
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
118 ;; Suggested key bindings
119 ;; ======================
121 ;; (define-key emacs-lisp-mode-map [(control ?S)] #'el-search-pattern)
122 ;; (define-key emacs-lisp-mode-map [(control ?%)] #'el-search-query-replace)
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)
127 ;; The bindings in `isearch-mode-map' let you conveniently switch to
128 ;; elisp searching from isearch.
131 ;; Bugs, Known Limitations
132 ;; =======================
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.
138 ;; - Similarly: Comments are normally preserved (where it makes
139 ;; sense). But when replacing like `(foo ,a ,b) -> `(foo ,b ,a)
148 ;; the comment will be lost.
154 ;; Thanks to Stefan Monnier for corrections and advice.
159 ;; - implement backward searching and wrapped searching
161 ;; - improve docstrings
163 ;; - add more examples
165 ;; - handle more reader syntaxes, e.g. #n, #n#
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.
183 (require 'elisp-mode)
187 ;;;; Configuration stuff
189 (defgroup el-search nil
190 "Expression based search and replace for `emacs-lisp-mode'."
193 (defcustom el-search-this-expression-identifier 'exp
194 "Name of the identifier referring to the whole expression.
195 The default value is `expr'. You can use this variable in the
196 search prompt to refer to value of the currently searched
200 (defface el-search-match '((((background dark)) (:background "#0000A0"))
201 (t (:background "DarkSlateGray1")))
202 "Face for highlighting the current match.")
207 (defun el-search--print (expr)
208 (let ((print-quoted t)
211 (prin1-to-string expr)))
213 (defvar el-search-read-expression-map
214 (let ((map (make-sparse-keymap)))
215 (set-keymap-parent map read-expression-map)
216 (define-key map [(control ?g)] #'abort-recursive-edit)
217 (define-key map [up] nil)
218 (define-key map [down] nil)
219 (define-key map [(control meta backspace)] #'backward-kill-sexp)
220 (define-key map [(control ?S)] #'exit-minibuffer)
222 "Map for reading input with `el-search-read-expression'.")
224 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
225 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
226 "Read expression for `my-eval-expression'."
227 (minibuffer-with-setup-hook
230 (use-local-map el-search-read-expression-map)
231 (setq font-lock-mode t)
232 (funcall font-lock-function 1)
235 (goto-char (point-max)))
236 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
237 (or hist 'read-expression-history) default)))
239 (defun el-search--read-pattern (prompt &optional default initial-contents read)
240 (el-search-read-expression
241 prompt initial-contents 'el-search-history
242 (or default (when-let ((this-sexp (sexp-at-point)))
243 (concat "'" (el-search--print this-sexp))))
246 (defun el-search--goto-next-sexp ()
247 "Move point to the beginning of the next sexp.
248 Don't move if already at beginning of a sexp."
249 (let ((not-done t) res)
251 (let ((stop-here nil) syntax-here
252 (looking-at-from-back (lambda (regexp n)
255 (looking-at regexp)))))
256 (while (not stop-here)
258 ((eobp) (signal 'end-of-buffer nil))
259 ((looking-at (rx (and (* space) ";"))) (forward-line))
260 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
261 ((progn (setq syntax-here (syntax-ppss))
262 (or (nth 4 syntax-here) (nth 8 syntax-here)))
263 (if (nth 4 syntax-here) (forward-line) (search-forward "\"")))
265 ;; FIXME: can the rest be done more generically?
266 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
267 (not (looking-at "\\_<"))
268 (not (funcall looking-at-from-back ",@" 2)))
270 ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
271 (and (looking-at "@") (funcall looking-at-from-back "," 1)))
273 (t (setq stop-here t)))))
276 (setq res (save-excursion (read (current-buffer))))
278 (error (forward-char))))
281 (defun el-search--match-p (pattern expression)
288 (defun el-search-expression-contains-match-p (pattern expression)
289 "Whether some subexp of EXPRESSION is matched by PATTERN."
290 (or (el-search--match-p pattern expression)
291 (and (consp expression)
292 (if (cdr (last expression))
294 (or (el-search-expression-contains-match-p pattern (car expression))
295 (el-search-expression-contains-match-p pattern (cdr expression)))
296 (cl-some (lambda (subexpr) (el-search-expression-contains-match-p pattern subexpr))
299 (defun el-search--maybe-wrap-pattern (pattern)
300 (if (el-search-expression-contains-match-p `',el-search-this-expression-identifier pattern)
301 `(and ,el-search-this-expression-identifier ,pattern)
304 (defun el-search--search-pattern (pattern &optional noerror)
305 "Search elisp buffer with `pcase' PATTERN.
306 Set point to the beginning of the occurrence found and return
307 point. Optional second argument, if non-nil, means if fail just
308 return nil (no error)."
309 ;; For better performance we read complete top-level sexps and test
310 ;; for matches. We enter top-level expressions in the buffer text
311 ;; only when the test was successful.
312 (let ((match-beg nil) (opoint (point)) current-expr)
314 (while (not match-beg)
316 (setq current-expr (el-search--goto-next-sexp))
319 (throw 'no-match t)))
320 (if (and (zerop (car (syntax-ppss)))
321 (not (el-search-expression-contains-match-p pattern current-expr)))
322 ;; nothing here; skip to next top level form
323 (let ((end-of-next-sexp (scan-sexps (point) 2)))
324 (if (not end-of-next-sexp)
326 (goto-char end-of-next-sexp)
328 (if (el-search--match-p pattern current-expr)
329 (setq match-beg (point)
332 (if noerror nil (signal 'end-of-buffer nil)))
335 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
336 ;; bound -> nil means till end of buffer
340 (while (or (not bound) (< (point) bound))
341 (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
342 (this-sexp (buffer-substring-no-properties (point) this-sexp-end)))
343 (funcall do-fun this-sexp this-sexp-end))
345 (el-search--goto-next-sexp))
347 (when ret-fun (funcall ret-fun))))
349 (defun el-search--create-read-map (&optional pos)
351 (el-search--do-subsexps
353 (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
354 (lambda () (nreverse mapping))
355 (save-excursion (thing-at-point--end-of-sexp) (point)))))
357 (defun el-search--repair-replacement-layout (printed mapping)
360 (el-search--do-subsexps
362 (lambda (sexp sexp-end)
363 (when-let ((old (cdr (assoc (read sexp) mapping))))
364 (delete-region (point) sexp-end)
365 (when (string-match-p "\n" old)
366 (unless (looking-back "^[[:space:]]*" (line-beginning-position))
368 (unless (looking-at "[[:space:]\)]*$")
372 (lambda () (buffer-substring (point-min) (point-max))))))
377 (defvar-local el-search-hl-overlay nil)
379 (defvar el-search-keep-hl nil)
381 (defun el-search-hl-sexp-at-point ()
382 (let ((bounds (list (point) (scan-sexps (point) 1))))
383 (if (overlayp el-search-hl-overlay)
384 (apply #'move-overlay el-search-hl-overlay bounds)
385 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
386 'face 'el-search-match)))
387 (add-hook 'post-command-hook (el-search-hl-post-command-fun (current-buffer)) t))
389 (defun el-search-hl-remove ()
390 (when (overlayp el-search-hl-overlay)
391 (delete-overlay el-search-hl-overlay)))
393 (defun el-search-hl-post-command-fun (buf)
395 (when (buffer-live-p buf)
396 (unless (or el-search-keep-hl
397 (eq this-command 'el-search-query-replace)
398 (eq this-command 'el-search-pattern))
399 (with-current-buffer buf
400 (el-search-hl-remove)
401 (remove-hook 'post-command-hook #'el-search-hl-post-command-fun t))))))
406 (defvar el-search-history '()
407 "List of input strings.")
409 (defvar el-search-success nil)
410 (defvar el-search-current-pattern nil)
413 (defun el-search-pattern (pattern)
414 "Do incremental elisp search forward."
415 (interactive (list (if (and (eq this-command last-command)
417 el-search-current-pattern
419 (el-search--read-pattern "Find pcase pattern: "
420 (car el-search-history)
422 ;; A very common mistake: input "foo" instead of "'foo"
423 (when (and (symbolp pattern)
424 (not (eq pattern '_))
425 (or (not (boundp pattern))
426 (not (eq (symbol-value pattern) pattern))))
427 (error "Please don't forget the quote when searching for a symbol"))
428 (el-search--maybe-wrap-pattern pattern)))))
429 (setq el-search-current-pattern pattern)
430 (setq el-search-success nil)
431 (let ((opoint (point)))
432 (when (eq this-command last-command)
434 (when (condition-case nil
435 (el-search--search-pattern pattern)
436 (end-of-buffer (message "No match")
438 (el-search-hl-remove)
441 (setq el-search-success t)
442 (el-search-hl-sexp-at-point)
443 (message "%s" (substitute-command-keys "Type \\[el-search-pattern] to repeat")))))
445 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping)
446 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
447 (el-search-keep-hl t) (opoint (point)))
449 (while (and (not done) (el-search--search-pattern pattern t))
450 (setq opoint (point))
451 (unless replace-all (el-search-hl-sexp-at-point))
452 (let* ((read-mapping (el-search--create-read-map))
453 (region (list (point) (scan-sexps (point) 1)))
454 (substring (apply #'buffer-substring-no-properties region))
455 (expr (read substring))
457 (new-expr (funcall `(lambda () (pcase ',expr (,pattern ,replacement)))))
458 (to-insert (el-search--repair-replacement-layout
459 (el-search--print new-expr) (append mapping read-mapping)))
460 (do-replace (lambda ()
462 (apply #'delete-region region)
463 (let ((inhibit-message t)
466 (indent-region opoint (point))
468 (el-search-hl-sexp-at-point)))
469 (cl-incf nbr-replaced)
470 (setq replaced-this t))))
473 (while (not (pcase (if replaced-this
474 (read-char-choice "[SPC ! q]" '(?\ ?! ?q ?n))
476 (concat "Replace this occurence"
477 (if (or (string-match-p "\n" to-insert)
478 (< 40 (length to-insert)))
479 "" (format " with `%s'" to-insert))
481 '(?y ?n ?r ?\ ?! ?q)))
482 (?r (funcall do-replace)
484 (?y (funcall do-replace)
487 (unless replaced-this (cl-incf nbr-skipped))
489 (?! (unless replaced-this
490 (funcall do-replace))
495 (unless (or done (eobp)) (forward-char 1)))))
496 (el-search-hl-remove)
498 (message "Replaced %d matches%s"
500 (if (zerop nbr-skipped) ""
501 (format " (%d skipped)" nbr-skipped)))))
503 (defun el-search-query-replace-read-args (&optional initial-contents)
504 (barf-if-buffer-read-only)
505 (let* ((from (el-search--read-pattern "Replace from: " nil initial-contents))
506 (to (el-search--read-pattern "Replace with result of evaluation of: " from)))
507 (list (el-search--maybe-wrap-pattern (read from)) (read to)
510 (el-search--create-read-map 1)))))
513 (defun el-search-query-replace (from to &optional mapping)
514 "Replace some occurrences of FROM pattern with evaluated TO."
515 (interactive (el-search-query-replace-read-args))
516 (setq el-search-current-pattern from)
517 (barf-if-buffer-read-only)
518 (el-search-search-and-replace-pattern from to mapping))
520 (defun el-search--take-over-from-isearch ()
521 (let ((other-end isearch-other-end)
522 (input isearch-string))
524 (when (and other-end (< other-end (point)))
525 (goto-char other-end))
529 (defun el-search-search-from-isearch ()
530 ;; FIXME: an interesting alternative would be to really integrate it
531 ;; with Isearch, using `isearch-search-fun-function'.
532 ;; Alas, this is not trivial if we want to transfer our optimizations.
535 (el-search--read-pattern
536 "Find pcase pattern: " nil (concat "'" (el-search--take-over-from-isearch)) t))
537 (setq this-command 'el-search-pattern))
540 (defun el-search-replace-from-isearch ()
542 (let ((this-command 'el-search-query-replace))
543 (apply #'el-search-query-replace
544 (el-search-query-replace-read-args (concat "'" (el-search--take-over-from-isearch))))))
549 ;;; el-search.el ends here