1 ;;; el-search.el --- Expression based incremental search for emacs-lisp-mode -*- lexical-binding: t -*-
3 ;; Copyright (C) 2015 Free Software Foundation, Inc
5 ;; Author: Michael Heerdegen <michael_heerdegen@web.de>
6 ;; Maintainer: Michael Heerdegen <michael_heerdegen@web.de>
7 ;; Created: 29 Jul 2015
9 ;; Compatibility: GNU Emacs 25
11 ;; Package-Requires: ((emacs "25"))
14 ;; This file is not part of GNU Emacs.
16 ;; GNU Emacs is free software: you can redistribute it and/or modify
17 ;; it under the terms of the GNU General Public License as published by
18 ;; the Free Software Foundation, either version 3 of the License, or
19 ;; (at your option) any later version.
21 ;; GNU Emacs is distributed in the hope that it will be useful,
22 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ;; GNU General Public License for more details.
26 ;; You should have received a copy of the GNU General Public License
27 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
36 ;; The main user entry point is `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).
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.
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.
53 ;; Example 1: if you enter
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
63 ;; Example 2: If you enter the pattern
67 ;; you search for all defvar forms that don't specify an init value.
69 ;; The following will search for defvar forms with a docstring whose
70 ;; first line is longer than 70 characters:
73 ;; ,(and s (guard (< 70 (length (car (split-string s "\n")))))))
76 ;; When a search pattern is processed, the searched buffer is current
77 ;; with point at the beginning of the currently tested expression.
83 ;; For pattern input, the minibuffer is put into `emacs-lisp-mode'.
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'.
92 ;; If you want to search a buffer for symbols that are defined in
93 ;; "cl-lib", you can use this pattern
95 ;; (guard (and (symbolp exp)
96 ;; (when-let ((file (symbol-file exp)))
97 ;; (string-match-p "cl-lib\\.elc?$" file))))
100 ;; ,----------------------------------------------------------------------
101 ;; | Q: "But I hate `pcase'! Can't we just do without?" |
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 ;; `----------------------------------------------------------------------
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.
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.
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',
131 ;; (foo 'a (* 2 (+ 3 4)) t)
135 ;; (foo (* 2 (+ 3 4)) 'a t).
139 ;; M-x el-search-query-replace RET
140 ;; `(foo ,a ,b . ,rest) RET
141 ;; `(foo ,b ,a . ,rest) RET
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
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.
156 ;; Suggested key bindings
157 ;; ======================
159 ;; (define-key emacs-lisp-mode-map [(control ?S)] #'el-search-pattern)
160 ;; (define-key emacs-lisp-mode-map [(control ?%)] #'el-search-query-replace)
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)
165 ;; The bindings in `isearch-mode-map' let you conveniently switch to
166 ;; elisp searching from isearch.
169 ;; Bugs, Known Limitations
170 ;; =======================
172 ;; - Replacing: in some cases the reader syntax of forms
173 ;; is changing due to reading+printing. "Some" because we can treat
174 ;; that problem in most cases.
176 ;; - Similarly: Comments are normally preserved (where it makes
177 ;; sense). But when replacing like `(foo ,a ,b) -> `(foo ,b ,a)
186 ;; the comment will be lost.
192 ;; Thanks to Stefan Monnier for corrections and advice.
197 ;; - When replacing like (progn A B C) -> A B C, the layout of the
198 ;; whole "group" A B C as a unit is lost. Instead of restoring layout
199 ;; as we do now (via "read mappings"), we could just make a backup of
200 ;; the original expression as a string, and use our search machinery
201 ;; to find occurrences in the replacement recursively.
203 ;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
205 ;; - implement backward searching
207 ;; - improve docstrings
209 ;; - handle more reader syntaxes, e.g. #n, #n#
211 ;; - Implement sessions; add multi-file support based on iterators. A
212 ;; file list is read in (or the user can specify an iterator as a
213 ;; variable). The state in the current buffer is just (buffer
214 ;; . marker). Or should this be abstracted into an own lib? Could
215 ;; be named "files-session" or so.
227 (require 'elisp-mode)
229 (require 'help-fns) ;el-search--make-docstring
232 ;;;; Configuration stuff
234 (defgroup el-search nil
235 "Expression based search and replace for `emacs-lisp-mode'."
238 (defcustom el-search-this-expression-identifier 'exp
239 "Name of the identifier referring to the current expression.
240 The default value is `exp'. You can use this name in the search
241 prompt to refer to the value of the currently tested expression."
244 (defface el-search-match '((((background dark)) (:background "#0000A0"))
245 (t (:background "DarkSlateGray3")))
246 "Face for highlighting the current match.")
248 (defface el-search-other-match '((((background dark)) (:background "#202060"))
249 (t (:background "DarkSlateGray1")))
250 "Face for highlighting the other matches.")
255 (defun el-search--print (expr)
256 (let ((print-quoted t)
259 (prin1-to-string expr)))
261 (defvar el-search-read-expression-map
262 (let ((map (make-sparse-keymap)))
263 (set-keymap-parent map read-expression-map)
264 (define-key map [(control ?g)] #'abort-recursive-edit)
265 (define-key map [up] nil)
266 (define-key map [down] nil)
267 (define-key map [(control meta backspace)] #'backward-kill-sexp)
268 (define-key map [(control ?S)] #'exit-minibuffer)
270 "Map for reading input with `el-search-read-expression'.")
272 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
273 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
274 "Read expression for `my-eval-expression'."
275 (minibuffer-with-setup-hook
278 (use-local-map el-search-read-expression-map)
279 (setq font-lock-mode t)
280 (funcall font-lock-function 1)
283 (goto-char (point-max)))
284 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
285 (or hist 'read-expression-history) default)))
287 (defvar el-search--initial-mb-contents nil)
289 (defun el-search--read-pattern (prompt &optional default read)
290 (let ((this-sexp (sexp-at-point)))
291 (minibuffer-with-setup-hook
294 (let ((more-defaults (list (concat "'" (el-search--print this-sexp)))))
295 (setq-local minibuffer-default-add-function
296 (lambda () (if (listp minibuffer-default)
297 (append minibuffer-default more-defaults)
298 (cons minibuffer-default more-defaults)))))))
299 (el-search-read-expression
300 prompt el-search--initial-mb-contents 'el-search-history default read))))
302 (defun el-search--end-of-sexp ()
303 ;;Point must be at sexp beginning
304 (or (scan-sexps (point) 1) (point-max)))
306 (defun el-search--ensure-sexp-start ()
307 "Move point to the beginning of the next sexp if necessary.
308 Don't move if already at beginning of a sexp.
309 Point must not be inside a string or comment."
310 (let ((not-done t) res)
312 (let ((stop-here nil)
313 (looking-at-from-back (lambda (regexp n)
316 (looking-at regexp)))))
317 (while (not stop-here)
319 ((eobp) (signal 'end-of-buffer nil))
320 ((looking-at (rx (and (* space) ";"))) (forward-line))
321 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
323 ;; FIXME: can the rest be done more generically?
324 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
325 (not (looking-at "\\_<"))
326 (not (funcall looking-at-from-back ",@" 2)))
328 ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
329 (and (looking-at "@") (funcall looking-at-from-back "," 1)))
331 (t (setq stop-here t)))))
334 (setq res (save-excursion (read (current-buffer))))
336 (error (forward-char))))
339 (defvar el-search--pcase-macros '()
340 "List of additional \"el-search\" pcase macros.")
342 (defun el-search--make-docstring ()
343 ;; code mainly from `pcase--make-docstring'
344 (let* ((main (documentation (symbol-function 'el-search-pattern) 'raw))
345 (ud (help-split-fundoc main 'pcase)))
347 (insert (or (cdr ud) main))
349 (pcase-lambda (`(,symbol . ,fun))
350 (when-let ((doc (documentation fun)))
352 (setq doc (help-fns--signature symbol doc fun fun nil))
353 (insert "\n" (or doc "Not documented."))))
354 (reverse el-search--pcase-macros))
355 (let ((combined-doc (buffer-string)))
356 (if ud (help-add-fundoc-usage combined-doc (car ud)) combined-doc)))))
358 (put 'el-search-pattern 'function-documentation '(el-search--make-docstring))
360 (defmacro el-search-defpattern (name args &rest body)
361 "Like `pcase-defmacro', but limited to el-search patterns.
362 The semantics is exactly that of `pcase-defmacro', but the scope
363 of the definitions is limited to \"el-search\"."
364 (declare (indent 2) (debug defun))
365 `(setf (alist-get ',name el-search--pcase-macros)
366 (lambda ,args ,@body)))
369 (defmacro el-search--with-additional-pcase-macros (&rest body)
370 `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
371 `((get ',symbol 'pcase-macroexpander) #',fun))
372 el-search--pcase-macros)
375 (defun el-search--matcher (pattern &rest body)
376 (eval ;use `eval' to allow for user defined pattern types at run time
377 `(el-search--with-additional-pcase-macros
378 (let ((byte-compile-debug t) ;make undefined pattern types raise an error
379 (warning-suppress-log-types '((bytecomp)))
380 (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
381 (byte-compile (lambda (expression)
383 (,pattern ,@(or body (list t)))
386 (defun el-search--match-p (matcher expression)
387 (funcall matcher expression))
389 (defun el-search--wrap-pattern (pattern)
390 `(and ,el-search-this-expression-identifier ,pattern))
392 (defun el-search--skip-expression (expression &optional read)
393 ;; Move forward at least one character. Don't move into a string or
394 ;; comment. Don't move further than the beginning of the next sexp.
395 ;; Try to move as far as possible. Point must be at the beginning
397 ;; If there are positions where `read' would succeed, but that do
398 ;; not represent a valid sexp start, move past them (e.g. when
399 ;; before "#'" move past both characters).
401 ;; EXPRESSION must be the (read) expression at point, but when READ
402 ;; is non-nil, ignore the first argument and read the expression at
404 (when read (setq expression (save-excursion (read (current-buffer)))))
406 ((or (null expression)
407 (equal [] expression)
408 (not (or (listp expression) (vectorp expression))))
409 (goto-char (el-search--end-of-sexp)))
410 ((looking-at (rx (or ",@" "," "#'" "'")))
411 (goto-char (match-end 0)))
414 (defun el-search--search-pattern-1 (matcher &optional noerror)
415 (let ((match-beg nil) (opoint (point)) current-expr)
417 ;; when inside a string or comment, move past it
418 (let ((syntax-here (syntax-ppss)))
419 (when (nth 3 syntax-here) ;inside a string
420 (goto-char (nth 8 syntax-here))
422 (when (nth 4 syntax-here) ;inside a comment
424 (while (and (not (eobp)) (looking-at (rx (and (* space) ";"))))
428 (while (not match-beg)
430 (setq current-expr (el-search--ensure-sexp-start))
433 (throw 'no-match t)))
434 (if (el-search--match-p matcher current-expr)
435 (setq match-beg (point)
437 (el-search--skip-expression current-expr))))
438 (if noerror nil (signal 'end-of-buffer nil)))
441 (defun el-search--search-pattern (pattern &optional noerror)
442 "Search elisp buffer with `pcase' PATTERN.
443 Set point to the beginning of the occurrence found and return
444 point. Optional second argument, if non-nil, means if fail just
445 return nil (no error)."
446 (el-search--search-pattern-1 (el-search--matcher pattern) noerror))
448 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
449 ;; In current buffer, for any expression start between POS and BOUND
450 ;; or (point-max), in order, call two argument function DO-FUN with
451 ;; the current sexp string and the ending position of the current
452 ;; sexp. When done, with RET-FUN given, call it with no args and
453 ;; return the result; else, return nil.
457 (while (< (point) (or bound (point-max)))
458 (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
459 (this-sexp-string (buffer-substring-no-properties (point) this-sexp-end)))
460 (funcall do-fun this-sexp-string this-sexp-end)
461 (el-search--skip-expression (read this-sexp-string))
462 (el-search--ensure-sexp-start)))
464 (when ret-fun (funcall ret-fun))))
466 (defun el-search--create-read-map (&optional pos)
468 (el-search--do-subsexps
470 (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
471 (lambda () (nreverse mapping))
472 (save-excursion (thing-at-point--end-of-sexp) (point)))))
474 (defun el-search--repair-replacement-layout (printed mapping)
477 (el-search--do-subsexps
479 (lambda (sexp sexp-end)
480 (when-let ((old (cdr (assoc (read sexp) mapping))))
481 (delete-region (point) sexp-end)
482 (when (string-match-p "\n" old)
483 (unless (looking-back "^[[:space:]]*" (line-beginning-position))
485 (unless (looking-at "[[:space:]\)]*$")
488 (save-excursion (insert old))))
489 (lambda () (buffer-substring (point-min) (point-max))))))
491 (defun el-search--check-pattern-args (type args predicate &optional message)
492 "Check whether all ARGS fulfill PREDICATE.
493 Raise an error if not. TYPE and optional argument MESSAGE are
494 used to construct the error message."
496 (unless (funcall predicate arg)
497 (error (concat "Pattern `%S': "
498 (or message (format "argument doesn't fulfill %S" predicate))
504 ;;;; Additional pattern type definitions
506 (defun el-search--split (matcher1 matcher2 list)
507 "Helper for the append pattern type.
509 When a splitting of LIST into two lists L1, L2 exist so that Li
510 is matched by MATCHERi, return (L1 L2) for such Li, else return
512 (let ((try-match (lambda (list1 list2)
513 (when (and (el-search--match-p matcher1 list1)
514 (el-search--match-p matcher2 list2))
515 (list list1 list2))))
516 (list1 list) (list2 '()) (match nil))
517 ;; don't use recursion, this could hit `max-lisp-eval-depth'
518 (while (and (not (setq match (funcall try-match list1 list2)))
520 (let ((last-list1 (last list1)))
521 (if-let ((cdr-last-list1 (cdr last-list1)))
522 ;; list1 is a dotted list. Then list2 must be empty.
523 (progn (setcdr last-list1 nil)
524 (setq list2 cdr-last-list1))
525 (setq list1 (butlast list1 1)
526 list2 (cons (car last-list1) list2)))))
529 (el-search-defpattern append (&rest patterns)
530 "Matches any list factorable into lists matched by PATTERNS in order.
532 PATTERNS is a list of patterns P1..Pn. Match any list L for that
533 lists L1..Ln exist that are matched by P1..Pn in order and L is
534 equal to the concatenation of L1..Ln. Ln is allowed to be no
537 When different ways of matching are possible, it is unspecified
542 (append '(1 2 3) x (app car-safe 7))
544 matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 6)."
547 (pcase-let ((`(,pattern . ,more-patterns) patterns))
549 ((null more-patterns) pattern)
550 ((null (cdr more-patterns))
552 (app ,(apply-partially #'el-search--split
553 (el-search--matcher pattern)
554 (el-search--matcher (car more-patterns)))
555 (,'\` ((,'\, ,pattern)
556 (,'\, ,(car more-patterns)))))))
557 (t `(append ,pattern (append ,@more-patterns)))))))
559 (el-search-defpattern string (&rest regexps)
560 "Matches any string that is matched by all REGEXPS."
561 (el-search--check-pattern-args 'string regexps #'stringp)
562 (let ((string (make-symbol "string"))
563 (regexp (make-symbol "regexp")))
565 (pred (lambda (,string)
567 (lambda (,regexp) (string-match-p ,regexp ,string))
568 (list ,@regexps)))))))
570 (el-search-defpattern symbol (&rest regexps)
571 "Matches any symbol whose name is matched by all REGEXPS."
572 (el-search--check-pattern-args 'symbol regexps #'stringp)
574 (app symbol-name (string ,@regexps))))
576 (defun el-search--contains-p (matcher exp)
577 "Return non-nil when tree EXP contains a match for MATCHER.
578 Recurse on all types of sequences. In the positive case the
579 return value is (t elt), where ELT is a matching element found in
581 (if (el-search--match-p matcher exp)
584 (let ((try-match (apply-partially #'el-search--contains-p matcher)))
586 (or (funcall try-match (car exp))
587 (funcall try-match (cdr exp)))
588 (cl-some try-match exp))))))
590 (el-search-defpattern contains (&rest patterns)
591 "Matches trees that contain a match for all PATTERNs.
592 Searches any tree of sequences recursively for matches. Objects
593 of any kind matched by all PATTERNs are also matched.
595 Example: (contains (string \"H\") 17) matches ((\"Hallo\") x (5 [1 17]))"
598 ((null (cdr patterns))
599 (let ((pattern (car patterns)))
600 `(app ,(apply-partially #'el-search--contains-p (el-search--matcher pattern))
601 (,'\` (t (,'\, ,pattern))))))
602 (t `(and ,@(mapcar (lambda (pattern) `(contains ,pattern)) patterns)))))
604 (el-search-defpattern not (pattern)
605 "Matches any object that is not matched by PATTERN."
606 `(app ,(apply-partially #'el-search--match-p (el-search--matcher pattern))
609 (defun el-search--match-symbol-file (regexp symbol)
610 (when-let ((symbol-file (and (symbolp symbol)
611 (symbol-file symbol))))
613 (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
614 (file-name-sans-extension (file-name-nondirectory symbol-file)))))
616 (el-search-defpattern source (regexp)
617 "Matches any symbol whose `symbol-file' is matched by REGEXP.
619 This pattern matches when the object is a symbol for that
620 `symbol-file' returns a (non-nil) FILE-NAME that fulfills
621 (string-match-p REGEXP (file-name-sans-extension
622 (file-name-nondirectory FILENAME)))
624 REGEXP can also be a symbol, in which case
626 (concat \"^\" (symbol-name regexp) \"$\")
628 is used as regular expression."
629 (el-search--check-pattern-args 'source (list regexp) #'stringp)
630 `(pred (el-search--match-symbol-file ,regexp)))
632 (defun el-search--match-key-sequence (keys expr)
633 (when-let ((expr-keys (pcase expr
634 ((or (pred stringp) (pred vectorp)) expr)
635 (`(kbd ,(and (pred stringp) string)) (ignore-errors (kbd string))))))
637 (mapcar (lambda (keys) (ignore-errors (key-description keys)))
638 (list keys expr-keys)))))
640 (el-search-defpattern keys (key-sequence)
641 "Matches descriptions of the KEY-SEQUENCE.
642 KEY-SEQUENCE is a string or vector representing a key sequence,
643 or an expression of the form (kbd STRING).
645 Match any description of the same key sequence in any of these
652 matches any of these expressions:
658 (when (eq (car-safe key-sequence) 'kbd)
659 (setq key-sequence (kbd (cadr key-sequence))))
660 (el-search--check-pattern-args 'keys (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
661 "argument not a string or vector")
662 `(pred (el-search--match-key-sequence ,key-sequence)))
664 (defun el-search--s (expr)
666 ((symbolp expr) `(symbol ,(symbol-name expr)))
667 ((stringp expr) `(string ,expr))
670 (el-search-defpattern l (&rest lpats)
671 "Alternative pattern type for matching lists.
672 Match any list with subsequent elements matched by all LPATS in
675 The idea is to be able to search for pieces of code (i.e. lists)
676 with very brief input by using a specialized syntax.
678 An LPAT can take the following forms:
680 SYMBOL Matches any symbol matched by SYMBOL's name interpreted as
682 STRING Matches any string matched by STRING interpreted as a
684 _ Matches any list element
685 __ Matches any number of list elements (including zero)
686 ^ Matches zero elements, but only at the beginning of a list
687 $ Matches zero elements, but only at the end of a list
688 PAT Anything else is interpreted as a normal pcase pattern, and
689 matches one list element matched by it
691 ^ is only valid as the first, $ as the last of the LPATS.
693 Example: To match defuns that contain \"hl\" in their name and
694 have at least one mandatory, but also optional arguments, you
695 could use this pattern:
697 (l ^ 'defun hl (l _ &optional))"
698 (let ((match-start nil) (match-end nil))
699 (when (eq (car-safe lpats) '^)
701 (cl-callf cdr lpats))
702 (when (eq (car-safe (last lpats)) '$)
704 (cl-callf butlast lpats 1))
705 `(append ,@(if match-start '() '(_))
711 ('_? '(or '() `(,_))) ;FIXME: useful - document? or should we provide a (? PAT)
713 (_ `(,'\` ((,'\, ,(el-search--s elt)))))))
715 ,@(if match-end '() '(_)))))
720 (defvar-local el-search-hl-overlay nil)
722 (defvar-local el-search-hl-other-overlays '())
724 (defvar el-search-keep-hl nil)
726 (defun el-search-hl-sexp (&optional bounds)
727 (let ((bounds (or bounds
728 (list (point) (el-search--end-of-sexp)))))
729 (if (overlayp el-search-hl-overlay)
730 (apply #'move-overlay el-search-hl-overlay bounds)
731 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
732 'face 'el-search-match))
733 (overlay-put el-search-hl-overlay 'priority 1002))
734 (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
736 (defun el-search--hl-other-matches-1 (pattern from to)
737 (mapc #'delete-overlay el-search-hl-other-overlays)
738 (setq el-search-hl-other-overlays '())
739 (let ((matcher (el-search--matcher pattern))
740 this-match-beg this-match-end
745 (setq this-match-beg (el-search--search-pattern-1 matcher t))
746 (if (not this-match-beg)
748 (goto-char this-match-beg)
749 (setq this-match-end (el-search--end-of-sexp))
750 (let ((ov (make-overlay this-match-beg this-match-end)))
751 (overlay-put ov 'face 'el-search-other-match)
752 (overlay-put ov 'priority 1001)
753 (push ov el-search-hl-other-overlays)
754 (goto-char this-match-end)
755 (when (>= (point) to) (setq done t))))))))
757 (defun el-search-hl-other-matches (pattern)
758 "Highlight all matches visible in the selected window."
759 (el-search--hl-other-matches-1 pattern
761 (goto-char (window-start))
762 (beginning-of-defun-raw)
765 (add-hook 'window-scroll-functions #'el-search--after-scroll t t))
767 (defun el-search--after-scroll (_win start)
768 (el-search--hl-other-matches-1 el-search-current-pattern
771 (beginning-of-defun-raw)
775 (defun el-search-hl-remove ()
776 (when (overlayp el-search-hl-overlay)
777 (delete-overlay el-search-hl-overlay))
778 (remove-hook 'window-scroll-functions #'el-search--after-scroll t)
779 (mapc #'delete-overlay el-search-hl-other-overlays)
780 (setq el-search-hl-other-overlays '()))
782 (defun el-search-hl-post-command-fun ()
783 (unless (or el-search-keep-hl
784 (eq this-command 'el-search-query-replace)
785 (eq this-command 'el-search-pattern))
786 (el-search-hl-remove)
787 (remove-hook 'post-command-hook 'el-search-hl-post-command-fun t)))
792 (defvar el-search-history '()
793 "List of input strings.")
795 (defvar el-search-success nil)
796 (defvar el-search-current-pattern nil)
799 (defun el-search-pattern (pattern)
800 "Start new or resume last elisp search.
802 Search current buffer for expressions that are matched by `pcase'
803 PATTERN. Use `read' to transform buffer contents into
807 Additional `pcase' pattern types to be used with this command can
808 be defined with `el-search-defpattern'.
810 The following additional pattern types are currently defined:"
811 (interactive (list (if (and (eq this-command last-command)
813 el-search-current-pattern
815 (el-search--read-pattern "Find pcase pattern: "
816 (car el-search-history)
818 ;; A very common mistake: input "foo" instead of "'foo"
819 (when (and (symbolp pattern)
820 (not (eq pattern '_))
821 (or (not (boundp pattern))
822 (not (eq (symbol-value pattern) pattern))))
823 (error "Please don't forget the quote when searching for a symbol"))
824 (el-search--wrap-pattern pattern)))))
825 (setq this-command 'el-search-pattern) ;in case we come from isearch
826 (setq el-search-current-pattern pattern)
827 (let ((opoint (point)))
828 (when (and (eq this-command last-command) el-search-success)
829 (el-search--skip-expression nil t))
830 (setq el-search-success nil)
831 (when (condition-case nil
832 (el-search--search-pattern pattern)
833 (end-of-buffer (message "No match")
835 (el-search-hl-remove)
838 (setq el-search-success t)
840 (unless (eq this-command last-command)
841 (el-search-hl-other-matches pattern)))))
843 (defvar el-search-search-and-replace-help-string
845 y Replace this match and move to the next.
846 SPC or n Skip this match and move to the next.
847 r Replace this match but don't move.
848 ! Replace all remaining matches automatically.
849 q Quit. To resume, use e.g. `repeat-complex-command'.
851 s Toggle splicing mode. When splicing mode is
852 on (default off), the replacement expression must
853 evaluate to a list, and the result is spliced into the
854 buffer, instead of just inserted.
856 Hit any key to proceed."
857 "Help string for ? in `el-search-query-replace'.")
859 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
860 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
861 (el-search-keep-hl t) (opoint (point))
862 (get-replacement (el-search--matcher pattern replacement)))
864 (while (and (not done) (el-search--search-pattern pattern t))
865 (setq opoint (point))
868 (unless (eq this-command last-command)
869 (el-search-hl-other-matches pattern)))
870 (let* ((read-mapping (el-search--create-read-map))
871 (region (list (point) (el-search--end-of-sexp)))
872 (substring (apply #'buffer-substring-no-properties region))
873 (expr (read substring))
875 (new-expr (funcall get-replacement expr))
876 (get-replacement-string
877 (lambda () (if (and splice (not (listp new-expr)))
878 (error "Expression to splice in is an atom")
879 (el-search--repair-replacement-layout
881 (mapconcat #'el-search--print new-expr " ")
882 (el-search--print new-expr))
883 (append mapping read-mapping)))))
884 (to-insert (funcall get-replacement-string))
885 (do-replace (lambda ()
887 (apply #'delete-region region)
888 (let ((inhibit-message t)
891 (indent-region opoint (point))
892 (el-search-hl-sexp (list opoint (point)))
894 (cl-incf nbr-replaced)
895 (setq replaced-this t))))
898 (while (not (pcase (if replaced-this
899 (read-char-choice "[SPC ! q] (? for help)"
902 (concat "Replace this occurrence"
903 (if (or (string-match-p "\n" to-insert)
904 (< 40 (length to-insert)))
905 "" (format " with `%s'" to-insert))
907 (if splice "{splice} " "")
908 "[y SPC r ! s q] (? for help)" )
909 '(?y ?n ?r ?\ ?! ?q ?s ??)))
910 (?r (funcall do-replace)
912 (?y (funcall do-replace)
915 (unless replaced-this (cl-incf nbr-skipped))
917 (?! (unless replaced-this
918 (funcall do-replace))
921 (?s (cl-callf not splice)
922 (setq to-insert (funcall get-replacement-string))
926 (?? (ignore (read-char el-search-search-and-replace-help-string))
928 (unless (or done (eobp)) (el-search--skip-expression nil t)))))
929 (el-search-hl-remove)
931 (message "Replaced %d matches%s"
933 (if (zerop nbr-skipped) ""
934 (format " (%d skipped)" nbr-skipped)))))
936 (defun el-search-query-replace-read-args ()
937 (barf-if-buffer-read-only)
938 (let* ((from (el-search--read-pattern "Replace from: "))
939 (to (let ((el-search--initial-mb-contents nil))
940 (el-search--read-pattern "Replace with result of evaluation of: " from))))
941 (list (el-search--wrap-pattern (read from)) (read to)
944 (el-search--create-read-map 1)))))
947 (defun el-search-query-replace (from to &optional mapping)
948 "Replace some occurrences of FROM pattern with evaluated TO."
949 (interactive (el-search-query-replace-read-args))
950 (setq this-command 'el-search-query-replace) ;in case we come from isearch
951 (setq el-search-current-pattern from)
952 (barf-if-buffer-read-only)
953 (el-search-search-and-replace-pattern from to mapping))
955 (defun el-search--take-over-from-isearch ()
956 (prog1 isearch-string (isearch-exit)))
959 (defun el-search-search-from-isearch ()
960 ;; FIXME: an interesting alternative would be to really integrate it
961 ;; with Isearch, using `isearch-search-fun-function'.
962 ;; Alas, this is not trivial if we want to transfer our optimizations.
964 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
965 ;; use `call-interactively' so we get recorded in `extended-command-history'
966 (call-interactively #'el-search-pattern)))
969 (defun el-search-replace-from-isearch ()
971 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
972 (call-interactively #'el-search-query-replace)))
977 ;;; el-search.el ends here