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