]> code.delx.au - gnu-emacs-elpa/blob - packages/el-search/el-search.el
Make debbugs-newest-bugs more robust
[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.2.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 ;; 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 ;; Convenience
81 ;; ===========
82 ;;
83 ;; For pattern input, the minibuffer is put into `emacs-lisp-mode'.
84 ;;
85 ;; Any input PATTERN is silently transformed into (and exp PATTERN)
86 ;; so that you can always refer to the whole currently tested
87 ;; expression via the variable `exp'.
88 ;;
89 ;;
90 ;; Example 3:
91 ;;
92 ;; If you want to search a buffer for symbols that are defined in
93 ;; "cl-lib", you can use this pattern
94 ;;
95 ;; (guard (and (symbolp exp)
96 ;; (when-let ((file (symbol-file exp)))
97 ;; (string-match-p "cl-lib\\.elc?$" file))))
98 ;;
99 ;;
100 ;; ,----------------------------------------------------------------------
101 ;; | Q: "But I hate `pcase'! Can't we just do without?" |
102 ;; | |
103 ;; | A: Respect that you kept up until here! Just use (guard CODE), where|
104 ;; | CODE is any normal Elisp expression that returns non-nil when and |
105 ;; | only when you have a match. Use the variable `exp' to refer to |
106 ;; | the currently tested expression. Just like in the last example! |
107 ;; `----------------------------------------------------------------------
108 ;;
109 ;;
110 ;; It's cumbersome to write out the same complicated pattern
111 ;; constructs in the minibuffer again and again. You can define your
112 ;; own pcase pattern types for the purpose of el-search with
113 ;; `el-search-defpattern'. It is just like `pcase-defmacro', but the
114 ;; effect is limited to this package. See C-h f `el-search-pattern'
115 ;; for a list of predefined additional pattern forms.
116 ;;
117 ;;
118 ;; Replacing
119 ;; =========
120 ;;
121 ;; You can replace expressions with command `el-search-query-replace'.
122 ;; You are queried for a (pcase) pattern and a replacement expression.
123 ;; For each match of the pattern, the replacement expression is
124 ;; evaluated with the bindings created by the pcase matching in
125 ;; effect, and printed to produce the replacement string.
126 ;;
127 ;; Example: In some buffer you want to swap the two expressions at the
128 ;; places of the first two arguments in all calls of function `foo',
129 ;; so that e.g.
130 ;;
131 ;; (foo 'a (* 2 (+ 3 4)) t)
132 ;;
133 ;; becomes
134 ;;
135 ;; (foo (* 2 (+ 3 4)) 'a t).
136 ;;
137 ;; This will do it:
138 ;;
139 ;; M-x el-search-query-replace RET
140 ;; `(foo ,a ,b . ,rest) RET
141 ;; `(foo ,b ,a . ,rest) RET
142 ;;
143 ;; Type y to replace a match and go to the next one, r to replace
144 ;; without moving, SPC to go to the next match and ! to replace all
145 ;; remaining matches automatically. q quits. n is like SPC, so that
146 ;; y and n work like in isearch (meaning "yes" and "no") if you are
147 ;; used to that.
148 ;;
149 ;; It is possible to replace a match with multiple expressions using
150 ;; "splicing mode". When it is active, the replacement expression
151 ;; must evaluate to a list, and is spliced instead of inserted into
152 ;; the buffer for any replaced match. Use s to toggle splicing mode
153 ;; in a `el-search-query-replace' session.
154 ;;
155 ;;
156 ;; Suggested key bindings
157 ;; ======================
158 ;;
159 ;; (define-key emacs-lisp-mode-map [(control ?S)] #'el-search-pattern)
160 ;; (define-key emacs-lisp-mode-map [(control ?%)] #'el-search-query-replace)
161 ;;
162 ;; (define-key isearch-mode-map [(control ?S)] #'el-search-search-from-isearch)
163 ;; (define-key isearch-mode-map [(control ?%)] #'el-search-replace-from-isearch)
164 ;;
165 ;; (define-key el-search-read-expression-map [(control ?S)] #'exit-minibuffer)
166 ;;
167 ;; The bindings in `isearch-mode-map' let you conveniently switch to
168 ;; "el-search" searching from isearch. The binding in
169 ;; `el-search-read-expression-map' allows you to hit C-S twice to
170 ;; start a search for the last search pattern.
171 ;;
172 ;;
173 ;; Bugs, Known Limitations
174 ;; =======================
175 ;;
176 ;; - Replacing: in some cases the reader syntax of forms
177 ;; is changing due to reading+printing. "Some" because we can treat
178 ;; that problem in most cases.
179 ;;
180 ;; - Similarly: Comments are normally preserved (where it makes
181 ;; sense). But when replacing like `(foo ,a ,b) -> `(foo ,b ,a)
182 ;;
183 ;; in a content like
184 ;;
185 ;; (foo
186 ;; a
187 ;; ;;a comment
188 ;; b)
189 ;;
190 ;; the comment will be lost.
191 ;;
192 ;; FIXME: when we have resumable sessions, pause and warn about this case.
193 ;;
194 ;;
195 ;; Acknowledgments
196 ;; ===============
197 ;;
198 ;; Thanks to Stefan Monnier for corrections and advice.
199 ;;
200 ;;
201 ;; TODO:
202 ;;
203 ;; - implement backward searching
204 ;;
205 ;; - Make `el-search-pattern' accept an &optional limit, at least for
206 ;; the non-interactive use case?
207 ;;
208 ;; - improve docstrings
209 ;;
210 ;; - handle more reader syntaxes, e.g. #n, #n#
211 ;;
212 ;; - Implement sessions; add multi-file support based on iterators. A
213 ;; file list is read in (or the user can specify an iterator as a
214 ;; variable). The state in the current buffer is just (buffer
215 ;; . marker). Or should this be abstracted into an own lib? Could
216 ;; be named "files-session" or so.
217 ;;
218 ;; - Make `el-search--format-replacement' work non-heuristically.
219 ;; Idea: When replacing, for every variable V bound by the search
220 ;; pattern that directly corresponds to some text T, provide some
221 ;; "match data" V -> T. Use this when formatting the replacement.
222 ;; Maybe use a special marker to "paste" in expressions, like (paste
223 ;; V), whereby the `paste' flag lands in the replacement and can be
224 ;; replaced textually afterwards.
225
226
227
228 ;;; Code:
229
230 ;;;; Requirements
231
232 (eval-when-compile
233 (require 'subr-x))
234
235 (require 'cl-lib)
236 (require 'elisp-mode)
237 (require 'thingatpt)
238 (require 'help-fns) ;el-search--make-docstring
239
240
241 ;;;; Configuration stuff
242
243 (defgroup el-search nil
244 "Expression based search and replace for `emacs-lisp-mode'."
245 :group 'lisp)
246
247 (defcustom el-search-this-expression-identifier 'exp
248 "Identifier ID referring to the current expression in pattern input.
249 When entering a PATTERN in an interactive \"el-search\" command,
250 the pattern actually used will be (and ID PATTERN).
251 The default value is `exp'."
252 :type 'symbol)
253
254 (defface el-search-match '((((background dark)) (:background "#0000A0"))
255 (t (:background "DarkSlateGray3")))
256 "Face for highlighting the current match.")
257
258 (defface el-search-other-match '((((background dark)) (:background "#202060"))
259 (t (:background "DarkSlateGray1")))
260 "Face for highlighting the other matches.")
261
262 (defcustom el-search-smart-case-fold-search t
263 "Whether to use smart case folding in pattern matching.
264 When an \"el-search\" pattern involves regexp matching (like for
265 \"string\" or \"source\") and this option is non-nil,
266 case-fold-search will be temporarily bound to t if the according
267 regexp contains any upper case letter, and nil else. This is
268 done independently for every single matching operation.
269
270 If nil, the value of `case-fold-search' is decisive."
271 :type 'boolean)
272
273 (defcustom el-search-use-sloppy-strings nil
274 "Whether to allow the usage of \"sloppy strings\".
275 When this option is turned on, for faster typing you are allowed
276 to specify symbols instead of strings as arguments to an
277 \"el-search\" pattern type that would otherwise accept only
278 strings, and their names will be used as input (with other words,
279 this spares you to type the string delimiters in many cases).
280
281 For example,
282
283 \(source ^cl\)
284
285 is then equivalent to
286
287 \(source \"^cl\"\)
288
289 When this option is off, the first form would just signal an
290 error."
291 :type 'boolean)
292
293
294 ;;;; Helpers
295
296 (defun el-search--smart-string-match-p (regexp string)
297 "`string-match-p' taking `el-search-smart-case-fold-search' into account."
298 (let ((case-fold-search (if el-search-smart-case-fold-search
299 (not (let ((case-fold-search nil))
300 (string-match-p "[[:upper:]]" regexp)))
301 case-fold-search)))
302 (string-match-p regexp string)))
303
304 (defun el-search--pp-to-string (expr)
305 (let ((print-length nil)
306 (print-level nil))
307 (pp-to-string expr)))
308
309 (defvar el-search-read-expression-map
310 (let ((map (make-sparse-keymap)))
311 (set-keymap-parent map read-expression-map)
312 (define-key map [(control ?g)] #'abort-recursive-edit)
313 (define-key map [up] nil)
314 (define-key map [down] nil)
315 (define-key map [(control ?j)] #'newline)
316 map)
317 "Map for reading input with `el-search-read-expression'.")
318
319 (defun el-search--setup-minibuffer ()
320 (let ((inhibit-read-only t))
321 (put-text-property 1 (minibuffer-prompt-end) 'font-lock-face 'minibuffer-prompt))
322 (emacs-lisp-mode)
323 (use-local-map el-search-read-expression-map)
324 (setq font-lock-mode t)
325 (funcall font-lock-function 1)
326 (goto-char (minibuffer-prompt-end))
327 (when (looking-at ".*\n")
328 (indent-sexp))
329 (goto-char (point-max))
330 (when-let ((this-sexp (with-current-buffer (window-buffer (minibuffer-selected-window))
331 (thing-at-point 'sexp))))
332 (let ((more-defaults (list (concat "'" this-sexp))))
333 (setq-local minibuffer-default-add-function
334 (lambda () (if (listp minibuffer-default)
335 (append minibuffer-default more-defaults)
336 (cons minibuffer-default more-defaults)))))))
337
338 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
339 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
340 "Read expression for `my-eval-expression'."
341 (minibuffer-with-setup-hook #'el-search--setup-minibuffer
342 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
343 (or hist 'read-expression-history) default)))
344
345 (defvar el-search-history '()
346 "List of search input strings.")
347
348 (defvar el-search-query-replace-history '()
349 "List of input strings from `el-search-query-replace'.")
350
351 (defvar el-search--initial-mb-contents nil)
352
353 (defun el-search--pushnew-to-history (input histvar)
354 (let ((hist-head (car (symbol-value histvar))))
355 (unless (or (string-match-p "\\`\\'" input)
356 (and (stringp hist-head)
357 (or (string= input hist-head)
358 (ignore-errors (equal (read input) (read hist-head))))))
359 (push (if (string-match-p "\\`.+\n" input)
360 (with-temp-buffer
361 (emacs-lisp-mode)
362 (insert "\n" input)
363 (indent-region 1 (point))
364 (buffer-string))
365 input)
366 (symbol-value histvar)))))
367
368 (defun el-search--read-pattern (prompt &optional default histvar)
369 (cl-callf or histvar 'el-search-history)
370 (let ((input (el-search-read-expression
371 prompt el-search--initial-mb-contents histvar default)))
372 (el-search--pushnew-to-history input histvar)
373 (if (not (string= input "")) input (car (symbol-value histvar)))))
374
375 (defun el-search--end-of-sexp ()
376 ;;Point must be at sexp beginning
377 (or (scan-sexps (point) 1) (point-max)))
378
379 (defun el-search--ensure-sexp-start ()
380 "Move point to the next sexp beginning position.
381 Don't move if already at beginning of a sexp. Point must not be
382 inside a string or comment. `read' the expression at that point
383 and return it."
384 ;; This doesn't catch end-of-buffer to keep the return value non-ambiguous
385 (let ((not-done t) res)
386 (while not-done
387 (let ((stop-here nil)
388 (looking-at-from-back (lambda (regexp n)
389 (and (<= n (- (point) (point-min)))
390 (save-excursion
391 (backward-char n)
392 (looking-at regexp))))))
393 (while (not stop-here)
394 (cond
395 ((eobp) (signal 'end-of-buffer nil))
396 ((looking-at (rx (and (* space) ";"))) (forward-line))
397 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
398
399 ;; FIXME: can the rest be done more generically?
400 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
401 (not (looking-at "\\_<"))
402 (not (funcall looking-at-from-back ",@" 2)))
403 (forward-symbol 1))
404 ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
405 (and (looking-at "@") (funcall looking-at-from-back "," 1)))
406 (forward-char))
407 (t (setq stop-here t)))))
408 (condition-case nil
409 (progn
410 (setq res (save-excursion (read (current-buffer))))
411 (setq not-done nil))
412 (error (forward-char))))
413 res))
414
415 (defvar el-search--pcase-macros '()
416 "List of additional \"el-search\" pcase macros.")
417
418 (defun el-search--make-docstring ()
419 ;; code mainly from `pcase--make-docstring'
420 (let* ((main (documentation (symbol-function 'el-search-pattern) 'raw))
421 (ud (help-split-fundoc main 'pcase)))
422 (with-temp-buffer
423 (insert (or (cdr ud) main))
424 (mapc
425 (pcase-lambda (`(,symbol . ,fun))
426 (when-let ((doc (documentation fun)))
427 (insert "\n\n\n-- ")
428 (setq doc (help-fns--signature symbol doc fun fun nil))
429 (insert "\n" (or doc "Not documented."))))
430 (reverse el-search--pcase-macros))
431 (let ((combined-doc (buffer-string)))
432 (if ud (help-add-fundoc-usage combined-doc (car ud)) combined-doc)))))
433
434 (put 'el-search-pattern 'function-documentation '(el-search--make-docstring))
435
436 (defmacro el-search-defpattern (name args &rest body)
437 "Like `pcase-defmacro', but limited to el-search patterns.
438 The semantics is exactly that of `pcase-defmacro', but the scope
439 of the definitions is limited to \"el-search\"."
440 (declare (indent 2) (debug defun))
441 `(setf (alist-get ',name el-search--pcase-macros)
442 (lambda ,args ,@body)))
443
444 (defun el-search--macroexpand-1 (pattern)
445 "Expand \"el-search\" PATTERN.
446 This is like `pcase--macroexpand', but expands only patterns
447 defined with `el-search-defpattern' and performs only one
448 expansion step.
449
450 Return PATTERN if this pattern type was not defined with
451 `el-search-defpattern'."
452 (if-let ((expander (alist-get (car-safe pattern) el-search--pcase-macros)))
453 (apply expander (cdr pattern))
454 pattern))
455
456 (defmacro el-search--with-additional-pcase-macros (&rest body)
457 `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
458 `((get ',symbol 'pcase-macroexpander) #',fun))
459 el-search--pcase-macros)
460 ,@body))
461
462 (defun el-search--matcher (pattern &rest body)
463 (eval ;use `eval' to allow for user defined pattern types at run time
464 (let ((expression (make-symbol "expression")))
465 `(el-search--with-additional-pcase-macros
466 (let ((byte-compile-debug t) ;make undefined pattern types raise an error
467 (warning-suppress-log-types '((bytecomp)))
468 (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
469 (byte-compile (lambda (,expression)
470 (pcase ,expression
471 (,pattern ,@(or body (list t)))
472 (_ nil)))))))))
473
474 (defun el-search--match-p (matcher expression)
475 (funcall matcher expression))
476
477 (defun el-search--wrap-pattern (pattern)
478 `(and ,el-search-this-expression-identifier ,pattern))
479
480 (defun el-search--skip-expression (expression &optional read)
481 ;; Move forward at least one character. Don't move into a string or
482 ;; comment. Don't move further than the beginning of the next sexp.
483 ;; Try to move as far as possible. Point must be at the beginning
484 ;; of an expression.
485 ;; If there are positions where `read' would succeed, but that do
486 ;; not represent a valid sexp start, move past them (e.g. when
487 ;; before "#'" move past both characters).
488 ;;
489 ;; EXPRESSION must be the (read) expression at point, but when READ
490 ;; is non-nil, ignore the first argument and read the expression at
491 ;; point instead.
492 (when read (setq expression (save-excursion (read (current-buffer)))))
493 (cond
494 ((or (null expression)
495 (equal [] expression)
496 (not (or (listp expression) (vectorp expression))))
497 (goto-char (el-search--end-of-sexp)))
498 ((looking-at (rx (or ",@" "," "#'" "'")))
499 (goto-char (match-end 0)))
500 (t (forward-char))))
501
502 (defun el-search--search-pattern-1 (matcher &optional noerror)
503 (let ((match-beg nil) (opoint (point)) current-expr)
504
505 ;; when inside a string or comment, move past it
506 (let ((syntax-here (syntax-ppss)))
507 (when (nth 3 syntax-here) ;inside a string
508 (goto-char (nth 8 syntax-here))
509 (forward-sexp))
510 (when (nth 4 syntax-here) ;inside a comment
511 (forward-line 1)
512 (while (and (not (eobp)) (looking-at (rx (and (* space) ";"))))
513 (forward-line 1))))
514
515 (if (catch 'no-match
516 (while (not match-beg)
517 (condition-case nil
518 (setq current-expr (el-search--ensure-sexp-start))
519 (end-of-buffer
520 (goto-char opoint)
521 (throw 'no-match t)))
522 (if (el-search--match-p matcher current-expr)
523 (setq match-beg (point)
524 opoint (point))
525 (el-search--skip-expression current-expr))))
526 (if noerror nil (signal 'end-of-buffer nil)))
527 match-beg))
528
529 (defun el-search--search-pattern (pattern &optional noerror)
530 "Search elisp buffer with `pcase' PATTERN.
531 Set point to the beginning of the occurrence found and return
532 point. Optional second argument, if non-nil, means if fail just
533 return nil (no error)."
534 (el-search--search-pattern-1 (el-search--matcher pattern) noerror))
535
536 (defun el-search--replace-hunk (region to-insert)
537 "Replace the text in REGION in current buffer with string TO-INSERT.
538 Add line breaks before and after TO-INSERT when appropriate and
539 reindent."
540 (atomic-change-group
541 (let* ((inhibit-message t)
542 (opoint (point))
543 (original-text (prog1 (apply #'buffer-substring-no-properties region)
544 (goto-char (car region))
545 (apply #'delete-region region)))
546 ;; care about other sexps in this line
547 (sexp-before-us (not (looking-back "\(\\|^\\s-*" (line-beginning-position))))
548 (sexp-after-us (not (looking-at "\\s-*[;\)]\\|$")))
549 (insert-newline-before
550 (or
551 (and (string-match-p "\n" to-insert)
552 (not (string-match-p "\n" original-text))
553 (or (and sexp-before-us sexp-after-us)
554 (looking-back
555 (rx (or (syntax word) (syntax symbol))
556 (+ blank)
557 (or (syntax word) (syntax symbol))
558 (* any))
559 (line-beginning-position))))
560 ;; (and sexp-before-us
561 ;; (> (+ (apply #'max (mapcar #'length (split-string to-insert "\n")))
562 ;; (- (point) (line-beginning-position)))
563 ;; fill-column))
564 ))
565 (insert-newline-after (and insert-newline-before sexp-after-us)))
566 (when insert-newline-before
567 (when (looking-back "\\s-+" (line-beginning-position))
568 (delete-region (match-beginning 0) (match-end 0)))
569 (insert "\n"))
570 (insert to-insert)
571 (when insert-newline-after
572 (insert "\n"))
573 (indent-region opoint (1+ (point))))))
574
575 (defun el-search--format-replacement (replacement original replace-expr-input splice)
576 ;; Return a printed representation of REPLACEMENT. Try to reuse the
577 ;; layout of subexpressions shared with the original (replaced)
578 ;; expression and the replace expression.
579 (if (and splice (not (listp replacement)))
580 (error "Expression to splice in is an atom")
581 (let ((orig-buffer (generate-new-buffer "orig-expr")))
582 (with-current-buffer orig-buffer
583 (emacs-lisp-mode)
584 (insert original)
585 (when replace-expr-input (insert "\n\n" replace-expr-input)))
586 (unwind-protect
587 (with-temp-buffer
588 (emacs-lisp-mode)
589 (insert (if splice
590 (mapconcat #'el-search--pp-to-string replacement " ")
591 (el-search--pp-to-string replacement)))
592 (goto-char 1)
593 (let (start this-sexp end orig-match-start orig-match-end done)
594 (while (and (< (point) (point-max))
595 (condition-case nil
596 (progn
597 (setq start (point)
598 this-sexp (read (current-buffer))
599 end (point))
600 t)
601 (end-of-buffer nil)))
602 (setq done nil orig-match-start nil)
603 (with-current-buffer orig-buffer
604 (goto-char 1)
605 (if (el-search--search-pattern `',this-sexp t)
606 (setq orig-match-start (point)
607 orig-match-end (progn (forward-sexp) (point)))
608 (setq done t)))
609 ;; find out whether we have a sequence of equal expressions
610 (while (and (not done)
611 (condition-case nil
612 (progn (setq this-sexp (read (current-buffer))) t)
613 ((invalid-read-syntax end-of-buffer end-of-file) nil)))
614 (if (with-current-buffer orig-buffer
615 (condition-case nil
616 (if (not (equal this-sexp (read (current-buffer))))
617 nil
618 (setq orig-match-end (point))
619 t)
620 ((invalid-read-syntax end-of-buffer end-of-file) nil)))
621 (setq end (point))
622 (setq done t)))
623 ;; FIXME: there could be another occurrence of THIS-SEXP in ORIG-BUFFER with more
624 ;; subsequent equal expressions after it
625 (if orig-match-start
626 (el-search--replace-hunk
627 (list start end)
628 (with-current-buffer orig-buffer
629 (buffer-substring-no-properties orig-match-start orig-match-end)))
630 (goto-char start)
631 (el-search--skip-expression nil t))
632 (condition-case nil
633 (el-search--ensure-sexp-start)
634 (end-of-buffer (goto-char (point-max))))))
635 (goto-char 1)
636 (forward-sexp)
637 (let ((result (buffer-substring 1 (point))))
638 (if (equal replacement (read result))
639 result
640 (error "Error in `el-search--format-replacement' - please make a bug report"))))
641 (kill-buffer orig-buffer)))))
642
643 (defun el-search--check-pattern-args (type args predicate &optional message)
644 "Check whether all ARGS fulfill PREDICATE.
645 Raise an error if not. The string arguments TYPE and optional
646 MESSAGE are used to construct the error message."
647 (mapc (lambda (arg)
648 (unless (funcall predicate arg)
649 (error (concat "Pattern `%s': "
650 (or message (format "argument doesn't fulfill %S" predicate))
651 ": %S")
652 type arg)))
653 args))
654
655 (defvar el-search-current-pattern nil)
656
657 (defvar el-search-success nil)
658
659
660 ;;;; Additional pattern type definitions
661
662 (defun el-search--split (matcher1 matcher2 list)
663 "Helper for the append pattern type.
664
665 When a splitting of LIST into two lists L1, L2 exist so that Li
666 is matched by MATCHERi, return (L1 L2) for such Li, else return
667 nil."
668 (let ((try-match (lambda (list1 list2)
669 (when (and (el-search--match-p matcher1 list1)
670 (el-search--match-p matcher2 list2))
671 (list list1 list2))))
672 (list1 list) (list2 '()) (match nil))
673 ;; don't use recursion, this could hit `max-lisp-eval-depth'
674 (while (and (not (setq match (funcall try-match list1 list2)))
675 (consp list1))
676 (let ((last-list1 (last list1)))
677 (if-let ((cdr-last-list1 (cdr last-list1)))
678 ;; list1 is a dotted list. Then list2 must be empty.
679 (progn (setcdr last-list1 nil)
680 (setq list2 cdr-last-list1))
681 (setq list1 (butlast list1 1)
682 list2 (cons (car last-list1) list2)))))
683 match))
684
685 (el-search-defpattern append (&rest patterns)
686 "Matches any list factorable into lists matched by PATTERNS in order.
687
688 PATTERNS is a list of patterns P1..Pn. Match any list L for that
689 lists L1..Ln exist that are matched by P1..Pn in order and L is
690 equal to the concatenation of L1..Ln. Ln is allowed to be no
691 list.
692
693 When different ways of matching are possible, it is unspecified
694 which one is chosen.
695
696 Example: the pattern
697
698 (append '(1 2 3) x (app car-safe 7))
699
700 matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 6)."
701 (if (null patterns)
702 '(pred null)
703 (pcase-let ((`(,pattern . ,more-patterns) patterns))
704 (cond
705 ((null more-patterns) pattern)
706 ((null (cdr more-patterns))
707 `(and (pred listp)
708 (app ,(apply-partially #'el-search--split
709 (el-search--matcher pattern)
710 (el-search--matcher (car more-patterns)))
711 (,'\` ((,'\, ,pattern)
712 (,'\, ,(car more-patterns)))))))
713 (t `(append ,pattern (append ,@more-patterns)))))))
714
715 (defun el-search--stringish-p (thing)
716 (or (stringp thing) (and el-search-use-sloppy-strings (symbolp thing))))
717
718 (el-search-defpattern string (&rest regexps)
719 "Matches any string that is matched by all REGEXPS."
720 (el-search--check-pattern-args "string" regexps #'el-search--stringish-p
721 "Argument not a string")
722 `(and (pred stringp)
723 ,@(mapcar (lambda (thing) `(pred (el-search--smart-string-match-p
724 ,(if (symbolp thing) (symbol-name thing) thing))))
725 regexps)))
726
727 (el-search-defpattern symbol (&rest regexps)
728 "Matches any symbol whose name is matched by all REGEXPS."
729 (el-search--check-pattern-args "symbol" regexps #'el-search--stringish-p
730 "Argument not a string")
731 `(and (pred symbolp)
732 (app symbol-name (string ,@regexps))))
733
734 (defun el-search--contains-p (matcher exp)
735 "Return non-nil when tree EXP contains a match for MATCHER.
736 Recurse on all types of sequences. In the positive case the
737 return value is (t elt), where ELT is a matching element found in
738 EXP."
739 (if (el-search--match-p matcher exp)
740 (list t exp)
741 (and (sequencep exp)
742 (let ((try-match (apply-partially #'el-search--contains-p matcher)))
743 (if (consp exp)
744 (or (funcall try-match (car exp))
745 (funcall try-match (cdr exp)))
746 (cl-some try-match exp))))))
747
748 (el-search-defpattern contains (&rest patterns)
749 "Matches trees that contain a match for all PATTERNs.
750 Searches any tree of sequences recursively for matches. Objects
751 of any kind matched by all PATTERNs are also matched.
752
753 Example: (contains (string \"H\") 17) matches ((\"Hallo\") x (5 [1 17]))"
754 (cond
755 ((null patterns) '_)
756 ((null (cdr patterns))
757 (let ((pattern (car patterns)))
758 `(app ,(apply-partially #'el-search--contains-p (el-search--matcher pattern))
759 (,'\` (t (,'\, ,pattern))))))
760 (t `(and ,@(mapcar (lambda (pattern) `(contains ,pattern)) patterns)))))
761
762 (el-search-defpattern not (pattern)
763 "Matches any object that is not matched by PATTERN."
764 `(app ,(apply-partially #'el-search--match-p (el-search--matcher pattern))
765 (pred not)))
766
767 (defun el-search--match-symbol-file (regexp symbol)
768 (when-let ((symbol-file (and (symbolp symbol)
769 (symbol-file symbol))))
770 (el-search--smart-string-match-p
771 (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
772 (file-name-sans-extension (file-name-nondirectory symbol-file)))))
773
774 (el-search-defpattern source (regexp)
775 "Matches any symbol whose `symbol-file' is matched by REGEXP.
776
777 This pattern matches when the object is a symbol for that
778 `symbol-file' returns a (non-nil) FILE-NAME that fulfills
779 (string-match-p REGEXP (file-name-sans-extension
780 (file-name-nondirectory FILENAME)))
781
782 REGEXP can also be a symbol, in which case
783
784 (concat \"^\" (symbol-name regexp) \"$\")
785
786 is used as regular expression."
787 (el-search--check-pattern-args "source" (list regexp) #'el-search--stringish-p
788 "Argument not a string")
789 `(pred (el-search--match-symbol-file ,(if (symbolp regexp) (symbol-name regexp) regexp))))
790
791 (defun el-search--match-key-sequence (keys expr)
792 (when-let ((expr-keys (pcase expr
793 ((or (pred stringp) (pred vectorp)) expr)
794 (`(kbd ,(and (pred stringp) string)) (ignore-errors (kbd string))))))
795 (apply #'equal
796 (mapcar (lambda (keys) (ignore-errors (key-description keys)))
797 (list keys expr-keys)))))
798
799 (el-search-defpattern keys (key-sequence)
800 "Matches descriptions of the KEY-SEQUENCE.
801 KEY-SEQUENCE is a string or vector representing a key sequence,
802 or an expression of the form (kbd STRING).
803
804 Match any description of the same key sequence in any of these
805 formats.
806
807 Example: the pattern
808
809 (keys (kbd \"C-s\"))
810
811 matches any of these expressions:
812
813 \"\\C-s\"
814 \"\C-s\"
815 (kbd \"C-s\")
816 [(control ?s)]"
817 (when (eq (car-safe key-sequence) 'kbd)
818 (setq key-sequence (kbd (cadr key-sequence))))
819 (el-search--check-pattern-args "keys" (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
820 "argument not a string or vector")
821 `(pred (el-search--match-key-sequence ,key-sequence)))
822
823 (defun el-search--transform-nontrivial-lpat (expr)
824 (cond
825 ((symbolp expr) `(or (symbol ,(symbol-name expr))
826 (,'\` (,'quote (,'\, (symbol ,(symbol-name expr)))))
827 (,'\` (,'function (,'\, (symbol ,(symbol-name expr)))))))
828 ((stringp expr) `(string ,expr))
829 (t expr)))
830
831 (el-search-defpattern l (&rest lpats)
832 "Alternative pattern type for matching lists.
833 Match any list with subsequent elements matched by all LPATS in
834 order.
835
836 The idea is to be able to search for pieces of code (i.e. lists)
837 with very brief input by using a specialized syntax.
838
839 An LPAT can take the following forms:
840
841 SYMBOL Matches any symbol S matched by SYMBOL's name interpreted
842 as a regexp. Matches also 'S and #'S for any such S.
843 STRING Matches any string matched by STRING interpreted as a
844 regexp
845 _ Matches any list element
846 __ Matches any number of list elements (including zero)
847 ^ Matches zero elements, but only at the beginning of a list
848 $ Matches zero elements, but only at the end of a list
849 PAT Anything else is interpreted as a normal pcase pattern, and
850 matches one list element matched by it
851
852 ^ is only valid as the first, $ as the last of the LPATS.
853
854 Example: To match defuns that contain \"hl\" in their name and
855 have at least one mandatory, but also optional arguments, you
856 could use this pattern:
857
858 (l ^ 'defun hl (l _ &optional))"
859 (let ((match-start nil) (match-end nil))
860 (when (eq (car-safe lpats) '^)
861 (setq match-start t)
862 (cl-callf cdr lpats))
863 (when (eq (car-safe (last lpats)) '$)
864 (setq match-end t)
865 (cl-callf butlast lpats 1))
866 `(append ,@(if match-start '() '(_))
867 ,@(mapcar
868 (lambda (elt)
869 (pcase elt
870 ('__ '_)
871 ('_ '`(,_))
872 ('_? '(or '() `(,_))) ;FIXME: useful - document? or should we provide a (? PAT)
873 ;thing?
874 (_ `(,'\` ((,'\, ,(el-search--transform-nontrivial-lpat elt)))))))
875 lpats)
876 ,@(if match-end '() '(_)))))
877
878 (el-search-defpattern char-prop (property)
879 "Matches the object if completely covered with PROPERTY.
880 This pattern matches the object if its representation in the
881 search buffer is completely covered with the character property
882 PROPERTY.
883
884 This pattern always tests the complete expression in the search
885 buffer, it is not possible to test subexpressions calculated in
886 the search pattern."
887 `(guard (and (get-char-property (point) ',property)
888 ,(macroexp-let2 nil limit '(scan-sexps (point) 1)
889 `(= (next-single-char-property-change
890 (point) ',property nil ,limit)
891 ,limit)))))
892
893 (el-search-defpattern includes-prop (property)
894 "Matches the object if partly covered with PROPERTY.
895 This pattern matches the object if its representation in the
896 search buffer is partly covered with the character property
897 PROPERTY.
898
899 This pattern always tests the complete expression in the search
900 buffer, it is not possible to test subexpressions calculated in
901 the search pattern."
902 `(guard (or (get-char-property (point) ',property)
903 ,(macroexp-let2 nil limit '(scan-sexps (point) 1)
904 `(not (= (next-single-char-property-change
905 (point) ',property nil ,limit)
906 ,limit))))))
907
908 (defvar diff-hl-reference-revision)
909 (declare-function diff-hl-changes "diff-hl")
910 (defvar-local el-search--cached-changes nil)
911
912 (defun el-search--changes-from-diff-hl (revision)
913 "Return a list of changed regions (as conses of positions) since REVISION.
914 Use variable `el-search--cached-changes' for caching."
915 (if (and (consp el-search--cached-changes)
916 (equal (car el-search--cached-changes)
917 revision))
918 (cdr el-search--cached-changes)
919 (require 'diff-hl)
920 ;; `diff-hl-changes' returns line numbers. We must convert them into positions.
921 (save-restriction
922 (widen)
923 (save-excursion
924 (let ((diff-hl-reference-revision revision)
925 (current-line-nbr 1) change-beg)
926 (goto-char 1)
927 (cdr (setq el-search--cached-changes
928 (cons revision
929 (delq nil (mapcar (pcase-lambda (`(,start-line ,nbr-lines ,kind))
930 (if (eq kind 'delete) nil
931 (forward-line (- start-line current-line-nbr))
932 (setq change-beg (point))
933 (forward-line (1- nbr-lines))
934 (setq current-line-nbr (+ start-line nbr-lines -1))
935 (cons change-beg (line-end-position))))
936 (diff-hl-changes)))))))))))
937
938 (defun el-search--change-p (posn &optional revision)
939 ;; Non-nil when sexp after POSN is part of a change
940 (when (buffer-modified-p)
941 (error "Buffer is modified - please save"))
942 (save-restriction
943 (widen)
944 (let ((changes (el-search--changes-from-diff-hl revision))
945 (sexp-end (scan-sexps posn 1)))
946 (while (and changes (< (cdar changes) sexp-end))
947 (pop changes))
948 (and changes
949 (<= (caar changes) posn)))))
950
951 (defun el-search--changed-p (posn &optional revision)
952 ;; Non-nil when sexp after POSN contains a change
953 (when (buffer-modified-p)
954 (error "Buffer is modified - please save"))
955 (save-restriction
956 (widen)
957 (let ((changes (el-search--changes-from-diff-hl revision)))
958 (while (and changes (<= (cdar changes) posn))
959 (pop changes))
960 (and changes
961 (< (caar changes) (scan-sexps posn 1))))))
962
963 (el-search-defpattern change (&optional revision)
964 "Matches the object if its text is part of a file change.
965
966 Requires library \"diff-hl\". REVISION defaults to the file's
967 repository's HEAD commit."
968 `(guard (el-search--change-p (point) ,revision)))
969
970 (el-search-defpattern changed (&optional revision)
971 "Matches the object if its text contains a file change.
972
973 Requires library \"diff-hl\". REVISION defaults to the file's
974 repository's HEAD commit."
975 `(guard (el-search--changed-p (point) ,revision)))
976
977
978 ;;;; Highlighting
979
980 (defvar-local el-search-hl-overlay nil)
981
982 (defvar-local el-search-hl-other-overlays '())
983
984 (defvar el-search-keep-hl nil)
985
986 (defun el-search-hl-sexp (&optional bounds)
987 (let ((bounds (or bounds
988 (list (point) (el-search--end-of-sexp)))))
989 (if (overlayp el-search-hl-overlay)
990 (apply #'move-overlay el-search-hl-overlay bounds)
991 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
992 'face 'el-search-match))
993 (overlay-put el-search-hl-overlay 'priority 1002))
994 (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
995
996 (defun el-search--hl-other-matches-1 (pattern from to)
997 (mapc #'delete-overlay el-search-hl-other-overlays)
998 (setq el-search-hl-other-overlays '())
999 (let ((matcher (el-search--matcher pattern))
1000 this-match-beg this-match-end
1001 (done nil))
1002 (save-excursion
1003 (goto-char from)
1004 (while (not done)
1005 (setq this-match-beg (el-search--search-pattern-1 matcher t))
1006 (if (not this-match-beg)
1007 (setq done t)
1008 (goto-char this-match-beg)
1009 (setq this-match-end (el-search--end-of-sexp))
1010 (let ((ov (make-overlay this-match-beg this-match-end)))
1011 (overlay-put ov 'face 'el-search-other-match)
1012 (overlay-put ov 'priority 1001)
1013 (push ov el-search-hl-other-overlays)
1014 (goto-char this-match-end)
1015 (when (>= (point) to) (setq done t))))))))
1016
1017 (defun el-search-hl-other-matches (pattern)
1018 "Highlight all matches visible in the selected window."
1019 (el-search--hl-other-matches-1 pattern
1020 (save-excursion
1021 (goto-char (window-start))
1022 (beginning-of-defun-raw)
1023 (point))
1024 (window-end))
1025 (add-hook 'window-scroll-functions #'el-search--after-scroll t t))
1026
1027 (defun el-search--after-scroll (_win start)
1028 (el-search--hl-other-matches-1 el-search-current-pattern
1029 (save-excursion
1030 (goto-char start)
1031 (beginning-of-defun-raw)
1032 (point))
1033 (window-end nil t)))
1034
1035 (defun el-search-hl-remove ()
1036 (when (overlayp el-search-hl-overlay)
1037 (delete-overlay el-search-hl-overlay))
1038 (remove-hook 'window-scroll-functions #'el-search--after-scroll t)
1039 (mapc #'delete-overlay el-search-hl-other-overlays)
1040 (setq el-search-hl-other-overlays '()))
1041
1042 (defun el-search-hl-post-command-fun ()
1043 (unless (or el-search-keep-hl
1044 (eq this-command 'el-search-query-replace)
1045 (eq this-command 'el-search-pattern))
1046 (el-search-hl-remove)
1047 (remove-hook 'post-command-hook 'el-search-hl-post-command-fun t)))
1048
1049
1050 ;;;; Core functions
1051
1052 ;;;###autoload
1053 (defun el-search-pattern (pattern &optional no-error)
1054 "Start new or resume last elisp search.
1055
1056 Search current buffer for expressions that are matched by `pcase'
1057 PATTERN. Use `read' to transform buffer contents into
1058 expressions.
1059
1060 Use `emacs-lisp-mode' for reading input. Some keys in the
1061 minibuffer have a special binding: to make it possible to edit
1062 multi line input, C-j inserts a newline, and up and down move the
1063 cursor vertically - see `el-search-read-expression-map' for more
1064 details.
1065
1066
1067 Additional `pcase' pattern types to be used with this command can
1068 be defined with `el-search-defpattern'.
1069
1070 The following additional pattern types are currently defined:"
1071 (interactive (list (if (and (eq this-command last-command)
1072 el-search-success)
1073 el-search-current-pattern
1074 (let* ((input (el-search--read-pattern "Find pcase pattern: "
1075 (car el-search-history)))
1076 (pattern (read input)))
1077 ;; A very common mistake: input "foo" instead of "'foo"
1078 (when (and (symbolp pattern)
1079 (not (eq pattern '_))
1080 (or (not (boundp pattern))
1081 (not (eq (symbol-value pattern) pattern))))
1082 (error "Please don't forget the quote when searching for a symbol"))
1083 ;; Make input available also in query-replace history
1084 (el-search--pushnew-to-history input 'el-search-query-replace-history)
1085 ;; and wrap the PATTERN
1086 (el-search--wrap-pattern pattern)))))
1087 (if (not (called-interactively-p 'any))
1088 (el-search--search-pattern pattern no-error)
1089 (setq this-command 'el-search-pattern) ;in case we come from isearch
1090 (setq el-search-current-pattern pattern)
1091 (let ((opoint (point)))
1092 (when (and (eq this-command last-command) el-search-success)
1093 (el-search--skip-expression nil t))
1094 (setq el-search-success nil)
1095 (when (condition-case nil
1096 (el-search--search-pattern pattern)
1097 (end-of-buffer (message "No match")
1098 (goto-char opoint)
1099 (el-search-hl-remove)
1100 (ding)
1101 nil))
1102 (setq el-search-success t)
1103 (el-search-hl-sexp)
1104 (unless (eq this-command last-command)
1105 (el-search-hl-other-matches pattern))))))
1106
1107 (defvar el-search-search-and-replace-help-string
1108 "\
1109 y Replace this match and move to the next.
1110 SPC or n Skip this match and move to the next.
1111 r Replace this match but don't move.
1112 ! Replace all remaining matches automatically.
1113 q Quit. To resume, use e.g. `repeat-complex-command'.
1114 ? Show this help.
1115 s Toggle splicing mode. When splicing mode is
1116 on (default off), the replacement expression must
1117 evaluate to a list, and the result is spliced into the
1118 buffer, instead of just inserted.
1119
1120 Hit any key to proceed."
1121 "Help string for ? in `el-search-query-replace'.")
1122
1123 (defun el-search--search-and-replace-pattern (pattern replacement &optional splice to-input-string)
1124 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
1125 (el-search-keep-hl t) (opoint (point))
1126 (get-replacement (el-search--matcher pattern replacement))
1127 (skip-matches-in-replacement 'ask))
1128 (unwind-protect
1129 (while (and (not done) (el-search--search-pattern pattern t))
1130 (setq opoint (point))
1131 (unless replace-all
1132 (el-search-hl-sexp)
1133 (unless (eq this-command last-command)
1134 (el-search-hl-other-matches pattern)))
1135 (let* ((region (list (point) (el-search--end-of-sexp)))
1136 (original-text (apply #'buffer-substring-no-properties region))
1137 (expr (read original-text))
1138 (replaced-this nil)
1139 (new-expr (funcall get-replacement expr))
1140 (get-replacement-string
1141 (lambda () (el-search--format-replacement new-expr original-text to-input-string splice)))
1142 (to-insert (funcall get-replacement-string))
1143 (replacement-contains-another-match
1144 (with-temp-buffer
1145 (emacs-lisp-mode)
1146 (insert to-insert)
1147 (goto-char 1)
1148 (el-search--skip-expression new-expr)
1149 (condition-case nil
1150 (progn (el-search--ensure-sexp-start)
1151 (el-search--search-pattern pattern t))
1152 (end-of-buffer nil))))
1153 (do-replace
1154 (lambda ()
1155 (save-excursion
1156 (el-search--replace-hunk (list (point) (el-search--end-of-sexp)) to-insert))
1157 (el-search--ensure-sexp-start) ;skip potentially newly added whitespace
1158 (el-search-hl-sexp (list opoint (point)))
1159 (cl-incf nbr-replaced)
1160 (setq replaced-this t))))
1161 (if replace-all
1162 (funcall do-replace)
1163 (while (not (pcase (if replaced-this
1164 (read-char-choice "[SPC ! q] (? for help)"
1165 '(?\ ?! ?q ?\C-g ?n ??))
1166 (read-char-choice
1167 (concat "Replace this occurrence"
1168 (if (or (string-match-p "\n" to-insert)
1169 (< 40 (length to-insert)))
1170 "" (format " with `%s'" to-insert))
1171 "? "
1172 (if splice "{splice} " "")
1173 "[y SPC r ! s q] (? for help)" )
1174 '(?y ?n ?r ?\ ?! ?q ?\C-g ?s ??)))
1175 (?r (funcall do-replace)
1176 nil)
1177 (?y (funcall do-replace)
1178 t)
1179 ((or ?\ ?n)
1180 (unless replaced-this (cl-incf nbr-skipped))
1181 t)
1182 (?! (unless replaced-this
1183 (funcall do-replace))
1184 (setq replace-all t)
1185 t)
1186 (?s (cl-callf not splice)
1187 (setq to-insert (funcall get-replacement-string))
1188 nil)
1189 ((or ?q ?\C-g)
1190 (setq done t)
1191 t)
1192 (?? (ignore (read-char el-search-search-and-replace-help-string))
1193 nil)))))
1194 (unless (or done (eobp))
1195 (cond
1196 ((not (and replaced-this replacement-contains-another-match))
1197 (el-search--skip-expression nil t))
1198 ((eq skip-matches-in-replacement 'ask)
1199 (if (setq skip-matches-in-replacement
1200 (yes-or-no-p "Match in replacement - always skip? "))
1201 (forward-sexp)
1202 (el-search--skip-expression nil t)
1203 (when replace-all
1204 (setq replace-all nil)
1205 (message "Falling back to interactive mode")
1206 (sit-for 3.))))
1207 (skip-matches-in-replacement (forward-sexp))
1208 (t
1209 (el-search--skip-expression nil t)
1210 (message "Replacement contains another match%s"
1211 (if replace-all " - falling back to interactive mode" ""))
1212 (setq replace-all nil)
1213 (sit-for 2.)))))))
1214 (el-search-hl-remove)
1215 (goto-char opoint)
1216 (message "Replaced %d matches%s"
1217 nbr-replaced
1218 (if (zerop nbr-skipped) ""
1219 (format " (%d skipped)" nbr-skipped)))))
1220
1221 (defun el-search-query-replace--read-args ()
1222 (barf-if-buffer-read-only)
1223 (let ((from-input (let ((el-search--initial-mb-contents
1224 (or el-search--initial-mb-contents
1225 (and (eq last-command 'el-search-pattern)
1226 (car el-search-history)))))
1227 (el-search--read-pattern "Query replace pattern: " nil
1228 'el-search-query-replace-history)))
1229 from to)
1230 (with-temp-buffer
1231 (emacs-lisp-mode)
1232 (insert from-input)
1233 (goto-char 1)
1234 (forward-sexp)
1235 (skip-chars-forward " \t\n\f")
1236 ;; FIXME: maybe more sanity tests here...
1237 (if (not (looking-at "->"))
1238 (setq from from-input
1239 to (let ((el-search--initial-mb-contents nil))
1240 (el-search--read-pattern "Replace with result of evaluation of: " from)))
1241 (delete-char 2)
1242 (goto-char 1)
1243 (forward-sexp)
1244 (setq from (buffer-substring 1 (point)))
1245 (skip-chars-forward " \t\n\f")
1246 (setq to (buffer-substring (point) (progn (forward-sexp) (point))))))
1247 (unless (and el-search-query-replace-history
1248 (not (string= from from-input))
1249 (string= from-input (car el-search-query-replace-history)))
1250 (push (with-temp-buffer
1251 (emacs-lisp-mode)
1252 (insert (let ((newline-in-from (string-match-p "\n" from))
1253 (newline-in-to (string-match-p "\n" to)))
1254 (format "%s%s%s ->%s%s"
1255 (if (and (or newline-in-from newline-in-to)
1256 (not (string-match-p "\\`\n" from))) "\n" "")
1257 (if newline-in-from "\n" "" ) from
1258 (if (and (or newline-in-from newline-in-to)
1259 (not (string-match-p "\\`\n" to))) "\n" " ") to)))
1260 (indent-region 1 (point-max))
1261 (buffer-string))
1262 el-search-query-replace-history))
1263 (el-search--pushnew-to-history from 'el-search-history)
1264 (list (el-search--wrap-pattern (read from)) (read to) to)))
1265
1266 ;;;###autoload
1267 (defun el-search-query-replace (from-pattern to-expr &optional textual-to)
1268 "Replace some matches of \"el-search\" pattern FROM-PATTERN.
1269
1270 TO-EXPR is an Elisp expression that is evaluated repeatedly for
1271 each match with bindings created in FROM-PATTERN in effect to
1272 produce a replacement expression. Operate from point
1273 to (point-max).
1274
1275 As each match is found, the user must type a character saying
1276 what to do with it. For directions, type ? at that time.
1277
1278 As an alternative to enter FROM-PATTERN and TO-EXPR separately,
1279 you can also give an input of the form
1280
1281 FROM-PATTERN -> TO-EXPR
1282
1283 to the first prompt and specify both expressions at once. This
1284 format is also used for history entries."
1285 (interactive (el-search-query-replace--read-args))
1286 (setq this-command 'el-search-query-replace) ;in case we come from isearch
1287 (setq el-search-current-pattern from-pattern)
1288 (barf-if-buffer-read-only)
1289 (el-search--search-and-replace-pattern from-pattern to-expr nil textual-to))
1290
1291 (defun el-search--take-over-from-isearch (&optional goto-left-end)
1292 (let ((other-end (and goto-left-end isearch-other-end))
1293 (input isearch-string))
1294 (isearch-exit)
1295 (when (and other-end (< other-end (point)))
1296 (goto-char other-end))
1297 input))
1298
1299 ;;;###autoload
1300 (defun el-search-search-from-isearch ()
1301 ;; FIXME: an interesting alternative would be to really integrate it
1302 ;; with Isearch, using `isearch-search-fun-function'.
1303 ;; Alas, this is not trivial if we want to transfer our optimizations.
1304 (interactive)
1305 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
1306 ;; use `call-interactively' so we get recorded in `extended-command-history'
1307 (call-interactively #'el-search-pattern)))
1308
1309 ;;;###autoload
1310 (defun el-search-replace-from-isearch ()
1311 (interactive)
1312 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch t))))
1313 (call-interactively #'el-search-query-replace)))
1314
1315
1316
1317 (provide 'el-search)
1318 ;;; el-search.el ends here