]> code.delx.au - gnu-emacs-elpa/blob - packages/el-search/el-search.el
remove example using `message' in a guard
[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.2
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 ;; - highlight matches around point in a timer
206 ;;
207 ;; - implement backward searching
208 ;;
209 ;; - improve docstrings
210 ;;
211 ;; - handle more reader syntaxes, e.g. #n, #n#
212 ;;
213 ;; - Implement sessions; add multi-file support based on iterators. A
214 ;; file list is read in (or the user can specify an iterator as a
215 ;; variable). The state in the current buffer is just (buffer
216 ;; . marker). Or should this be abstracted into an own lib? Could
217 ;; be named "files-session" or so.
218
219
220
221 ;;; Code:
222
223 ;;;; Requirements
224
225 (eval-when-compile
226 (require 'subr-x))
227
228 (require 'cl-lib)
229 (require 'elisp-mode)
230 (require 'thingatpt)
231 (require 'help-fns) ;el-search--make-docstring
232
233
234 ;;;; Configuration stuff
235
236 (defgroup el-search nil
237 "Expression based search and replace for `emacs-lisp-mode'."
238 :group 'lisp)
239
240 (defcustom el-search-this-expression-identifier 'exp
241 "Name of the identifier referring to the current expression.
242 The default value is `exp'. You can use this name in the search
243 prompt to refer to the value of the currently tested expression."
244 :type 'symbol)
245
246 (defface el-search-match '((((background dark)) (:background "#0000A0"))
247 (t (:background "DarkSlateGray1")))
248 "Face for highlighting the current match.")
249
250
251 ;;;; Helpers
252
253 (defun el-search--print (expr)
254 (let ((print-quoted t)
255 (print-length nil)
256 (print-level nil))
257 (prin1-to-string expr)))
258
259 (defvar el-search-read-expression-map
260 (let ((map (make-sparse-keymap)))
261 (set-keymap-parent map read-expression-map)
262 (define-key map [(control ?g)] #'abort-recursive-edit)
263 (define-key map [up] nil)
264 (define-key map [down] nil)
265 (define-key map [(control meta backspace)] #'backward-kill-sexp)
266 (define-key map [(control ?S)] #'exit-minibuffer)
267 map)
268 "Map for reading input with `el-search-read-expression'.")
269
270 ;; $$$$$FIXME: this should be in Emacs! There is only a helper `read--expression'.
271 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
272 "Read expression for `my-eval-expression'."
273 (minibuffer-with-setup-hook
274 (lambda ()
275 (emacs-lisp-mode)
276 (use-local-map el-search-read-expression-map)
277 (setq font-lock-mode t)
278 (funcall font-lock-function 1)
279 (backward-sexp)
280 (indent-sexp)
281 (goto-char (point-max)))
282 (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
283 (or hist 'read-expression-history) default)))
284
285 (defvar el-search--initial-mb-contents nil)
286
287 (defun el-search--read-pattern (prompt &optional default read)
288 (let ((this-sexp (sexp-at-point)))
289 (minibuffer-with-setup-hook
290 (lambda ()
291 (when this-sexp
292 (let ((more-defaults (list (concat "'" (el-search--print this-sexp)))))
293 (setq-local minibuffer-default-add-function
294 (lambda () (if (listp minibuffer-default)
295 (append minibuffer-default more-defaults)
296 (cons minibuffer-default more-defaults)))))))
297 (el-search-read-expression
298 prompt el-search--initial-mb-contents 'el-search-history default read))))
299
300 (defun el-search--end-of-sexp ()
301 ;;Point must be at sexp beginning
302 (or (scan-sexps (point) 1) (point-max)))
303
304 (defun el-search--ensure-sexp-start ()
305 "Move point to the beginning of the next sexp if necessary.
306 Don't move if already at beginning of a sexp.
307 Point must not be inside a string or comment."
308 (let ((not-done t) res)
309 (while not-done
310 (let ((stop-here nil)
311 (looking-at-from-back (lambda (regexp n)
312 (save-excursion
313 (backward-char n)
314 (looking-at regexp)))))
315 (while (not stop-here)
316 (cond
317 ((eobp) (signal 'end-of-buffer nil))
318 ((looking-at (rx (and (* space) ";"))) (forward-line))
319 ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
320
321 ;; FIXME: can the rest be done more generically?
322 ((and (looking-at (rx (or (syntax symbol) (syntax word))))
323 (not (looking-at "\\_<"))
324 (not (funcall looking-at-from-back ",@" 2)))
325 (forward-symbol 1))
326 ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
327 (and (looking-at "@") (funcall looking-at-from-back "," 1)))
328 (forward-char))
329 (t (setq stop-here t)))))
330 (condition-case nil
331 (progn
332 (setq res (save-excursion (read (current-buffer))))
333 (setq not-done nil))
334 (error (forward-char))))
335 res))
336
337 (defvar el-search--pcase-macros '()
338 "List of additional \"el-search\" pcase macros.")
339
340 (defun el-search--make-docstring ()
341 ;; code mainly from `pcase--make-docstring'
342 (let* ((main (documentation (symbol-function 'el-search-pattern) 'raw))
343 (ud (help-split-fundoc main 'pcase)))
344 (with-temp-buffer
345 (insert (or (cdr ud) main))
346 (mapc
347 (pcase-lambda (`(,symbol . ,fun))
348 (when-let ((doc (documentation fun)))
349 (insert "\n\n-- ")
350 (setq doc (help-fns--signature symbol doc fun fun nil))
351 (insert "\n" (or doc "Not documented."))))
352 (reverse el-search--pcase-macros))
353 (let ((combined-doc (buffer-string)))
354 (if ud (help-add-fundoc-usage combined-doc (car ud)) combined-doc)))))
355
356 (put 'el-search-pattern 'function-documentation '(el-search--make-docstring))
357
358 (defmacro el-search-defpattern (name args &rest body)
359 "Like `pcase-defmacro', but limited to el-search patterns.
360 The semantics is exactly that of `pcase-defmacro', but the scope
361 of the definitions is limited to \"el-search\"."
362 (declare (indent 2) (debug defun))
363 `(setf (alist-get ',name el-search--pcase-macros)
364 (lambda ,args ,@body)))
365
366
367 (defmacro el-search--with-additional-pcase-macros (&rest body)
368 `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
369 `((get ',symbol 'pcase-macroexpander) #',fun))
370 el-search--pcase-macros)
371 ,@body))
372
373 (defun el-search--matcher (pattern &rest body)
374 (eval ;use `eval' to allow for user defined pattern types at run time
375 `(el-search--with-additional-pcase-macros
376 (let ((byte-compile-debug t) ;make undefined pattern types raise an error
377 (warning-suppress-log-types '((bytecomp)))
378 (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
379 (byte-compile (lambda (expression)
380 (pcase expression
381 (,pattern ,@(or body (list t)))
382 (_ nil))))))))
383
384 (defun el-search--match-p (matcher expression)
385 (funcall matcher expression))
386
387 (defun el-search--wrap-pattern (pattern)
388 `(and ,el-search-this-expression-identifier ,pattern))
389
390 (defun el-search--skip-expression (expression &optional read)
391 ;; Move forward at least one character. Don't move into a string or
392 ;; comment. Don't move further than the beginning of the next sexp.
393 ;; Try to move as far as possible. Point must be at the beginning
394 ;; of an expression.
395 ;; If there are positions where `read' would succeed, but that do
396 ;; not represent a valid sexp start, move past them (e.g. when
397 ;; before "#'" move past both characters).
398 ;;
399 ;; EXPRESSION must be the (read) expression at point, but when READ
400 ;; is non-nil, ignore the first argument and read the expression at
401 ;; point instead.
402 (when read (setq expression (save-excursion (read (current-buffer)))))
403 (cond
404 ((or (null expression)
405 (equal [] expression)
406 (not (or (listp expression) (vectorp expression))))
407 (goto-char (el-search--end-of-sexp)))
408 ((looking-at (rx (or ",@" "," "#'" "'")))
409 (goto-char (match-end 0)))
410 (t (forward-char))))
411
412 (defun el-search--search-pattern (pattern &optional noerror)
413 "Search elisp buffer with `pcase' PATTERN.
414 Set point to the beginning of the occurrence found and return
415 point. Optional second argument, if non-nil, means if fail just
416 return nil (no error)."
417
418 (let ((matcher (el-search--matcher pattern)) (match-beg nil) (opoint (point)) current-expr)
419
420 ;; when inside a string or comment, move past it
421 (let ((syntax-here (syntax-ppss)))
422 (when (nth 3 syntax-here) ;inside a string
423 (goto-char (nth 8 syntax-here))
424 (forward-sexp))
425 (when (nth 4 syntax-here) ;inside a comment
426 (forward-line 1)
427 (while (and (not (eobp)) (looking-at (rx (and (* space) ";"))))
428 (forward-line 1))))
429
430 (if (catch 'no-match
431 (while (not match-beg)
432 (condition-case nil
433 (setq current-expr (el-search--ensure-sexp-start))
434 (end-of-buffer
435 (goto-char opoint)
436 (throw 'no-match t)))
437 (if (el-search--match-p matcher current-expr)
438 (setq match-beg (point)
439 opoint (point))
440 (el-search--skip-expression current-expr))))
441 (if noerror nil (signal 'end-of-buffer nil)))
442 match-beg))
443
444 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
445 ;; In current buffer, for any expression start between POS and BOUND
446 ;; or (point-max), in order, call two argument function DO-FUN with
447 ;; the current sexp string and the ending position of the current
448 ;; sexp. When done, with RET-FUN given, call it with no args and
449 ;; return the result; else, return nil.
450 (save-excursion
451 (goto-char pos)
452 (condition-case nil
453 (while (< (point) (or bound (point-max)))
454 (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
455 (this-sexp-string (buffer-substring-no-properties (point) this-sexp-end)))
456 (funcall do-fun this-sexp-string this-sexp-end)
457 (el-search--skip-expression (read this-sexp-string))
458 (el-search--ensure-sexp-start)))
459 (end-of-buffer))
460 (when ret-fun (funcall ret-fun))))
461
462 (defun el-search--create-read-map (&optional pos)
463 (let ((mapping '()))
464 (el-search--do-subsexps
465 (or pos (point))
466 (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
467 (lambda () (nreverse mapping))
468 (save-excursion (thing-at-point--end-of-sexp) (point)))))
469
470 (defun el-search--repair-replacement-layout (printed mapping)
471 (with-temp-buffer
472 (insert printed)
473 (el-search--do-subsexps
474 (point-min)
475 (lambda (sexp sexp-end)
476 (when-let ((old (cdr (assoc (read sexp) mapping))))
477 (delete-region (point) sexp-end)
478 (when (string-match-p "\n" old)
479 (unless (looking-back "^[[:space:]]*" (line-beginning-position))
480 (insert "\n"))
481 (unless (looking-at "[[:space:]\)]*$")
482 (insert "\n")
483 (backward-char)))
484 (save-excursion (insert old))))
485 (lambda () (buffer-substring (point-min) (point-max))))))
486
487 (defun el-search--check-pattern-args (type args predicate &optional message)
488 "Check whether all ARGS fulfill PREDICATE.
489 Raise an error if not. TYPE and optional argument MESSAGE are
490 used to construct the error message."
491 (mapc (lambda (arg)
492 (unless (funcall predicate arg)
493 (error (concat "Pattern `%S': "
494 (or message (format "argument doesn't fulfill %S" predicate))
495 ": %S")
496 type arg)))
497 args))
498
499
500 ;;;; Additional pattern type definitions
501
502 (defun el-search--split (matcher1 matcher2 list)
503 "Helper for the append pattern type.
504
505 When a splitting of LIST into two lists L1, L2 exist so that Li
506 is matched by MATCHERi, return (L1 L2) for such Li, else return
507 nil."
508 (let ((try-match (lambda (list1 list2)
509 (when (and (el-search--match-p matcher1 list1)
510 (el-search--match-p matcher2 list2))
511 (list list1 list2))))
512 (list1 list) (list2 '()) (match nil))
513 ;; don't use recursion, this could hit `max-lisp-eval-depth'
514 (while (and (not (setq match (funcall try-match list1 list2)))
515 (consp list1))
516 (let ((last-list1 (last list1)))
517 (if-let ((cdr-last-list1 (cdr last-list1)))
518 ;; list1 is a dotted list. Then list2 must be empty.
519 (progn (setcdr last-list1 nil)
520 (setq list2 cdr-last-list1))
521 (setq list1 (butlast list1 1)
522 list2 (cons (car last-list1) list2)))))
523 match))
524
525 (el-search-defpattern append (&rest patterns)
526 "Matches any list factorable into lists matched by PATTERNS in order.
527
528 PATTERNS is a list of patterns P1..Pn. Match any list L for that
529 lists L1..Ln exist that are matched by P1..Pn in order and L is
530 equal to the concatenation of L1..Ln. Ln is allowed to be no
531 list.
532
533 When different ways of matching are possible, it is unspecified
534 which one is chosen.
535
536 Example: the pattern
537
538 (append '(1 2 3) x (app car-safe 7))
539
540 matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 6)."
541 (if (null patterns)
542 '(pred null)
543 (pcase-let ((`(,pattern . ,more-patterns) patterns))
544 (cond
545 ((null more-patterns) pattern)
546 ((null (cdr more-patterns))
547 `(and (pred listp)
548 (app ,(apply-partially #'el-search--split
549 (el-search--matcher pattern)
550 (el-search--matcher (car more-patterns)))
551 (,'\` ((,'\, ,pattern)
552 (,'\, ,(car more-patterns)))))))
553 (t `(append ,pattern (append ,@more-patterns)))))))
554
555 (el-search-defpattern string (&rest regexps)
556 "Matches any string that is matched by all REGEXPS."
557 (el-search--check-pattern-args 'string regexps #'stringp)
558 (let ((string (make-symbol "string"))
559 (regexp (make-symbol "regexp")))
560 `(and (pred stringp)
561 (pred (lambda (,string)
562 (cl-every
563 (lambda (,regexp) (string-match-p ,regexp ,string))
564 (list ,@regexps)))))))
565
566 (el-search-defpattern symbol (&rest regexps)
567 "Matches any symbol whose name is matched by all REGEXPS."
568 (el-search--check-pattern-args 'symbol regexps #'stringp)
569 `(and (pred symbolp)
570 (app symbol-name (string ,@regexps))))
571
572 (defun el-search--match-symbol-file (regexp symbol)
573 (when-let ((symbol-file (and (symbolp symbol)
574 (symbol-file symbol))))
575 (string-match-p
576 (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
577 (file-name-sans-extension (file-name-nondirectory symbol-file)))))
578
579 (el-search-defpattern source (regexp)
580 "Matches any symbol whose `symbol-file' is matched by REGEXP.
581
582 This pattern matches when the object is a symbol for that
583 `symbol-file' returns a (non-nil) FILE-NAME that fulfills
584 (string-match-p REGEXP (file-name-sans-extension
585 (file-name-nondirectory FILENAME)))
586
587 REGEXP can also be a symbol, in which case
588
589 (concat \"^\" (symbol-name regexp) \"$\")
590
591 is used as regular expression."
592 (el-search--check-pattern-args 'source (list regexp) #'stringp)
593 `(pred (el-search--match-symbol-file ,regexp)))
594
595 (defun el-search--match-key-sequence (keys expr)
596 (when-let ((expr-keys (pcase expr
597 ((or (pred stringp) (pred vectorp)) expr)
598 (`(kbd ,(and (pred stringp) string)) (ignore-errors (kbd string))))))
599 (apply #'equal
600 (mapcar (lambda (keys) (ignore-errors (key-description keys)))
601 (list keys expr-keys)))))
602
603 (el-search-defpattern keys (key-sequence)
604 "Matches descriptions of the KEY-SEQUENCE.
605 KEY-SEQUENCE is a string or vector representing a key sequence,
606 or an expression of the form (kbd STRING).
607
608 Match any description of the same key sequence in any of these
609 formats.
610
611 Example: the pattern
612
613 (keys (kbd \"C-s\"))
614
615 matches any of these expressions:
616
617 \"\\C-s\"
618 \"\C-s\"
619 (kbd \"C-s\")
620 [(control ?s)]"
621 (when (eq (car-safe key-sequence) 'kbd)
622 (setq key-sequence (kbd (cadr key-sequence))))
623 (el-search--check-pattern-args 'keys (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
624 "argument not a string or vector")
625 `(pred (el-search--match-key-sequence ,key-sequence)))
626
627
628 ;;;; Highlighting
629
630 (defvar-local el-search-hl-overlay nil)
631
632 (defvar el-search-keep-hl nil)
633
634 (defun el-search-hl-sexp (&optional bounds)
635 (let ((bounds (or bounds
636 (list (point) (el-search--end-of-sexp)))))
637 (if (overlayp el-search-hl-overlay)
638 (apply #'move-overlay el-search-hl-overlay bounds)
639 (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
640 'face 'el-search-match)))
641 (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
642
643 (defun el-search-hl-remove ()
644 (when (overlayp el-search-hl-overlay)
645 (delete-overlay el-search-hl-overlay)))
646
647 (defun el-search-hl-post-command-fun ()
648 (unless (or el-search-keep-hl
649 (eq this-command 'el-search-query-replace)
650 (eq this-command 'el-search-pattern))
651 (el-search-hl-remove)
652 (remove-hook 'post-command-hook 'el-search-hl-post-command-fun t)))
653
654
655 ;;;; Core functions
656
657 (defvar el-search-history '()
658 "List of input strings.")
659
660 (defvar el-search-success nil)
661 (defvar el-search-current-pattern nil)
662
663 ;;;###autoload
664 (defun el-search-pattern (pattern)
665 "Start new or resume last elisp search.
666
667 Search current buffer for expressions that are matched by `pcase'
668 PATTERN. Use `read' to transform buffer contents into
669 expressions.
670
671
672 Additional `pcase' pattern types to be used with this command can
673 be defined with `el-search-defpattern'.
674
675 The following additional pattern types are currently defined:\n"
676 (interactive (list (if (and (eq this-command last-command)
677 el-search-success)
678 el-search-current-pattern
679 (let ((pattern
680 (el-search--read-pattern "Find pcase pattern: "
681 (car el-search-history)
682 t)))
683 ;; A very common mistake: input "foo" instead of "'foo"
684 (when (and (symbolp pattern)
685 (not (eq pattern '_))
686 (or (not (boundp pattern))
687 (not (eq (symbol-value pattern) pattern))))
688 (error "Please don't forget the quote when searching for a symbol"))
689 (el-search--wrap-pattern pattern)))))
690 (setq this-command 'el-search-pattern) ;in case we come from isearch
691 (setq el-search-current-pattern pattern)
692 (let ((opoint (point)))
693 (when (and (eq this-command last-command) el-search-success)
694 (el-search--skip-expression nil t))
695 (setq el-search-success nil)
696 (message "%s" (substitute-command-keys "Type \\[el-search-pattern] to repeat"))
697 (when (condition-case nil
698 (el-search--search-pattern pattern)
699 (end-of-buffer (message "No match")
700 (goto-char opoint)
701 (el-search-hl-remove)
702 (ding)
703 nil))
704 (setq el-search-success t)
705 (el-search-hl-sexp))))
706
707 (defvar el-search-search-and-replace-help-string
708 "\
709 y Replace this match and move to the next.
710 SPC or n Skip this match and move to the next.
711 r Replace this match but don't move.
712 ! Replace all remaining matches automatically.
713 q Quit. To resume, use e.g. `repeat-complex-command'.
714 ? Show this help.
715 s Toggle splicing mode. When splicing mode is
716 on (default off), the replacement expression must
717 evaluate to a list, and the result is spliced into the
718 buffer, instead of just inserted.
719
720 Hit any key to proceed."
721 "Help string for ? in `el-search-query-replace'.")
722
723 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
724 (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
725 (el-search-keep-hl t) (opoint (point))
726 (get-replacement (el-search--matcher pattern replacement)))
727 (unwind-protect
728 (while (and (not done) (el-search--search-pattern pattern t))
729 (setq opoint (point))
730 (unless replace-all (el-search-hl-sexp))
731 (let* ((read-mapping (el-search--create-read-map))
732 (region (list (point) (el-search--end-of-sexp)))
733 (substring (apply #'buffer-substring-no-properties region))
734 (expr (read substring))
735 (replaced-this nil)
736 (new-expr (funcall get-replacement expr))
737 (get-replacement-string
738 (lambda () (if (and splice (not (listp new-expr)))
739 (error "Expression to splice in is an atom")
740 (el-search--repair-replacement-layout
741 (if splice
742 (mapconcat #'el-search--print new-expr " ")
743 (el-search--print new-expr))
744 (append mapping read-mapping)))))
745 (to-insert (funcall get-replacement-string))
746 (do-replace (lambda ()
747 (atomic-change-group
748 (apply #'delete-region region)
749 (let ((inhibit-message t)
750 (opoint (point)))
751 (insert to-insert)
752 (indent-region opoint (point))
753 (el-search-hl-sexp (list opoint (point)))
754 (goto-char opoint)))
755 (cl-incf nbr-replaced)
756 (setq replaced-this t))))
757 (if replace-all
758 (funcall do-replace)
759 (while (not (pcase (if replaced-this
760 (read-char-choice "[SPC ! q] (? for help)"
761 '(?\ ?! ?q ?n ??))
762 (read-char-choice
763 (concat "Replace this occurrence"
764 (if (or (string-match-p "\n" to-insert)
765 (< 40 (length to-insert)))
766 "" (format " with `%s'" to-insert))
767 "? "
768 (if splice "{splice} " "")
769 "[y SPC r ! s q] (? for help)" )
770 '(?y ?n ?r ?\ ?! ?q ?s ??)))
771 (?r (funcall do-replace)
772 nil)
773 (?y (funcall do-replace)
774 t)
775 ((or ?\ ?n)
776 (unless replaced-this (cl-incf nbr-skipped))
777 t)
778 (?! (unless replaced-this
779 (funcall do-replace))
780 (setq replace-all t)
781 t)
782 (?s (cl-callf not splice)
783 (setq to-insert (funcall get-replacement-string))
784 nil)
785 (?q (setq done t)
786 t)
787 (?? (ignore (read-char el-search-search-and-replace-help-string))
788 nil)))))
789 (unless (or done (eobp)) (el-search--skip-expression nil t)))))
790 (el-search-hl-remove)
791 (goto-char opoint)
792 (message "Replaced %d matches%s"
793 nbr-replaced
794 (if (zerop nbr-skipped) ""
795 (format " (%d skipped)" nbr-skipped)))))
796
797 (defun el-search-query-replace-read-args ()
798 (barf-if-buffer-read-only)
799 (let* ((from (el-search--read-pattern "Replace from: "))
800 (to (let ((el-search--initial-mb-contents nil))
801 (el-search--read-pattern "Replace with result of evaluation of: " from))))
802 (list (el-search--wrap-pattern (read from)) (read to)
803 (with-temp-buffer
804 (insert to)
805 (el-search--create-read-map 1)))))
806
807 ;;;###autoload
808 (defun el-search-query-replace (from to &optional mapping)
809 "Replace some occurrences of FROM pattern with evaluated TO."
810 (interactive (el-search-query-replace-read-args))
811 (setq this-command 'el-search-query-replace) ;in case we come from isearch
812 (setq el-search-current-pattern from)
813 (barf-if-buffer-read-only)
814 (el-search-search-and-replace-pattern from to mapping))
815
816 (defun el-search--take-over-from-isearch ()
817 (let ((other-end isearch-other-end)
818 (input isearch-string))
819 (isearch-exit)
820 (when (and other-end (< other-end (point)))
821 (goto-char other-end))
822 input))
823
824 ;;;###autoload
825 (defun el-search-search-from-isearch ()
826 ;; FIXME: an interesting alternative would be to really integrate it
827 ;; with Isearch, using `isearch-search-fun-function'.
828 ;; Alas, this is not trivial if we want to transfer our optimizations.
829 (interactive)
830 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
831 ;; use `call-interactively' so we get recorded in `extended-command-history'
832 (call-interactively #'el-search-pattern)))
833
834 ;;;###autoload
835 (defun el-search-replace-from-isearch ()
836 (interactive)
837 (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
838 (call-interactively #'el-search-query-replace)))
839
840
841
842 (provide 'el-search)
843 ;;; el-search.el ends here