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