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