]> code.delx.au - gnu-emacs-elpa/blob - packages/el-search/el-search.el
New user option: el-search-use-sloppy-strings
[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.3
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 ;; The bindings in `isearch-mode-map' let you conveniently switch to
166 ;; elisp searching from isearch.
167 ;;
168 ;;
169 ;; Bugs, Known Limitations
170 ;; =======================
171 ;;
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.
175 ;;
176 ;; - Similarly: Comments are normally preserved (where it makes
177 ;; sense). But when replacing like `(foo ,a ,b) -> `(foo ,b ,a)
178 ;;
179 ;; in a content like
180 ;;
181 ;; (foo
182 ;; a
183 ;; ;;a comment
184 ;; b)
185 ;;
186 ;; the comment will be lost.
187 ;;
188 ;;
189 ;; Acknowledgments
190 ;; ===============
191 ;;
192 ;; Thanks to Stefan Monnier for corrections and advice.
193 ;;
194 ;;
195 ;; TODO:
196 ;;
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.
202 ;;
203 ;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
204 ;;
205 ;; - implement backward searching
206 ;;
207 ;; - improve docstrings
208 ;;
209 ;; - handle more reader syntaxes, e.g. #n, #n#
210 ;;
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.
216
217
218
219 ;;; Code:
220
221 ;;;; Requirements
222
223 (eval-when-compile
224 (require 'subr-x))
225
226 (require 'cl-lib)
227 (require 'elisp-mode)
228 (require 'thingatpt)
229 (require 'help-fns) ;el-search--make-docstring
230
231
232 ;;;; Configuration stuff
233
234 (defgroup el-search nil
235 "Expression based search and replace for `emacs-lisp-mode'."
236 :group 'lisp)
237
238 (defcustom el-search-this-expression-identifier 'exp
239 "Identifier referring to the current expression in pattern input.
240 When entering a PATTERN in an interactive \"el-search\" command,
241 the pattern actually used will be
242
243 `(and ,el-search-this-expression-identifier ,pattern)
244
245 The default value is `exp'."
246 :type 'symbol)
247
248 (defface el-search-match '((((background dark)) (:background "#0000A0"))
249 (t (:background "DarkSlateGray3")))
250 "Face for highlighting the current match.")
251
252 (defface el-search-other-match '((((background dark)) (:background "#202060"))
253 (t (:background "DarkSlateGray1")))
254 "Face for highlighting the other matches.")
255
256 (defcustom el-search-smart-case-fold-search t
257 "Whether to use smart case folding in pattern matching.
258 When an \"el-search\" pattern involves regexp matching (like for
259 \"string\" or \"source\") and this option is non-nil,
260 case-fold-search will be temporarily bound to t if the according
261 regexp contains any upper case letter, and nil else. This is
262 done independently for every single matching operation.
263
264 If nil, the value of `case-fold-search' is decisive."
265 :type 'boolean)
266
267 (defcustom el-search-use-sloppy-strings nil
268 "Whether to allow the usage of \"sloppy strings\".
269 When this option is turned on, for faster typing you are allowed
270 to specify symbols instead of strings as arguments to an
271 \"el-search\" pattern type that would otherwise accept only
272 strings, and their names will be used as input (with other words,
273 this spares you to type the string delimiters in many cases).
274
275 For example,
276
277 \(source ^cl\)
278
279 is then equivalent to
280
281 \(source \"^cl\"\)
282
283 When this option is off, the first form would just signal an
284 error."
285 :type 'boolean)
286
287
288 ;;;; Helpers
289
290 (defun el-search--smart-string-match-p (regexp string)
291 "`string-match-p' taking `el-search-smart-case-fold-search' into account."
292 (let ((case-fold-search (if el-search-smart-case-fold-search
293 (not (let ((case-fold-search nil))
294 (string-match-p "[[:upper:]]" regexp)))
295 case-fold-search)))
296 (string-match-p regexp string)))
297
298 (defun el-search--print (expr)
299 (let ((print-quoted t)
300 (print-length nil)
301 (print-level nil))
302 (prin1-to-string expr)))
303
304 (defvar el-search-read-expression-map
305 (let ((map (make-sparse-keymap)))
306 (set-keymap-parent map read-expression-map)
307 (define-key map [(control ?g)] #'abort-recursive-edit)
308 (define-key map [up] nil)
309 (define-key map [down] nil)
310 (define-key map [(control meta backspace)] #'backward-kill-sexp)
311 (define-key map [(control ?S)] #'exit-minibuffer)
312 map)
313 "Map for reading input with `el-search-read-expression'.")
314
315 (defun el-search--setup-minibuffer ()
316 (emacs-lisp-mode)
317 (use-local-map el-search-read-expression-map)
318 (setq font-lock-mode t)
319 (funcall font-lock-function 1)
320 (backward-sexp)
321 (indent-sexp)
322 (goto-char (point-max))
323 (when-let ((this-sexp (with-current-buffer (window-buffer (minibuffer-selected-window))
324 (thing-at-point 'sexp))))
325 (let ((more-defaults (list (concat "'" this-sexp))))
326 (setq-local minibuffer-default-add-function
327 (lambda () (if (listp minibuffer-default)
328 (append minibuffer-default more-defaults)
329 (cons minibuffer-default more-defaults)))))))
330
331 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
332 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
333 "Read expression for `my-eval-expression'."
334 (minibuffer-with-setup-hook #'el-search--setup-minibuffer
335 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
336 (or hist 'read-expression-history) default)))
337
338 (defvar el-search--initial-mb-contents nil)
339
340 (defun el-search--read-pattern (prompt &optional default read)
341 (let ((input (el-search-read-expression
342 prompt el-search--initial-mb-contents 'el-search-history default read)))
343 (if (or read (not (string= input ""))) input (car el-search-history))))
344
345 (defun el-search--end-of-sexp ()
346 ;;Point must be at sexp beginning
347 (or (scan-sexps (point) 1) (point-max)))
348
349 (defun el-search--ensure-sexp-start ()
350 "Move point to the next sexp beginning position.
351 Don't move if already at beginning of a sexp. Point must not be
352 inside a string or comment. `read' the expression at that point
353 and return it."
354 (let ((not-done t) res)
355 (while not-done
356 (let ((stop-here nil)
357 (looking-at-from-back (lambda (regexp n)
358 (save-excursion
359 (backward-char n)
360 (looking-at regexp)))))
361 (while (not stop-here)
362 (cond
363 ((eobp) (signal 'end-of-buffer nil))
364 ((looking-at (rx (and (* space) ";"))) (forward-line))
365 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
366
367 ;; FIXME: can the rest be done more generically?
368 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
369 (not (looking-at "\\_<"))
370 (not (funcall looking-at-from-back ",@" 2)))
371 (forward-symbol 1))
372 ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
373 (and (looking-at "@") (funcall looking-at-from-back "," 1)))
374 (forward-char))
375 (t (setq stop-here t)))))
376 (condition-case nil
377 (progn
378 (setq res (save-excursion (read (current-buffer))))
379 (setq not-done nil))
380 (error (forward-char))))
381 res))
382
383 (defvar el-search--pcase-macros '()
384 "List of additional \"el-search\" pcase macros.")
385
386 (defun el-search--make-docstring ()
387 ;; code mainly from `pcase--make-docstring'
388 (let* ((main (documentation (symbol-function 'el-search-pattern) 'raw))
389 (ud (help-split-fundoc main 'pcase)))
390 (with-temp-buffer
391 (insert (or (cdr ud) main))
392 (mapc
393 (pcase-lambda (`(,symbol . ,fun))
394 (when-let ((doc (documentation fun)))
395 (insert "\n\n\n-- ")
396 (setq doc (help-fns--signature symbol doc fun fun nil))
397 (insert "\n" (or doc "Not documented."))))
398 (reverse el-search--pcase-macros))
399 (let ((combined-doc (buffer-string)))
400 (if ud (help-add-fundoc-usage combined-doc (car ud)) combined-doc)))))
401
402 (put 'el-search-pattern 'function-documentation '(el-search--make-docstring))
403
404 (defmacro el-search-defpattern (name args &rest body)
405 "Like `pcase-defmacro', but limited to el-search patterns.
406 The semantics is exactly that of `pcase-defmacro', but the scope
407 of the definitions is limited to \"el-search\"."
408 (declare (indent 2) (debug defun))
409 `(setf (alist-get ',name el-search--pcase-macros)
410 (lambda ,args ,@body)))
411
412 (defun el-search--macroexpand-1 (pattern)
413 "Expand \"el-search\" PATTERN.
414 This is like `pcase--macroexpand', but expands only patterns
415 defined with `el-search-defpattern' and performs only one
416 expansion step.
417
418 Return PATTERN if this pattern type was not defined with
419 `el-search-defpattern'."
420 (if-let ((expander (alist-get (car-safe pattern) el-search--pcase-macros)))
421 (apply expander (cdr pattern))
422 pattern))
423
424 (defmacro el-search--with-additional-pcase-macros (&rest body)
425 `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
426 `((get ',symbol 'pcase-macroexpander) #',fun))
427 el-search--pcase-macros)
428 ,@body))
429
430 (defun el-search--matcher (pattern &rest body)
431 (eval ;use `eval' to allow for user defined pattern types at run time
432 (let ((expression (make-symbol "expression")))
433 `(el-search--with-additional-pcase-macros
434 (let ((byte-compile-debug t) ;make undefined pattern types raise an error
435 (warning-suppress-log-types '((bytecomp)))
436 (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
437 (byte-compile (lambda (,expression)
438 (pcase ,expression
439 (,pattern ,@(or body (list t)))
440 (_ nil)))))))))
441
442 (defun el-search--match-p (matcher expression)
443 (funcall matcher expression))
444
445 (defun el-search--wrap-pattern (pattern)
446 `(and ,el-search-this-expression-identifier ,pattern))
447
448 (defun el-search--skip-expression (expression &optional read)
449 ;; Move forward at least one character. Don't move into a string or
450 ;; comment. Don't move further than the beginning of the next sexp.
451 ;; Try to move as far as possible. Point must be at the beginning
452 ;; of an expression.
453 ;; If there are positions where `read' would succeed, but that do
454 ;; not represent a valid sexp start, move past them (e.g. when
455 ;; before "#'" move past both characters).
456 ;;
457 ;; EXPRESSION must be the (read) expression at point, but when READ
458 ;; is non-nil, ignore the first argument and read the expression at
459 ;; point instead.
460 (when read (setq expression (save-excursion (read (current-buffer)))))
461 (cond
462 ((or (null expression)
463 (equal [] expression)
464 (not (or (listp expression) (vectorp expression))))
465 (goto-char (el-search--end-of-sexp)))
466 ((looking-at (rx (or ",@" "," "#'" "'")))
467 (goto-char (match-end 0)))
468 (t (forward-char))))
469
470 (defun el-search--search-pattern-1 (matcher &optional noerror)
471 (let ((match-beg nil) (opoint (point)) current-expr)
472
473 ;; when inside a string or comment, move past it
474 (let ((syntax-here (syntax-ppss)))
475 (when (nth 3 syntax-here) ;inside a string
476 (goto-char (nth 8 syntax-here))
477 (forward-sexp))
478 (when (nth 4 syntax-here) ;inside a comment
479 (forward-line 1)
480 (while (and (not (eobp)) (looking-at (rx (and (* space) ";"))))
481 (forward-line 1))))
482
483 (if (catch 'no-match
484 (while (not match-beg)
485 (condition-case nil
486 (setq current-expr (el-search--ensure-sexp-start))
487 (end-of-buffer
488 (goto-char opoint)
489 (throw 'no-match t)))
490 (if (el-search--match-p matcher current-expr)
491 (setq match-beg (point)
492 opoint (point))
493 (el-search--skip-expression current-expr))))
494 (if noerror nil (signal 'end-of-buffer nil)))
495 match-beg))
496
497 (defun el-search--search-pattern (pattern &optional noerror)
498 "Search elisp buffer with `pcase' PATTERN.
499 Set point to the beginning of the occurrence found and return
500 point. Optional second argument, if non-nil, means if fail just
501 return nil (no error)."
502 (el-search--search-pattern-1 (el-search--matcher pattern) noerror))
503
504 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
505 ;; In current buffer, for any expression start between POS and BOUND
506 ;; or (point-max), in order, call two argument function DO-FUN with
507 ;; the current sexp string and the ending position of the current
508 ;; sexp. When done, with RET-FUN given, call it with no args and
509 ;; return the result; else, return nil.
510 (save-excursion
511 (goto-char pos)
512 (condition-case nil
513 (while (< (point) (or bound (point-max)))
514 (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
515 (this-sexp-string (buffer-substring-no-properties (point) this-sexp-end)))
516 (funcall do-fun this-sexp-string this-sexp-end)
517 (el-search--skip-expression (read this-sexp-string))
518 (el-search--ensure-sexp-start)))
519 (end-of-buffer))
520 (when ret-fun (funcall ret-fun))))
521
522 (defun el-search--create-read-map (&optional pos)
523 (let ((mapping '()))
524 (el-search--do-subsexps
525 (or pos (point))
526 (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
527 (lambda () (nreverse mapping))
528 (save-excursion (thing-at-point--end-of-sexp) (point)))))
529
530 (defun el-search--repair-replacement-layout (printed mapping)
531 (with-temp-buffer
532 (insert printed)
533 (el-search--do-subsexps
534 (point-min)
535 (lambda (sexp sexp-end)
536 (when-let ((old (cdr (assoc (read sexp) mapping))))
537 (delete-region (point) sexp-end)
538 (when (string-match-p "\n" old)
539 (unless (looking-back "^[[:space:]]*" (line-beginning-position))
540 (insert "\n"))
541 (unless (looking-at "[[:space:]\)]*$")
542 (insert "\n")
543 (backward-char)))
544 (save-excursion (insert old))))
545 (lambda () (buffer-substring (point-min) (point-max))))))
546
547 (defun el-search--check-pattern-args (type args predicate &optional message)
548 "Check whether all ARGS fulfill PREDICATE.
549 Raise an error if not. TYPE and optional argument MESSAGE are
550 used to construct the error message."
551 (mapc (lambda (arg)
552 (unless (funcall predicate arg)
553 (error (concat "Pattern `%S': "
554 (or message (format "argument doesn't fulfill %S" predicate))
555 ": %S")
556 type arg)))
557 args))
558
559
560 ;;;; Additional pattern type definitions
561
562 (defun el-search--split (matcher1 matcher2 list)
563 "Helper for the append pattern type.
564
565 When a splitting of LIST into two lists L1, L2 exist so that Li
566 is matched by MATCHERi, return (L1 L2) for such Li, else return
567 nil."
568 (let ((try-match (lambda (list1 list2)
569 (when (and (el-search--match-p matcher1 list1)
570 (el-search--match-p matcher2 list2))
571 (list list1 list2))))
572 (list1 list) (list2 '()) (match nil))
573 ;; don't use recursion, this could hit `max-lisp-eval-depth'
574 (while (and (not (setq match (funcall try-match list1 list2)))
575 (consp list1))
576 (let ((last-list1 (last list1)))
577 (if-let ((cdr-last-list1 (cdr last-list1)))
578 ;; list1 is a dotted list. Then list2 must be empty.
579 (progn (setcdr last-list1 nil)
580 (setq list2 cdr-last-list1))
581 (setq list1 (butlast list1 1)
582 list2 (cons (car last-list1) list2)))))
583 match))
584
585 (el-search-defpattern append (&rest patterns)
586 "Matches any list factorable into lists matched by PATTERNS in order.
587
588 PATTERNS is a list of patterns P1..Pn. Match any list L for that
589 lists L1..Ln exist that are matched by P1..Pn in order and L is
590 equal to the concatenation of L1..Ln. Ln is allowed to be no
591 list.
592
593 When different ways of matching are possible, it is unspecified
594 which one is chosen.
595
596 Example: the pattern
597
598 (append '(1 2 3) x (app car-safe 7))
599
600 matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 6)."
601 (if (null patterns)
602 '(pred null)
603 (pcase-let ((`(,pattern . ,more-patterns) patterns))
604 (cond
605 ((null more-patterns) pattern)
606 ((null (cdr more-patterns))
607 `(and (pred listp)
608 (app ,(apply-partially #'el-search--split
609 (el-search--matcher pattern)
610 (el-search--matcher (car more-patterns)))
611 (,'\` ((,'\, ,pattern)
612 (,'\, ,(car more-patterns)))))))
613 (t `(append ,pattern (append ,@more-patterns)))))))
614
615 (defun el-search--stringish-p (thing)
616 (or (stringp thing) (and el-search-use-sloppy-strings (symbolp thing))))
617
618 (el-search-defpattern string (&rest regexps)
619 "Matches any string that is matched by all REGEXPS."
620 (el-search--check-pattern-args 'string regexps #'el-search--stringish-p
621 "Argument not a string")
622 (let ((string (make-symbol "string"))
623 (regexp (make-symbol "regexp")))
624 `(and (pred stringp)
625 (pred (lambda (,string)
626 (cl-every
627 (lambda (,regexp) (el-search--smart-string-match-p ,regexp ,string))
628 ',regexps))))))
629
630 (el-search-defpattern symbol (&rest regexps)
631 "Matches any symbol whose name is matched by all REGEXPS."
632 (el-search--check-pattern-args 'symbol regexps #'el-search--stringish-p
633 "Argument not a string")
634 `(and (pred symbolp)
635 (app symbol-name (string ,@regexps))))
636
637 (defun el-search--contains-p (matcher exp)
638 "Return non-nil when tree EXP contains a match for MATCHER.
639 Recurse on all types of sequences. In the positive case the
640 return value is (t elt), where ELT is a matching element found in
641 EXP."
642 (if (el-search--match-p matcher exp)
643 (list t exp)
644 (and (sequencep exp)
645 (let ((try-match (apply-partially #'el-search--contains-p matcher)))
646 (if (consp exp)
647 (or (funcall try-match (car exp))
648 (funcall try-match (cdr exp)))
649 (cl-some try-match exp))))))
650
651 (el-search-defpattern contains (&rest patterns)
652 "Matches trees that contain a match for all PATTERNs.
653 Searches any tree of sequences recursively for matches. Objects
654 of any kind matched by all PATTERNs are also matched.
655
656 Example: (contains (string \"H\") 17) matches ((\"Hallo\") x (5 [1 17]))"
657 (cond
658 ((null patterns) '_)
659 ((null (cdr patterns))
660 (let ((pattern (car patterns)))
661 `(app ,(apply-partially #'el-search--contains-p (el-search--matcher pattern))
662 (,'\` (t (,'\, ,pattern))))))
663 (t `(and ,@(mapcar (lambda (pattern) `(contains ,pattern)) patterns)))))
664
665 (el-search-defpattern not (pattern)
666 "Matches any object that is not matched by PATTERN."
667 `(app ,(apply-partially #'el-search--match-p (el-search--matcher pattern))
668 (pred not)))
669
670 (defun el-search--match-symbol-file (regexp symbol)
671 (when-let ((symbol-file (and (symbolp symbol)
672 (symbol-file symbol))))
673 (el-search--smart-string-match-p
674 (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
675 (file-name-sans-extension (file-name-nondirectory symbol-file)))))
676
677 (el-search-defpattern source (regexp)
678 "Matches any symbol whose `symbol-file' is matched by REGEXP.
679
680 This pattern matches when the object is a symbol for that
681 `symbol-file' returns a (non-nil) FILE-NAME that fulfills
682 (string-match-p REGEXP (file-name-sans-extension
683 (file-name-nondirectory FILENAME)))
684
685 REGEXP can also be a symbol, in which case
686
687 (concat \"^\" (symbol-name regexp) \"$\")
688
689 is used as regular expression."
690 (el-search--check-pattern-args 'source (list regexp) #'el-search--stringish-p
691 "Argument not a string")
692 `(pred (el-search--match-symbol-file ,(if (symbolp regexp) (symbol-name regexp) regexp))))
693
694 (defun el-search--match-key-sequence (keys expr)
695 (when-let ((expr-keys (pcase expr
696 ((or (pred stringp) (pred vectorp)) expr)
697 (`(kbd ,(and (pred stringp) string)) (ignore-errors (kbd string))))))
698 (apply #'equal
699 (mapcar (lambda (keys) (ignore-errors (key-description keys)))
700 (list keys expr-keys)))))
701
702 (el-search-defpattern keys (key-sequence)
703 "Matches descriptions of the KEY-SEQUENCE.
704 KEY-SEQUENCE is a string or vector representing a key sequence,
705 or an expression of the form (kbd STRING).
706
707 Match any description of the same key sequence in any of these
708 formats.
709
710 Example: the pattern
711
712 (keys (kbd \"C-s\"))
713
714 matches any of these expressions:
715
716 \"\\C-s\"
717 \"\C-s\"
718 (kbd \"C-s\")
719 [(control ?s)]"
720 (when (eq (car-safe key-sequence) 'kbd)
721 (setq key-sequence (kbd (cadr key-sequence))))
722 (el-search--check-pattern-args 'keys (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
723 "argument not a string or vector")
724 `(pred (el-search--match-key-sequence ,key-sequence)))
725
726 (defun el-search--s (expr)
727 (cond
728 ((symbolp expr) `(or (symbol ,(symbol-name expr))
729 (,'\` (,'quote (,'\, (symbol ,(symbol-name expr)))))
730 (,'\` (,'function (,'\, (symbol ,(symbol-name expr)))))))
731 ((stringp expr) `(string ,expr))
732 (t expr)))
733
734 (el-search-defpattern l (&rest lpats)
735 "Alternative pattern type for matching lists.
736 Match any list with subsequent elements matched by all LPATS in
737 order.
738
739 The idea is to be able to search for pieces of code (i.e. lists)
740 with very brief input by using a specialized syntax.
741
742 An LPAT can take the following forms:
743
744 SYMBOL Matches any symbol S matched by SYMBOL's name interpreted
745 as a regexp. Matches also 'S and #'S for any such S.
746 STRING Matches any string matched by STRING interpreted as a
747 regexp
748 _ Matches any list element
749 __ Matches any number of list elements (including zero)
750 ^ Matches zero elements, but only at the beginning of a list
751 $ Matches zero elements, but only at the end of a list
752 PAT Anything else is interpreted as a normal pcase pattern, and
753 matches one list element matched by it
754
755 ^ is only valid as the first, $ as the last of the LPATS.
756
757 Example: To match defuns that contain \"hl\" in their name and
758 have at least one mandatory, but also optional arguments, you
759 could use this pattern:
760
761 (l ^ 'defun hl (l _ &optional))"
762 (let ((match-start nil) (match-end nil))
763 (when (eq (car-safe lpats) '^)
764 (setq match-start t)
765 (cl-callf cdr lpats))
766 (when (eq (car-safe (last lpats)) '$)
767 (setq match-end t)
768 (cl-callf butlast lpats 1))
769 `(append ,@(if match-start '() '(_))
770 ,@(mapcar
771 (lambda (elt)
772 (pcase elt
773 ('__ '_)
774 ('_ '`(,_))
775 ('_? '(or '() `(,_))) ;FIXME: useful - document? or should we provide a (? PAT)
776 ;thing?
777 (_ `(,'\` ((,'\, ,(el-search--s elt)))))))
778 lpats)
779 ,@(if match-end '() '(_)))))
780
781
782 ;;;; Highlighting
783
784 (defvar-local el-search-hl-overlay nil)
785
786 (defvar-local el-search-hl-other-overlays '())
787
788 (defvar el-search-keep-hl nil)
789
790 (defun el-search-hl-sexp (&optional bounds)
791 (let ((bounds (or bounds
792 (list (point) (el-search--end-of-sexp)))))
793 (if (overlayp el-search-hl-overlay)
794 (apply #'move-overlay el-search-hl-overlay bounds)
795 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
796 'face 'el-search-match))
797 (overlay-put el-search-hl-overlay 'priority 1002))
798 (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
799
800 (defun el-search--hl-other-matches-1 (pattern from to)
801 (mapc #'delete-overlay el-search-hl-other-overlays)
802 (setq el-search-hl-other-overlays '())
803 (let ((matcher (el-search--matcher pattern))
804 this-match-beg this-match-end
805 (done nil))
806 (save-excursion
807 (goto-char from)
808 (while (not done)
809 (setq this-match-beg (el-search--search-pattern-1 matcher t))
810 (if (not this-match-beg)
811 (setq done t)
812 (goto-char this-match-beg)
813 (setq this-match-end (el-search--end-of-sexp))
814 (let ((ov (make-overlay this-match-beg this-match-end)))
815 (overlay-put ov 'face 'el-search-other-match)
816 (overlay-put ov 'priority 1001)
817 (push ov el-search-hl-other-overlays)
818 (goto-char this-match-end)
819 (when (>= (point) to) (setq done t))))))))
820
821 (defun el-search-hl-other-matches (pattern)
822 "Highlight all matches visible in the selected window."
823 (el-search--hl-other-matches-1 pattern
824 (save-excursion
825 (goto-char (window-start))
826 (beginning-of-defun-raw)
827 (point))
828 (window-end))
829 (add-hook 'window-scroll-functions #'el-search--after-scroll t t))
830
831 (defun el-search--after-scroll (_win start)
832 (el-search--hl-other-matches-1 el-search-current-pattern
833 (save-excursion
834 (goto-char start)
835 (beginning-of-defun-raw)
836 (point))
837 (window-end nil t)))
838
839 (defun el-search-hl-remove ()
840 (when (overlayp el-search-hl-overlay)
841 (delete-overlay el-search-hl-overlay))
842 (remove-hook 'window-scroll-functions #'el-search--after-scroll t)
843 (mapc #'delete-overlay el-search-hl-other-overlays)
844 (setq el-search-hl-other-overlays '()))
845
846 (defun el-search-hl-post-command-fun ()
847 (unless (or el-search-keep-hl
848 (eq this-command 'el-search-query-replace)
849 (eq this-command 'el-search-pattern))
850 (el-search-hl-remove)
851 (remove-hook 'post-command-hook 'el-search-hl-post-command-fun t)))
852
853
854 ;;;; Core functions
855
856 (defvar el-search-history '()
857 "List of input strings.")
858
859 (defvar el-search-success nil)
860 (defvar el-search-current-pattern nil)
861
862 ;;;###autoload
863 (defun el-search-pattern (pattern)
864 "Start new or resume last elisp search.
865
866 Search current buffer for expressions that are matched by `pcase'
867 PATTERN. Use `read' to transform buffer contents into
868 expressions.
869
870
871 Additional `pcase' pattern types to be used with this command can
872 be defined with `el-search-defpattern'.
873
874 The following additional pattern types are currently defined:"
875 (interactive (list (if (and (eq this-command last-command)
876 el-search-success)
877 el-search-current-pattern
878 (let ((pattern
879 (el-search--read-pattern "Find pcase pattern: "
880 (car el-search-history)
881 t)))
882 ;; A very common mistake: input "foo" instead of "'foo"
883 (when (and (symbolp pattern)
884 (not (eq pattern '_))
885 (or (not (boundp pattern))
886 (not (eq (symbol-value pattern) pattern))))
887 (error "Please don't forget the quote when searching for a symbol"))
888 (el-search--wrap-pattern pattern)))))
889 (if (not (called-interactively-p 'any))
890 (el-search--search-pattern pattern)
891 (setq this-command 'el-search-pattern) ;in case we come from isearch
892 (setq el-search-current-pattern pattern)
893 (let ((opoint (point)))
894 (when (and (eq this-command last-command) el-search-success)
895 (el-search--skip-expression nil t))
896 (setq el-search-success nil)
897 (when (condition-case nil
898 (el-search--search-pattern pattern)
899 (end-of-buffer (message "No match")
900 (goto-char opoint)
901 (el-search-hl-remove)
902 (ding)
903 nil))
904 (setq el-search-success t)
905 (el-search-hl-sexp)
906 (unless (eq this-command last-command)
907 (el-search-hl-other-matches pattern))))))
908
909 (defvar el-search-search-and-replace-help-string
910 "\
911 y Replace this match and move to the next.
912 SPC or n Skip this match and move to the next.
913 r Replace this match but don't move.
914 ! Replace all remaining matches automatically.
915 q Quit. To resume, use e.g. `repeat-complex-command'.
916 ? Show this help.
917 s Toggle splicing mode. When splicing mode is
918 on (default off), the replacement expression must
919 evaluate to a list, and the result is spliced into the
920 buffer, instead of just inserted.
921
922 Hit any key to proceed."
923 "Help string for ? in `el-search-query-replace'.")
924
925 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
926 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
927 (el-search-keep-hl t) (opoint (point))
928 (get-replacement (el-search--matcher pattern replacement)))
929 (unwind-protect
930 (while (and (not done) (el-search--search-pattern pattern t))
931 (setq opoint (point))
932 (unless replace-all
933 (el-search-hl-sexp)
934 (unless (eq this-command last-command)
935 (el-search-hl-other-matches pattern)))
936 (let* ((read-mapping (el-search--create-read-map))
937 (region (list (point) (el-search--end-of-sexp)))
938 (substring (apply #'buffer-substring-no-properties region))
939 (expr (read substring))
940 (replaced-this nil)
941 (new-expr (funcall get-replacement expr))
942 (get-replacement-string
943 (lambda () (if (and splice (not (listp new-expr)))
944 (error "Expression to splice in is an atom")
945 (el-search--repair-replacement-layout
946 (if splice
947 (mapconcat #'el-search--print new-expr " ")
948 (el-search--print new-expr))
949 (append mapping read-mapping)))))
950 (to-insert (funcall get-replacement-string))
951 (do-replace (lambda ()
952 (atomic-change-group
953 (apply #'delete-region region)
954 (let ((inhibit-message t)
955 (opoint (point)))
956 (insert to-insert)
957 (indent-region opoint (point))
958 (el-search-hl-sexp (list opoint (point)))
959 (goto-char opoint)))
960 (cl-incf nbr-replaced)
961 (setq replaced-this t))))
962 (if replace-all
963 (funcall do-replace)
964 (while (not (pcase (if replaced-this
965 (read-char-choice "[SPC ! q] (? for help)"
966 '(?\ ?! ?q ?n ??))
967 (read-char-choice
968 (concat "Replace this occurrence"
969 (if (or (string-match-p "\n" to-insert)
970 (< 40 (length to-insert)))
971 "" (format " with `%s'" to-insert))
972 "? "
973 (if splice "{splice} " "")
974 "[y SPC r ! s q] (? for help)" )
975 '(?y ?n ?r ?\ ?! ?q ?s ??)))
976 (?r (funcall do-replace)
977 nil)
978 (?y (funcall do-replace)
979 t)
980 ((or ?\ ?n)
981 (unless replaced-this (cl-incf nbr-skipped))
982 t)
983 (?! (unless replaced-this
984 (funcall do-replace))
985 (setq replace-all t)
986 t)
987 (?s (cl-callf not splice)
988 (setq to-insert (funcall get-replacement-string))
989 nil)
990 (?q (setq done t)
991 t)
992 (?? (ignore (read-char el-search-search-and-replace-help-string))
993 nil)))))
994 (unless (or done (eobp)) (el-search--skip-expression nil t)))))
995 (el-search-hl-remove)
996 (goto-char opoint)
997 (message "Replaced %d matches%s"
998 nbr-replaced
999 (if (zerop nbr-skipped) ""
1000 (format " (%d skipped)" nbr-skipped)))))
1001
1002 (defun el-search-query-replace-read-args ()
1003 (barf-if-buffer-read-only)
1004 (let* ((from (el-search--read-pattern "Replace from: "))
1005 (to (let ((el-search--initial-mb-contents nil))
1006 (el-search--read-pattern "Replace with result of evaluation of: " from))))
1007 (list (el-search--wrap-pattern (read from)) (read to)
1008 (with-temp-buffer
1009 (insert to)
1010 (el-search--create-read-map 1)))))
1011
1012 ;;;###autoload
1013 (defun el-search-query-replace (from to &optional mapping)
1014 "Replace some occurrences of FROM pattern with evaluated TO."
1015 (interactive (el-search-query-replace-read-args))
1016 (setq this-command 'el-search-query-replace) ;in case we come from isearch
1017 (setq el-search-current-pattern from)
1018 (barf-if-buffer-read-only)
1019 (el-search-search-and-replace-pattern from to mapping))
1020
1021 (defun el-search--take-over-from-isearch (&optional goto-left-end)
1022 (let ((other-end (and goto-left-end isearch-other-end))
1023 (input isearch-string))
1024 (isearch-exit)
1025 (when (and other-end (< other-end (point)))
1026 (goto-char other-end))
1027 input))
1028
1029 ;;;###autoload
1030 (defun el-search-search-from-isearch ()
1031 ;; FIXME: an interesting alternative would be to really integrate it
1032 ;; with Isearch, using `isearch-search-fun-function'.
1033 ;; Alas, this is not trivial if we want to transfer our optimizations.
1034 (interactive)
1035 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
1036 ;; use `call-interactively' so we get recorded in `extended-command-history'
1037 (call-interactively #'el-search-pattern)))
1038
1039 ;;;###autoload
1040 (defun el-search-replace-from-isearch ()
1041 (interactive)
1042 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch t))))
1043 (call-interactively #'el-search-query-replace)))
1044
1045
1046
1047 (provide 'el-search)
1048 ;;; el-search.el ends here