]> code.delx.au - gnu-emacs-elpa/blob - packages/el-search/el-search.el
Merge branch 'master' of git://bzr.sv.gnu.org/emacs/elpa
[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.1.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 ;; Introduction
33 ;; ============
34 ;;
35 ;;
36 ;; The main user entry point is `el-search-pattern'. This command
37 ;; prompts for a `pcase' pattern and searches the current buffer for
38 ;; matching expressions by iteratively `read'ing buffer contents. For
39 ;; any match, point is put at the beginning of the expression found
40 ;; (unlike isearch which puts point at the end of matches).
41 ;;
42 ;; Why is it based on `pcase'? Because pattern matching (and the
43 ;; ability to combine destructuring and condition testing) is well
44 ;; suited for this task. In addition, pcase allows to add specialized
45 ;; pattern types and to combine them with other patterns in a natural
46 ;; and transparent way out of the box.
47 ;;
48 ;; It doesn't matter how the code is actually formatted. Comments are
49 ;; ignored, and strings are treated as atomic objects, their contents
50 ;; are not being searched.
51 ;;
52 ;;
53 ;; Example 1: if you enter
54 ;;
55 ;; 97
56 ;;
57 ;; at the prompt, this will find any occurrence of the number 97 in
58 ;; the code, but not 977 or (+ 90 7) or "My string containing 97".
59 ;; But it will find anything `eq' to 97 after reading, e.g. #x61 or
60 ;; ?a.
61 ;;
62 ;;
63 ;; Example 2: If you enter the pattern
64 ;;
65 ;; `(defvar ,_)
66 ;;
67 ;; you search for all defvar forms that don't specify an init value.
68 ;;
69 ;; The following will search for defvar forms with a docstring whose
70 ;; first line is longer than 70 characters:
71 ;;
72 ;; `(defvar ,_ ,_
73 ;; ,(and s (guard (< 70 (length (car (split-string s "\n")))))))
74 ;;
75 ;;
76 ;; When a search pattern is processed, the searched buffer is current
77 ;; with point at the beginning of the currently tested expression.
78 ;;
79 ;;
80 ;; Example 3:
81 ;;
82 ;; I can be useful to use (guard EXP) patterns for side effects (note:
83 ;; this only works when applied to the top level expression).
84 ;;
85 ;; The following pattern will search for symbols defined in any
86 ;; library whose name starts with "cl". As a side effect, it prints
87 ;; the current line number, whether we have a macro or a function, and
88 ;; the defining file in the echo area for each match:
89 ;;
90 ;; (and (pred symbolp)
91 ;; (let file (symbol-file exp))
92 ;; (guard file)
93 ;; (let lib-name (file-name-sans-extension
94 ;; (file-name-nondirectory file)))
95 ;; (guard (string-match-p "^cl" lib-name))
96 ;; (or (and (pred macrop) (let type "macro "))
97 ;; (and (pred functionp) (let type "function "))
98 ;; (let type ""))
99 ;; (guard (message "Line %d: %s`%S' (from \"%s\")"
100 ;; (line-number-at-pos)
101 ;; type
102 ;; exp
103 ;; lib-name)))
104 ;;
105 ;; `message' never returns nil, so the last `guard' always "matches".
106 ;;
107 ;;
108 ;; Convenience
109 ;; ===========
110 ;;
111 ;; For pattern input, the minibuffer is put into `emacs-lisp-mode'.
112 ;;
113 ;; Any input PATTERN is silently transformed into (and exp PATTERN)
114 ;; so that you can always refer to the whole currently tested
115 ;; expression via the variable `exp'.
116 ;;
117 ;; Example 4:
118 ;;
119 ;; If you want to search a buffer for symbols that are defined in
120 ;; "cl-lib", you can use this pattern
121 ;;
122 ;; (guard (and (symbolp exp)
123 ;; (when-let ((file (symbol-file exp)))
124 ;; (string-match-p "cl-lib\\.elc?$" file))))
125 ;;
126 ;;
127 ;; ,----------------------------------------------------------------------
128 ;; | Q: "But I hate `pcase'! Can't we just do without?" |
129 ;; | |
130 ;; | A: Respect that you kept up until here! Just use (guard CODE), where|
131 ;; | CODE is any normal Elisp expression that returns non-nil when and |
132 ;; | only when you have a match. Use the variable `exp' to refer to |
133 ;; | the currently tested expression. Just like in the last example! |
134 ;; `----------------------------------------------------------------------
135 ;;
136 ;;
137 ;; It's cumbersome to write out the same complicated pattern
138 ;; constructs in the minibuffer again and again. You can define your
139 ;; own pcase pattern types for the purpose of el-search with
140 ;; `el-search-defpattern'. It is just like `pcase-defmacro', but the
141 ;; effect is limited to this package. See C-h f `el-search-pattern'
142 ;; for a list of predefined additional pattern forms.
143 ;;
144 ;;
145 ;; Replacing
146 ;; =========
147 ;;
148 ;; You can replace expressions with command `el-search-query-replace'.
149 ;; You are queried for a (pcase) pattern and a replacement expression.
150 ;; For each match of the pattern, the replacement expression is
151 ;; evaluated with the bindings created by the pcase matching in
152 ;; effect, and printed to produce the replacement string.
153 ;;
154 ;; Example: In some buffer you want to swap the two expressions at the
155 ;; places of the first two arguments in all calls of function `foo',
156 ;; so that e.g.
157 ;;
158 ;; (foo 'a (* 2 (+ 3 4)) t)
159 ;;
160 ;; becomes
161 ;;
162 ;; (foo (* 2 (+ 3 4)) 'a t).
163 ;;
164 ;; This will do it:
165 ;;
166 ;; M-x el-search-query-replace RET
167 ;; `(foo ,a ,b . ,rest) RET
168 ;; `(foo ,b ,a . ,rest) RET
169 ;;
170 ;; Type y to replace a match and go to the next one, r to replace
171 ;; without moving, SPC to go to the next match and ! to replace all
172 ;; remaining matches automatically. q quits. n is like SPC, so that
173 ;; y and n work like in isearch (meaning "yes" and "no") if you are
174 ;; used to that.
175 ;;
176 ;; It is possible to replace a match with multiple expressions using
177 ;; "splicing mode". When it is active, the replacement expression
178 ;; must evaluate to a list, and is spliced instead of inserted into
179 ;; the buffer for any replaced match. Use s to toggle splicing mode
180 ;; in a `el-search-query-replace' session.
181 ;;
182 ;;
183 ;; Suggested key bindings
184 ;; ======================
185 ;;
186 ;; (define-key emacs-lisp-mode-map [(control ?S)] #'el-search-pattern)
187 ;; (define-key emacs-lisp-mode-map [(control ?%)] #'el-search-query-replace)
188 ;;
189 ;; (define-key isearch-mode-map [(control ?S)] #'el-search-search-from-isearch)
190 ;; (define-key isearch-mode-map [(control ?%)] #'el-search-replace-from-isearch)
191 ;;
192 ;; The bindings in `isearch-mode-map' let you conveniently switch to
193 ;; elisp searching from isearch.
194 ;;
195 ;;
196 ;; Bugs, Known Limitations
197 ;; =======================
198 ;;
199 ;; - Replacing: in some cases the reader syntax of forms
200 ;; is changing due to reading+printing. "Some" because we can treat
201 ;; that problem in most cases.
202 ;;
203 ;; - Similarly: Comments are normally preserved (where it makes
204 ;; sense). But when replacing like `(foo ,a ,b) -> `(foo ,b ,a)
205 ;;
206 ;; in a content like
207 ;;
208 ;; (foo
209 ;; a
210 ;; ;;a comment
211 ;; b)
212 ;;
213 ;; the comment will be lost.
214 ;;
215 ;;
216 ;; Acknowledgments
217 ;; ===============
218 ;;
219 ;; Thanks to Stefan Monnier for corrections and advice.
220 ;;
221 ;;
222 ;; TODO:
223 ;;
224 ;; - When replacing like (progn A B C) -> A B C, the layout of the
225 ;; whole "group" A B C as a unit is lost. Instead of restoring layout
226 ;; as we do now (via "read mappings"), we could just make a backup of
227 ;; the original expression as a string, and use our search machinery
228 ;; to find occurrences in the replacement recursively.
229 ;;
230 ;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
231 ;;
232 ;; - highlight matches around point in a timer
233 ;;
234 ;; - implement backward searching
235 ;;
236 ;; - improve docstrings
237 ;;
238 ;; - handle more reader syntaxes, e.g. #n, #n#
239 ;;
240 ;; - Implement sessions; add multi-file support based on iterators. A
241 ;; file list is read in (or the user can specify an iterator as a
242 ;; variable). The state in the current buffer is just (buffer
243 ;; . marker). Or should this be abstracted into an own lib? Could
244 ;; be named "files-session" or so.
245
246
247
248 ;;; Code:
249
250 ;;;; Requirements
251
252 (eval-when-compile
253 (require 'subr-x))
254
255 (require 'cl-lib)
256 (require 'elisp-mode)
257 (require 'thingatpt)
258 (require 'help-fns) ;el-search--make-docstring
259
260
261 ;;;; Configuration stuff
262
263 (defgroup el-search nil
264 "Expression based search and replace for `emacs-lisp-mode'."
265 :group 'lisp)
266
267 (defcustom el-search-this-expression-identifier 'exp
268 "Name of the identifier referring to the current expression.
269 The default value is `exp'. You can use this name in the search
270 prompt to refer to the value of the currently tested expression."
271 :type 'symbol)
272
273 (defface el-search-match '((((background dark)) (:background "#0000A0"))
274 (t (:background "DarkSlateGray1")))
275 "Face for highlighting the current match.")
276
277
278 ;;;; Helpers
279
280 (defun el-search--print (expr)
281 (let ((print-quoted t)
282 (print-length nil)
283 (print-level nil))
284 (prin1-to-string expr)))
285
286 (defvar el-search-read-expression-map
287 (let ((map (make-sparse-keymap)))
288 (set-keymap-parent map read-expression-map)
289 (define-key map [(control ?g)] #'abort-recursive-edit)
290 (define-key map [up] nil)
291 (define-key map [down] nil)
292 (define-key map [(control meta backspace)] #'backward-kill-sexp)
293 (define-key map [(control ?S)] #'exit-minibuffer)
294 map)
295 "Map for reading input with `el-search-read-expression'.")
296
297 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
298 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
299 "Read expression for `my-eval-expression'."
300 (minibuffer-with-setup-hook
301 (lambda ()
302 (emacs-lisp-mode)
303 (use-local-map el-search-read-expression-map)
304 (setq font-lock-mode t)
305 (funcall font-lock-function 1)
306 (backward-sexp)
307 (indent-sexp)
308 (goto-char (point-max)))
309 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
310 (or hist 'read-expression-history) default)))
311
312 (defvar el-search--initial-mb-contents nil)
313
314 (defun el-search--read-pattern (prompt &optional default read)
315 (let ((this-sexp (sexp-at-point)))
316 (minibuffer-with-setup-hook
317 (lambda ()
318 (when this-sexp
319 (let ((more-defaults (list (concat "'" (el-search--print this-sexp)))))
320 (setq-local minibuffer-default-add-function
321 (lambda () (if (listp minibuffer-default)
322 (append minibuffer-default more-defaults)
323 (cons minibuffer-default more-defaults)))))))
324 (el-search-read-expression
325 prompt el-search--initial-mb-contents 'el-search-history default read))))
326
327 (defun el-search--end-of-sexp ()
328 ;;Point must be at sexp beginning
329 (or (scan-sexps (point) 1) (point-max)))
330
331 (defun el-search--ensure-sexp-start ()
332 "Move point to the beginning of the next sexp if necessary.
333 Don't move if already at beginning of a sexp.
334 Point must not be inside a string or comment."
335 (let ((not-done t) res)
336 (while not-done
337 (let ((stop-here nil)
338 (looking-at-from-back (lambda (regexp n)
339 (save-excursion
340 (backward-char n)
341 (looking-at regexp)))))
342 (while (not stop-here)
343 (cond
344 ((eobp) (signal 'end-of-buffer nil))
345 ((looking-at (rx (and (* space) ";"))) (forward-line))
346 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
347
348 ;; FIXME: can the rest be done more generically?
349 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
350 (not (looking-at "\\_<"))
351 (not (funcall looking-at-from-back ",@" 2)))
352 (forward-symbol 1))
353 ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
354 (and (looking-at "@") (funcall looking-at-from-back "," 1)))
355 (forward-char))
356 (t (setq stop-here t)))))
357 (condition-case nil
358 (progn
359 (setq res (save-excursion (read (current-buffer))))
360 (setq not-done nil))
361 (error (forward-char))))
362 res))
363
364 (defvar el-search--pcase-macros '()
365 "List of additional \"el-search\" pcase macros.")
366
367 (defun el-search--make-docstring ()
368 ;; code mainly from `pcase--make-docstring'
369 (let* ((main (documentation (symbol-function 'el-search-pattern) 'raw))
370 (ud (help-split-fundoc main 'pcase)))
371 (with-temp-buffer
372 (insert (or (cdr ud) main))
373 (mapc
374 (pcase-lambda (`(,symbol . ,fun))
375 (when-let ((doc (documentation fun)))
376 (insert "\n\n-- ")
377 (setq doc (help-fns--signature symbol doc fun fun nil))
378 (insert "\n" (or doc "Not documented."))))
379 (reverse el-search--pcase-macros))
380 (let ((combined-doc (buffer-string)))
381 (if ud (help-add-fundoc-usage combined-doc (car ud)) combined-doc)))))
382
383 (put 'el-search-pattern 'function-documentation '(el-search--make-docstring))
384
385 (defmacro el-search-defpattern (name args &rest body)
386 "Like `pcase-defmacro', but limited to el-search patterns.
387 The semantics is exactly that of `pcase-defmacro', but the scope
388 of the definitions is limited to \"el-search\"."
389 (declare (indent 2) (debug defun))
390 `(setf (alist-get ',name el-search--pcase-macros)
391 (lambda ,args ,@body)))
392
393
394 (defmacro el-search--with-additional-pcase-macros (&rest body)
395 `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
396 `((get ',symbol 'pcase-macroexpander) #',fun))
397 el-search--pcase-macros)
398 ,@body))
399
400 (defun el-search--matcher (pattern &rest body)
401 (eval ;use `eval' to allow for user defined pattern types at run time
402 `(el-search--with-additional-pcase-macros
403 (let ((byte-compile-debug t) ;make undefined pattern types raise an error
404 (warning-suppress-log-types '((bytecomp)))
405 (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
406 (byte-compile (lambda (expression)
407 (pcase expression
408 (,pattern ,@(or body (list t)))
409 (_ nil))))))))
410
411 (defun el-search--match-p (matcher expression)
412 (funcall matcher expression))
413
414 (defun el-search--wrap-pattern (pattern)
415 `(and ,el-search-this-expression-identifier ,pattern))
416
417 (defun el-search--skip-expression (expression &optional read)
418 ;; Move forward at least one character. Don't move into a string or
419 ;; comment. Don't move further than the beginning of the next sexp.
420 ;; Try to move as far as possible. Point must be at the beginning
421 ;; of an expression.
422 ;; If there are positions where `read' would succeed, but that do
423 ;; not represent a valid sexp start, move past them (e.g. when
424 ;; before "#'" move past both characters).
425 ;;
426 ;; EXPRESSION must be the (read) expression at point, but when READ
427 ;; is non-nil, ignore the first argument and read the expression at
428 ;; point instead.
429 (when read (setq expression (save-excursion (read (current-buffer)))))
430 (cond
431 ((or (null expression)
432 (equal [] expression)
433 (not (or (listp expression) (vectorp expression))))
434 (goto-char (el-search--end-of-sexp)))
435 ((looking-at (rx (or ",@" "," "#'" "'")))
436 (goto-char (match-end 0)))
437 (t (forward-char))))
438
439 (defun el-search--search-pattern (pattern &optional noerror)
440 "Search elisp buffer with `pcase' PATTERN.
441 Set point to the beginning of the occurrence found and return
442 point. Optional second argument, if non-nil, means if fail just
443 return nil (no error)."
444
445 (let ((matcher (el-search--matcher pattern)) (match-beg nil) (opoint (point)) current-expr)
446
447 ;; when inside a string or comment, move past it
448 (let ((syntax-here (syntax-ppss)))
449 (when (nth 3 syntax-here) ;inside a string
450 (goto-char (nth 8 syntax-here))
451 (forward-sexp))
452 (when (nth 4 syntax-here) ;inside a comment
453 (forward-line 1)
454 (while (and (not (eobp)) (looking-at (rx (and (* space) ";"))))
455 (forward-line 1))))
456
457 (if (catch 'no-match
458 (while (not match-beg)
459 (condition-case nil
460 (setq current-expr (el-search--ensure-sexp-start))
461 (end-of-buffer
462 (goto-char opoint)
463 (throw 'no-match t)))
464 (if (el-search--match-p matcher current-expr)
465 (setq match-beg (point)
466 opoint (point))
467 (el-search--skip-expression current-expr))))
468 (if noerror nil (signal 'end-of-buffer nil)))
469 match-beg))
470
471 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
472 ;; In current buffer, for any expression start between POS and BOUND
473 ;; or (point-max), in order, call two argument function DO-FUN with
474 ;; the current sexp string and the ending position of the current
475 ;; sexp. When done, with RET-FUN given, call it with no args and
476 ;; return the result; else, return nil.
477 (save-excursion
478 (goto-char pos)
479 (condition-case nil
480 (while (< (point) (or bound (point-max)))
481 (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
482 (this-sexp-string (buffer-substring-no-properties (point) this-sexp-end)))
483 (funcall do-fun this-sexp-string this-sexp-end)
484 (el-search--skip-expression (read this-sexp-string))
485 (el-search--ensure-sexp-start)))
486 (end-of-buffer))
487 (when ret-fun (funcall ret-fun))))
488
489 (defun el-search--create-read-map (&optional pos)
490 (let ((mapping '()))
491 (el-search--do-subsexps
492 (or pos (point))
493 (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
494 (lambda () (nreverse mapping))
495 (save-excursion (thing-at-point--end-of-sexp) (point)))))
496
497 (defun el-search--repair-replacement-layout (printed mapping)
498 (with-temp-buffer
499 (insert printed)
500 (el-search--do-subsexps
501 (point-min)
502 (lambda (sexp sexp-end)
503 (when-let ((old (cdr (assoc (read sexp) mapping))))
504 (delete-region (point) sexp-end)
505 (when (string-match-p "\n" old)
506 (unless (looking-back "^[[:space:]]*" (line-beginning-position))
507 (insert "\n"))
508 (unless (looking-at "[[:space:]\)]*$")
509 (insert "\n")
510 (backward-char)))
511 (save-excursion (insert old))))
512 (lambda () (buffer-substring (point-min) (point-max))))))
513
514 (defun el-search--check-pattern-args (type args predicate &optional message)
515 "Check whether all ARGS fulfill PREDICATE.
516 Raise an error if not. TYPE and optional argument MESSAGE are
517 used to construct the error message."
518 (mapc (lambda (arg)
519 (unless (funcall predicate arg)
520 (error (concat "Pattern `%S': "
521 (or message (format "argument doesn't fulfill %S" predicate))
522 ": %S")
523 type arg)))
524 args))
525
526
527 ;;;; Additional pattern type definitions
528
529 (el-search-defpattern string (&rest regexps)
530 "Matches any string that is matched by all REGEXPS."
531 (el-search--check-pattern-args 'string regexps #'stringp)
532 (let ((string (make-symbol "string"))
533 (regexp (make-symbol "regexp")))
534 `(and (pred stringp)
535 (pred (lambda (,string)
536 (cl-every
537 (lambda (,regexp) (string-match-p ,regexp ,string))
538 (list ,@regexps)))))))
539
540 (el-search-defpattern symbol (&rest regexps)
541 "Matches any symbol whose name is matched by all REGEXPS."
542 (el-search--check-pattern-args 'symbol regexps #'stringp)
543 `(and (pred symbolp)
544 (app symbol-name (string ,@regexps))))
545
546 (defun el-search--match-symbol-file (regexp symbol)
547 (when-let ((symbol-file (and (symbolp symbol)
548 (symbol-file symbol))))
549 (string-match-p
550 (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
551 (file-name-sans-extension (file-name-nondirectory symbol-file)))))
552
553 (el-search-defpattern source (regexp)
554 "Matches any symbol whose `symbol-file' is matched by REGEXP.
555
556 This pattern matches when the object is a symbol for that
557 `symbol-file' returns a (non-nil) FILE-NAME that fulfills
558 (string-match-p REGEXP (file-name-sans-extension
559 (file-name-nondirectory FILENAME)))
560
561 REGEXP can also be a symbol, in which case
562
563 (concat \"^\" (symbol-name regexp) \"$\")
564
565 is used as regular expression."
566 (el-search--check-pattern-args 'source (list regexp) #'stringp)
567 `(pred (el-search--match-symbol-file ,regexp)))
568
569 (defun el-search--match-key-sequence (keys expr)
570 (when-let ((expr-keys (pcase expr
571 ((or (pred stringp) (pred vectorp)) expr)
572 (`(kbd ,(and (pred stringp) string)) (ignore-errors (kbd string))))))
573 (apply #'equal
574 (mapcar (lambda (keys) (ignore-errors (key-description keys)))
575 (list keys expr-keys)))))
576
577 (el-search-defpattern keys (key-sequence)
578 "Matches descriptions of the KEY-SEQUENCE.
579 KEY-SEQUENCE is a string or vector representing a key sequence,
580 or an expression of the form (kbd STRING).
581
582 Match any description of the same key sequence in any of these
583 formats.
584
585 Example: the pattern
586
587 (keys (kbd \"C-s\"))
588
589 matches any of these expressions:
590
591 \"\\C-s\"
592 \"\C-s\"
593 (kbd \"C-s\")
594 [(control ?s)]"
595 (when (eq (car-safe key-sequence) 'kbd)
596 (setq key-sequence (kbd (cadr key-sequence))))
597 (el-search--check-pattern-args 'keys (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
598 "argument not a string or vector")
599 `(pred (el-search--match-key-sequence ,key-sequence)))
600
601
602 ;;;; Highlighting
603
604 (defvar-local el-search-hl-overlay nil)
605
606 (defvar el-search-keep-hl nil)
607
608 (defun el-search-hl-sexp (&optional bounds)
609 (let ((bounds (or bounds
610 (list (point) (el-search--end-of-sexp)))))
611 (if (overlayp el-search-hl-overlay)
612 (apply #'move-overlay el-search-hl-overlay bounds)
613 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
614 'face 'el-search-match)))
615 (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
616
617 (defun el-search-hl-remove ()
618 (when (overlayp el-search-hl-overlay)
619 (delete-overlay el-search-hl-overlay)))
620
621 (defun el-search-hl-post-command-fun ()
622 (unless (or el-search-keep-hl
623 (eq this-command 'el-search-query-replace)
624 (eq this-command 'el-search-pattern))
625 (el-search-hl-remove)
626 (remove-hook 'post-command-hook 'el-search-hl-post-command-fun t)))
627
628
629 ;;;; Core functions
630
631 (defvar el-search-history '()
632 "List of input strings.")
633
634 (defvar el-search-success nil)
635 (defvar el-search-current-pattern nil)
636
637 ;;;###autoload
638 (defun el-search-pattern (pattern)
639 "Start new or resume last elisp search.
640
641 Search current buffer for expressions that are matched by `pcase'
642 PATTERN. Use `read' to transform buffer contents into
643 expressions.
644
645
646 Additional `pcase' pattern types to be used with this command can
647 be defined with `el-search-defpattern'.
648
649 The following additional pattern types are currently defined:\n"
650 (interactive (list (if (and (eq this-command last-command)
651 el-search-success)
652 el-search-current-pattern
653 (let ((pattern
654 (el-search--read-pattern "Find pcase pattern: "
655 (car el-search-history)
656 t)))
657 ;; A very common mistake: input "foo" instead of "'foo"
658 (when (and (symbolp pattern)
659 (not (eq pattern '_))
660 (or (not (boundp pattern))
661 (not (eq (symbol-value pattern) pattern))))
662 (error "Please don't forget the quote when searching for a symbol"))
663 (el-search--wrap-pattern pattern)))))
664 (setq this-command 'el-search-pattern) ;in case we come from isearch
665 (setq el-search-current-pattern pattern)
666 (let ((opoint (point)))
667 (when (and (eq this-command last-command) el-search-success)
668 (el-search--skip-expression nil t))
669 (setq el-search-success nil)
670 (message "%s" (substitute-command-keys "Type \\[el-search-pattern] to repeat"))
671 (when (condition-case nil
672 (el-search--search-pattern pattern)
673 (end-of-buffer (message "No match")
674 (goto-char opoint)
675 (el-search-hl-remove)
676 (ding)
677 nil))
678 (setq el-search-success t)
679 (el-search-hl-sexp))))
680
681 (defvar el-search-search-and-replace-help-string
682 "\
683 y Replace this match and move to the next.
684 SPC or n Skip this match and move to the next.
685 r Replace this match but don't move.
686 ! Replace all remaining matches automatically.
687 q Quit. To resume, use e.g. `repeat-complex-command'.
688 ? Show this help.
689 s Toggle splicing mode. When splicing mode is
690 on (default off), the replacement expression must
691 evaluate to a list, and the result is spliced into the
692 buffer, instead of just inserted.
693
694 Hit any key to proceed."
695 "Help string for ? in `el-search-query-replace'.")
696
697 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
698 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
699 (el-search-keep-hl t) (opoint (point))
700 (get-replacement (el-search--matcher pattern replacement)))
701 (unwind-protect
702 (while (and (not done) (el-search--search-pattern pattern t))
703 (setq opoint (point))
704 (unless replace-all (el-search-hl-sexp))
705 (let* ((read-mapping (el-search--create-read-map))
706 (region (list (point) (el-search--end-of-sexp)))
707 (substring (apply #'buffer-substring-no-properties region))
708 (expr (read substring))
709 (replaced-this nil)
710 (new-expr (funcall get-replacement expr))
711 (get-replacement-string
712 (lambda () (if (and splice (not (listp new-expr)))
713 (error "Expression to splice in is an atom")
714 (el-search--repair-replacement-layout
715 (if splice
716 (mapconcat #'el-search--print new-expr " ")
717 (el-search--print new-expr))
718 (append mapping read-mapping)))))
719 (to-insert (funcall get-replacement-string))
720 (do-replace (lambda ()
721 (atomic-change-group
722 (apply #'delete-region region)
723 (let ((inhibit-message t)
724 (opoint (point)))
725 (insert to-insert)
726 (indent-region opoint (point))
727 (el-search-hl-sexp (list opoint (point)))
728 (goto-char opoint)))
729 (cl-incf nbr-replaced)
730 (setq replaced-this t))))
731 (if replace-all
732 (funcall do-replace)
733 (while (not (pcase (if replaced-this
734 (read-char-choice "[SPC ! q] (? for help)"
735 '(?\ ?! ?q ?n ??))
736 (read-char-choice
737 (concat "Replace this occurrence"
738 (if (or (string-match-p "\n" to-insert)
739 (< 40 (length to-insert)))
740 "" (format " with `%s'" to-insert))
741 "? "
742 (if splice "{splice} " "")
743 "[y SPC r ! s q] (? for help)" )
744 '(?y ?n ?r ?\ ?! ?q ?s ??)))
745 (?r (funcall do-replace)
746 nil)
747 (?y (funcall do-replace)
748 t)
749 ((or ?\ ?n)
750 (unless replaced-this (cl-incf nbr-skipped))
751 t)
752 (?! (unless replaced-this
753 (funcall do-replace))
754 (setq replace-all t)
755 t)
756 (?s (cl-callf not splice)
757 (setq to-insert (funcall get-replacement-string))
758 nil)
759 (?q (setq done t)
760 t)
761 (?? (ignore (read-char el-search-search-and-replace-help-string))
762 nil)))))
763 (unless (or done (eobp)) (el-search--skip-expression nil t)))))
764 (el-search-hl-remove)
765 (goto-char opoint)
766 (message "Replaced %d matches%s"
767 nbr-replaced
768 (if (zerop nbr-skipped) ""
769 (format " (%d skipped)" nbr-skipped)))))
770
771 (defun el-search-query-replace-read-args ()
772 (barf-if-buffer-read-only)
773 (let* ((from (el-search--read-pattern "Replace from: "))
774 (to (let ((el-search--initial-mb-contents nil))
775 (el-search--read-pattern "Replace with result of evaluation of: " from))))
776 (list (el-search--wrap-pattern (read from)) (read to)
777 (with-temp-buffer
778 (insert to)
779 (el-search--create-read-map 1)))))
780
781 ;;;###autoload
782 (defun el-search-query-replace (from to &optional mapping)
783 "Replace some occurrences of FROM pattern with evaluated TO."
784 (interactive (el-search-query-replace-read-args))
785 (setq this-command 'el-search-query-replace) ;in case we come from isearch
786 (setq el-search-current-pattern from)
787 (barf-if-buffer-read-only)
788 (el-search-search-and-replace-pattern from to mapping))
789
790 (defun el-search--take-over-from-isearch ()
791 (let ((other-end isearch-other-end)
792 (input isearch-string))
793 (isearch-exit)
794 (when (and other-end (< other-end (point)))
795 (goto-char other-end))
796 input))
797
798 ;;;###autoload
799 (defun el-search-search-from-isearch ()
800 ;; FIXME: an interesting alternative would be to really integrate it
801 ;; with Isearch, using `isearch-search-fun-function'.
802 ;; Alas, this is not trivial if we want to transfer our optimizations.
803 (interactive)
804 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
805 ;; use `call-interactively' so we get recorded in `extended-command-history'
806 (call-interactively #'el-search-pattern)))
807
808 ;;;###autoload
809 (defun el-search-replace-from-isearch ()
810 (interactive)
811 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
812 (call-interactively #'el-search-query-replace)))
813
814
815
816 (provide 'el-search)
817 ;;; el-search.el ends here