]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/el-search/el-search.el
Rename two functions
[gnu-emacs-elpa] / packages / el-search / el-search.el
index 7d625f8ea4fa21c4509c3be756ad661eb909e3b3..db57a5e12b17c23b041d7df700876f3608260cd2 100644 (file)
@@ -7,7 +7,7 @@
 ;; Created: 29 Jul 2015
 ;; Keywords: lisp
 ;; Compatibility: GNU Emacs 25
-;; Version: 0.1.2
+;; Version: 0.1.3
 ;; Package-Requires: ((emacs "25"))
 
 
@@ -65,7 +65,7 @@
 ;;   `(defvar ,_)
 ;;
 ;; you search for all defvar forms that don't specify an init value.
-;; 
+;;
 ;; The following will search for defvar forms with a docstring whose
 ;; first line is longer than 70 characters:
 ;;
 ;; with point at the beginning of the currently tested expression.
 ;;
 ;;
-;; Example 3:
-;;
-;; I can be useful to use (guard EXP) patterns for side effects (note:
-;; this only works when applied to the top level expression).
-;;
-;; The following pattern will search for symbols defined in any
-;; library whose name starts with "cl".  As a side effect, it prints
-;; the current line number, whether we have a macro or a function, and
-;; the defining file in the echo area for each match:
-;;
-;;   (and (pred symbolp)
-;;        (let file (symbol-file exp))
-;;        (guard file)
-;;        (let lib-name (file-name-sans-extension
-;;                       (file-name-nondirectory file)))
-;;        (guard (string-match-p "^cl" lib-name))
-;;        (or (and (pred macrop)    (let type "macro "))
-;;            (and (pred functionp) (let type "function "))
-;;            (let type ""))
-;;        (guard (message "Line %d: %s`%S' (from \"%s\")"
-;;                        (line-number-at-pos)
-;;                        type
-;;                        exp
-;;                        lib-name)))
-;;
-;; `message' never returns nil, so the last `guard' always "matches".
-;;
-;;
 ;; Convenience
 ;; ===========
 ;;
 ;; so that you can always refer to the whole currently tested
 ;; expression via the variable `exp'.
 ;;
-;; Example 4:
+;;
+;; Example 3:
 ;;
 ;; If you want to search a buffer for symbols that are defined in
 ;; "cl-lib", you can use this pattern
 ;;    (define-key isearch-mode-map [(control ?%)] #'el-search-replace-from-isearch)
 ;;
 ;; The bindings in `isearch-mode-map' let you conveniently switch to
-;; elisp searching from isearch.
+;; "el-search" searching from isearch.
 ;;
 ;;
 ;; Bugs, Known Limitations
 ;;
 ;; the comment will be lost.
 ;;
+;; FIXME: when we have resumable sessions, pause and warn about this case.
+;;
 ;;
 ;;  Acknowledgments
 ;;  ===============
 ;;
 ;; TODO:
 ;;
-;; - When replacing like (progn A B C) -> A B C, the layout of the
-;; whole "group" A B C as a unit is lost.  Instead of restoring layout
-;; as we do now (via "read mappings"), we could just make a backup of
-;; the original expression as a string, and use our search machinery
-;; to find occurrences in the replacement recursively.
-;;
-;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
-;;
-;; - highlight matches around point in a timer
-;;
 ;; - implement backward searching
 ;;
+;; - Make `el-search-pattern' accept an &optional limit, at least for
+;;   the non-interactive use case?
+;;
 ;; - improve docstrings
 ;;
 ;; - handle more reader syntaxes, e.g. #n, #n#
   :group 'lisp)
 
 (defcustom el-search-this-expression-identifier 'exp
-  "Name of the identifier referring to the current expression.
-The default value is `exp'.  You can use this name in the search
-prompt to refer to the value of the currently tested expression."
+  "Identifier referring to the current expression in pattern input.
+When entering a PATTERN in an interactive \"el-search\" command,
+the pattern actually used will be
+
+    `(and ,el-search-this-expression-identifier ,pattern)
+
+The default value is `exp'."
   :type 'symbol)
 
 (defface el-search-match '((((background dark)) (:background "#0000A0"))
-                          (t                   (:background "DarkSlateGray1")))
+                          (t                   (:background "DarkSlateGray3")))
   "Face for highlighting the current match.")
 
+(defface el-search-other-match '((((background dark)) (:background "#202060"))
+                                 (t                   (:background "DarkSlateGray1")))
+  "Face for highlighting the other matches.")
+
+(defcustom el-search-smart-case-fold-search t
+  "Whether to use smart case folding in pattern matching.
+When an \"el-search\" pattern involves regexp matching (like for
+\"string\" or \"source\") and this option is non-nil,
+case-fold-search will be temporarily bound to t if the according
+regexp contains any upper case letter, and nil else.  This is
+done independently for every single matching operation.
+
+If nil, the value of `case-fold-search' is decisive."
+  :type 'boolean)
+
+(defcustom el-search-use-sloppy-strings nil
+  "Whether to allow the usage of \"sloppy strings\".
+When this option is turned on, for faster typing you are allowed
+to specify symbols instead of strings as arguments to an
+\"el-search\" pattern type that would otherwise accept only
+strings, and their names will be used as input (with other words,
+this spares you to type the string delimiters in many cases).
+
+For example,
+
+  \(source ^cl\)
+
+is then equivalent to
+
+  \(source \"^cl\"\)
+
+When this option is off, the first form would just signal an
+error."
+  :type 'boolean)
+
 
 ;;;; Helpers
 
+(defun el-search--smart-string-match-p (regexp string)
+  "`string-match-p' taking `el-search-smart-case-fold-search' into account."
+  (let ((case-fold-search (if el-search-smart-case-fold-search
+                              (not (let ((case-fold-search nil))
+                                     (string-match-p "[[:upper:]]" regexp)))
+                            case-fold-search)))
+    (string-match-p regexp string)))
+
 (defun el-search--print (expr)
   (let ((print-quoted t)
         (print-length nil)
@@ -294,44 +309,46 @@ prompt to refer to the value of the currently tested expression."
     map)
   "Map for reading input with `el-search-read-expression'.")
 
+(defun el-search--setup-minibuffer ()
+  (emacs-lisp-mode)
+  (use-local-map el-search-read-expression-map)
+  (setq font-lock-mode t)
+  (funcall font-lock-function 1)
+  (backward-sexp)
+  (indent-sexp)
+  (goto-char (point-max))
+  (when-let ((this-sexp (with-current-buffer (window-buffer (minibuffer-selected-window))
+                          (thing-at-point 'sexp))))
+    (let ((more-defaults (list (concat "'" this-sexp))))
+      (setq-local minibuffer-default-add-function
+                  (lambda () (if (listp minibuffer-default)
+                            (append minibuffer-default more-defaults)
+                          (cons minibuffer-default more-defaults)))))))
+
 ;; $$$$$FIXME: this should be in Emacs!  There is only a helper `read--expression'.
 (defun el-search-read-expression (prompt &optional initial-contents hist default read)
   "Read expression for `my-eval-expression'."
-  (minibuffer-with-setup-hook
-      (lambda ()
-        (emacs-lisp-mode)
-        (use-local-map el-search-read-expression-map)
-        (setq font-lock-mode t)
-        (funcall font-lock-function 1)
-        (backward-sexp)
-        (indent-sexp)
-        (goto-char (point-max)))
+  (minibuffer-with-setup-hook #'el-search--setup-minibuffer
     (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
                           (or hist 'read-expression-history) default)))
 
 (defvar el-search--initial-mb-contents nil)
 
 (defun el-search--read-pattern (prompt &optional default read)
-  (let ((this-sexp (sexp-at-point)))
-    (minibuffer-with-setup-hook
-        (lambda ()
-          (when this-sexp
-            (let ((more-defaults (list (concat "'" (el-search--print this-sexp)))))
-              (setq-local minibuffer-default-add-function
-                          (lambda () (if (listp minibuffer-default)
-                                    (append minibuffer-default more-defaults)
-                                  (cons minibuffer-default more-defaults)))))))
-      (el-search-read-expression
-       prompt el-search--initial-mb-contents 'el-search-history default read))))
+  (let ((input (el-search-read-expression
+                prompt el-search--initial-mb-contents 'el-search-history default read)))
+    (if (or read (not (string= input ""))) input (car el-search-history))))
 
 (defun el-search--end-of-sexp ()
   ;;Point must be at sexp beginning
   (or (scan-sexps (point) 1) (point-max)))
 
 (defun el-search--ensure-sexp-start ()
-  "Move point to the beginning of the next sexp if necessary.
-Don't move if already at beginning of a sexp.
-Point must not be inside a string or comment."
+  "Move point to the next sexp beginning position.
+Don't move if already at beginning of a sexp.  Point must not be
+inside a string or comment.  `read' the expression at that point
+and return it."
+  ;; This doesn't catch end-of-buffer to keep the return value non-ambiguous
   (let ((not-done t) res)
     (while not-done
       (let ((stop-here nil)
@@ -373,7 +390,7 @@ Point must not be inside a string or comment."
       (mapc
        (pcase-lambda (`(,symbol . ,fun))
          (when-let ((doc (documentation fun)))
-           (insert "\n\n-- ")
+           (insert "\n\n\n-- ")
            (setq doc (help-fns--signature symbol doc fun fun nil))
            (insert "\n" (or doc "Not documented."))))
        (reverse el-search--pcase-macros))
@@ -390,6 +407,17 @@ of the definitions is limited to \"el-search\"."
   `(setf (alist-get ',name el-search--pcase-macros)
          (lambda ,args ,@body)))
 
+(defun el-search--macroexpand-1 (pattern)
+  "Expand \"el-search\" PATTERN.
+This is like `pcase--macroexpand', but expands only patterns
+defined with `el-search-defpattern' and performs only one
+expansion step.
+
+Return PATTERN if this pattern type was not defined with
+`el-search-defpattern'."
+  (if-let ((expander (alist-get (car-safe pattern) el-search--pcase-macros)))
+      (apply expander (cdr pattern))
+    pattern))
 
 (defmacro el-search--with-additional-pcase-macros (&rest body)
   `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
@@ -399,14 +427,15 @@ of the definitions is limited to \"el-search\"."
 
 (defun el-search--matcher (pattern &rest body)
   (eval ;use `eval' to allow for user defined pattern types at run time
-   `(el-search--with-additional-pcase-macros
-     (let ((byte-compile-debug t) ;make undefined pattern types raise an error
-           (warning-suppress-log-types '((bytecomp)))
-           (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
-       (byte-compile (lambda (expression)
-                       (pcase expression
-                         (,pattern ,@(or body (list t)))
-                         (_        nil))))))))
+   (let ((expression (make-symbol "expression")))
+     `(el-search--with-additional-pcase-macros
+       (let ((byte-compile-debug t) ;make undefined pattern types raise an error
+             (warning-suppress-log-types '((bytecomp)))
+             (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
+         (byte-compile (lambda (,expression)
+                         (pcase ,expression
+                           (,pattern ,@(or body (list t)))
+                           (_        nil)))))))))
 
 (defun el-search--match-p (matcher expression)
   (funcall matcher expression))
@@ -436,13 +465,8 @@ of the definitions is limited to \"el-search\"."
     (goto-char (match-end 0)))
    (t (forward-char))))
 
-(defun el-search--search-pattern (pattern &optional noerror)
-  "Search elisp buffer with `pcase' PATTERN.
-Set point to the beginning of the occurrence found and return
-point.  Optional second argument, if non-nil, means if fail just
-return nil (no error)."
-  
-  (let ((matcher (el-search--matcher pattern)) (match-beg nil) (opoint (point)) current-expr)
+(defun el-search--search-pattern-1 (matcher &optional noerror)
+  (let ((match-beg nil) (opoint (point)) current-expr)
 
     ;; when inside a string or comment, move past it
     (let ((syntax-here (syntax-ppss)))
@@ -468,56 +492,93 @@ return nil (no error)."
         (if noerror nil (signal 'end-of-buffer nil)))
     match-beg))
 
-(defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
-  ;; In current buffer, for any expression start between POS and BOUND
-  ;; or (point-max), in order, call two argument function DO-FUN with
-  ;; the current sexp string and the ending position of the current
-  ;; sexp.  When done, with RET-FUN given, call it with no args and
-  ;; return the result; else, return nil.
-  (save-excursion
-    (goto-char pos)
-    (condition-case nil
-        (while (< (point) (or bound (point-max)))
-          (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
-                 (this-sexp-string (buffer-substring-no-properties (point) this-sexp-end)))
-            (funcall do-fun this-sexp-string this-sexp-end)
-            (el-search--skip-expression (read this-sexp-string))
-            (el-search--ensure-sexp-start)))
-      (end-of-buffer))
-    (when ret-fun (funcall ret-fun))))
-
-(defun el-search--create-read-map (&optional pos)
-  (let ((mapping '()))
-    (el-search--do-subsexps
-     (or pos (point))
-     (lambda (sexp _) (push (cons (read sexp) sexp) mapping))
-     (lambda () (nreverse mapping))
-     (save-excursion (thing-at-point--end-of-sexp) (point)))))
-
-(defun el-search--repair-replacement-layout (printed mapping)
-  (with-temp-buffer
-    (insert printed)
-    (el-search--do-subsexps
-     (point-min)
-     (lambda (sexp sexp-end)
-       (when-let ((old (cdr (assoc (read sexp) mapping))))
-         (delete-region (point) sexp-end)
-         (when (string-match-p "\n" old)
-           (unless (looking-back "^[[:space:]]*" (line-beginning-position))
-             (insert "\n"))
-           (unless (looking-at "[[:space:]\)]*$")
-             (insert "\n")
-             (backward-char)))
-         (save-excursion (insert old))))
-     (lambda () (buffer-substring (point-min) (point-max))))))
+(defun el-search--search-pattern (pattern &optional noerror)
+  "Search elisp buffer with `pcase' PATTERN.
+Set point to the beginning of the occurrence found and return
+point.  Optional second argument, if non-nil, means if fail just
+return nil (no error)."
+  (el-search--search-pattern-1 (el-search--matcher pattern) noerror))
+
+(defun el-search--format-replacement (replacement original replace-expr-input splice)
+  ;; Return a printed representation of REPLACEMENT.  Try to reuse the
+  ;; layout of subexpressions shared with the original (replaced)
+  ;; expression and the replace expression.
+  (if (and splice (not (listp replacement)))
+      (error "Expression to splice in is an atom")
+    (let ((orig-buffer (generate-new-buffer "orig-expr")))
+      (with-current-buffer orig-buffer
+        (emacs-lisp-mode)
+        (insert original)
+        (when replace-expr-input (insert "\n\n" replace-expr-input)))
+      (unwind-protect
+          (with-temp-buffer
+            (emacs-lisp-mode)
+            (insert (if splice
+                        (mapconcat #'el-search--print replacement " ")
+                      (el-search--print replacement)))
+            (goto-char 1)
+            (let (start this-sexp end orig-match-start orig-match-end done)
+              (while (and (< (point) (point-max))
+                          (condition-case nil
+                              (progn
+                                (setq start (point)
+                                      this-sexp (read (current-buffer))
+                                      end   (point))
+                                t)
+                            (end-of-buffer nil)))
+                (setq done nil orig-match-start nil)
+                (with-current-buffer orig-buffer
+                  (goto-char 1)
+                  (if (el-search--search-pattern `',this-sexp t)
+                      (setq orig-match-start (point)
+                            orig-match-end (progn (forward-sexp) (point)))
+                    (setq done t)))
+                ;; find out whether we have a sequence of equal expressions
+                (while (and (not done)
+                            (condition-case nil
+                                (progn (setq this-sexp (read (current-buffer))) t)
+                              ((invalid-read-syntax end-of-buffer end-of-file) nil)))
+                  (if (with-current-buffer orig-buffer
+                        (condition-case nil
+                            (if (not (equal this-sexp (read (current-buffer))))
+                                nil
+                              (setq orig-match-end (point))
+                              t)
+                          ((invalid-read-syntax end-of-buffer end-of-file) nil)))
+                      (setq end (point))
+                    (setq done t)))
+                (if orig-match-start
+                    (let ((match (with-current-buffer orig-buffer
+                                   (buffer-substring-no-properties orig-match-start
+                                                                   orig-match-end))))
+                      (delete-region start end)
+                      (goto-char start)
+                      (when (string-match-p "\n" match)
+                        (unless (looking-back "^[[:space:]\(]*" (line-beginning-position))
+                          (insert "\n"))
+                        (unless (looking-at "[[:space:]\)]*$")
+                          (insert "\n")
+                          (backward-char)))
+                      (insert match))
+                  (goto-char start)
+                  (el-search--skip-expression nil t))
+                (condition-case nil
+                    (el-search--ensure-sexp-start)
+                  (end-of-buffer (goto-char (point-max))))))
+            (delete-trailing-whitespace (point-min) (point-max)) ;FIXME: this should not be necessary
+            (let ((result (buffer-substring (point-min) (point-max))))
+              (if (equal replacement (read result))
+                  result
+                (error "Error in `el-search--format-replacement' - please make a bug report"))))
+        (kill-buffer orig-buffer)))))
 
 (defun el-search--check-pattern-args (type args predicate &optional message)
   "Check whether all ARGS fulfill PREDICATE.
-Raise an error if not.  TYPE and optional argument MESSAGE are
-used to construct the error message."
+Raise an error if not.  The string arguments TYPE and optional
+MESSAGE are used to construct the error message."
   (mapc (lambda (arg)
           (unless (funcall predicate arg)
-            (error (concat "Pattern `%S': "
+            (error (concat "Pattern `%s': "
                            (or message (format "argument doesn't fulfill %S" predicate))
                            ": %S")
                    type arg)))
@@ -579,27 +640,62 @@ matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 6)."
                           (,'\, ,(car more-patterns)))))))
        (t `(append ,pattern (append ,@more-patterns)))))))
 
+(defun el-search--stringish-p (thing)
+  (or (stringp thing) (and el-search-use-sloppy-strings (symbolp thing))))
+
 (el-search-defpattern string (&rest regexps)
   "Matches any string that is matched by all REGEXPS."
-  (el-search--check-pattern-args 'string regexps #'stringp)
-  (let ((string (make-symbol "string"))
-        (regexp (make-symbol "regexp")))
-    `(and (pred stringp)
-          (pred (lambda (,string)
-                  (cl-every
-                   (lambda (,regexp) (string-match-p ,regexp ,string))
-                   (list ,@regexps)))))))
+  (el-search--check-pattern-args "string" regexps #'el-search--stringish-p
+                                 "Argument not a string")
+  `(and (pred stringp)
+        ,@(mapcar (lambda (thing) `(pred (el-search--smart-string-match-p
+                                     ,(if (symbolp thing) (symbol-name thing) thing))))
+                  regexps)))
 
 (el-search-defpattern symbol (&rest regexps)
   "Matches any symbol whose name is matched by all REGEXPS."
-  (el-search--check-pattern-args 'symbol regexps #'stringp)
+  (el-search--check-pattern-args "symbol" regexps #'el-search--stringish-p
+                                 "Argument not a string")
   `(and (pred symbolp)
         (app symbol-name (string ,@regexps))))
 
+(defun el-search--contains-p (matcher exp)
+  "Return non-nil when tree EXP contains a match for MATCHER.
+Recurse on all types of sequences.  In the positive case the
+return value is (t elt), where ELT is a matching element found in
+EXP."
+  (if (el-search--match-p matcher exp)
+      (list t exp)
+    (and (sequencep exp)
+         (let ((try-match (apply-partially #'el-search--contains-p matcher)))
+           (if (consp exp)
+               (or (funcall try-match (car exp))
+                   (funcall try-match (cdr exp)))
+             (cl-some try-match exp))))))
+
+(el-search-defpattern contains (&rest patterns)
+  "Matches trees that contain a match for all PATTERNs.
+Searches any tree of sequences recursively for matches.  Objects
+of any kind matched by all PATTERNs are also matched.
+
+  Example: (contains (string \"H\") 17) matches ((\"Hallo\") x (5 [1 17]))"
+  (cond
+   ((null patterns) '_)
+   ((null (cdr patterns))
+    (let ((pattern (car patterns)))
+      `(app ,(apply-partially #'el-search--contains-p (el-search--matcher pattern))
+            (,'\` (t (,'\, ,pattern))))))
+   (t `(and ,@(mapcar (lambda (pattern) `(contains ,pattern)) patterns)))))
+
+(el-search-defpattern not (pattern)
+  "Matches any object that is not matched by PATTERN."
+  `(app ,(apply-partially #'el-search--match-p (el-search--matcher pattern))
+        (pred not)))
+
 (defun el-search--match-symbol-file (regexp symbol)
   (when-let ((symbol-file (and (symbolp symbol)
                                (symbol-file symbol))))
-    (string-match-p
+    (el-search--smart-string-match-p
      (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
      (file-name-sans-extension (file-name-nondirectory symbol-file)))))
 
@@ -616,8 +712,9 @@ REGEXP can also be a symbol, in which case
   (concat \"^\" (symbol-name regexp) \"$\")
 
 is used as regular expression."
-  (el-search--check-pattern-args 'source (list regexp) #'stringp)
-  `(pred (el-search--match-symbol-file ,regexp)))
+  (el-search--check-pattern-args "source" (list regexp) #'el-search--stringish-p
+                                 "Argument not a string")
+  `(pred (el-search--match-symbol-file ,(if (symbolp regexp) (symbol-name regexp) regexp))))
 
 (defun el-search--match-key-sequence (keys expr)
   (when-let ((expr-keys (pcase expr
@@ -647,15 +744,72 @@ matches any of these expressions:
     [(control ?s)]"
   (when (eq (car-safe key-sequence) 'kbd)
     (setq key-sequence (kbd (cadr key-sequence))))
-  (el-search--check-pattern-args 'keys (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
+  (el-search--check-pattern-args "keys" (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
                                  "argument not a string or vector")
   `(pred (el-search--match-key-sequence ,key-sequence)))
 
+(defun el-search--s (expr)
+  (cond
+   ((symbolp expr) `(or (symbol ,(symbol-name expr))
+                        (,'\` (,'quote    (,'\, (symbol ,(symbol-name expr)))))
+                        (,'\` (,'function (,'\, (symbol ,(symbol-name expr)))))))
+   ((stringp expr) `(string ,expr))
+   (t expr)))
+
+(el-search-defpattern l (&rest lpats)
+  "Alternative pattern type for matching lists.
+Match any list with subsequent elements matched by all LPATS in
+order.
+
+The idea is to be able to search for pieces of code (i.e. lists)
+with very brief input by using a specialized syntax.
+
+An LPAT can take the following forms:
+
+SYMBOL  Matches any symbol S matched by SYMBOL's name interpreted
+        as a regexp.  Matches also 'S and #'S for any such S.
+STRING  Matches any string matched by STRING interpreted as a
+        regexp
+_       Matches any list element
+__      Matches any number of list elements (including zero)
+^       Matches zero elements, but only at the beginning of a list
+$       Matches zero elements, but only at the end of a list
+PAT     Anything else is interpreted as a normal pcase pattern, and
+        matches one list element matched by it
+
+^ is only valid as the first, $ as the last of the LPATS.
+
+Example: To match defuns that contain \"hl\" in their name and
+have at least one mandatory, but also optional arguments, you
+could use this pattern:
+
+    (l ^ 'defun hl (l _ &optional))"
+  (let ((match-start nil) (match-end nil))
+    (when (eq (car-safe lpats) '^)
+      (setq match-start t)
+      (cl-callf cdr lpats))
+    (when (eq (car-safe (last lpats)) '$)
+      (setq match-end t)
+      (cl-callf butlast lpats 1))
+    `(append ,@(if match-start '() '(_))
+             ,@(mapcar
+                (lambda (elt)
+                  (pcase elt
+                    ('__ '_)
+                    ('_ '`(,_))
+                    ('_? '(or '() `(,_))) ;FIXME: useful - document? or should we provide a (? PAT)
+                                          ;thing?
+                    (_ `(,'\` ((,'\, ,(el-search--s elt)))))))
+                lpats)
+             ,@(if match-end '() '(_)))))
+
 
 ;;;; Highlighting
 
 (defvar-local el-search-hl-overlay nil)
 
+(defvar-local el-search-hl-other-overlays '())
+
 (defvar el-search-keep-hl nil)
 
 (defun el-search-hl-sexp (&optional bounds)
@@ -664,12 +818,55 @@ matches any of these expressions:
     (if (overlayp el-search-hl-overlay)
         (apply #'move-overlay el-search-hl-overlay bounds)
       (overlay-put (setq el-search-hl-overlay (apply #'make-overlay bounds))
-                   'face 'el-search-match)))
+                   'face 'el-search-match))
+    (overlay-put el-search-hl-overlay 'priority 1002))
   (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
 
+(defun el-search--hl-other-matches-1 (pattern from to)
+  (mapc #'delete-overlay el-search-hl-other-overlays)
+  (setq el-search-hl-other-overlays '())
+  (let ((matcher (el-search--matcher pattern))
+        this-match-beg this-match-end
+        (done nil))
+    (save-excursion
+      (goto-char from)
+      (while (not done)
+        (setq this-match-beg (el-search--search-pattern-1 matcher t))
+        (if (not this-match-beg)
+            (setq done t)
+          (goto-char this-match-beg)
+          (setq this-match-end (el-search--end-of-sexp))
+          (let ((ov (make-overlay this-match-beg this-match-end)))
+            (overlay-put ov 'face 'el-search-other-match)
+            (overlay-put ov 'priority 1001)
+            (push ov el-search-hl-other-overlays)
+            (goto-char this-match-end)
+            (when (>= (point) to) (setq done t))))))))
+
+(defun el-search-hl-other-matches (pattern)
+  "Highlight all matches visible in the selected window."
+  (el-search--hl-other-matches-1 pattern
+                                 (save-excursion
+                                   (goto-char (window-start))
+                                   (beginning-of-defun-raw)
+                                   (point))
+                                 (window-end))
+  (add-hook 'window-scroll-functions #'el-search--after-scroll t t))
+
+(defun el-search--after-scroll (_win start)
+  (el-search--hl-other-matches-1 el-search-current-pattern
+                                 (save-excursion
+                                   (goto-char start)
+                                   (beginning-of-defun-raw)
+                                   (point))
+                                 (window-end nil t)))
+
 (defun el-search-hl-remove ()
   (when (overlayp el-search-hl-overlay)
-    (delete-overlay el-search-hl-overlay)))
+    (delete-overlay el-search-hl-overlay))
+  (remove-hook 'window-scroll-functions #'el-search--after-scroll t)
+  (mapc #'delete-overlay el-search-hl-other-overlays)
+  (setq el-search-hl-other-overlays '()))
 
 (defun el-search-hl-post-command-fun ()
   (unless (or el-search-keep-hl
@@ -699,7 +896,7 @@ expressions.
 Additional `pcase' pattern types to be used with this command can
 be defined with `el-search-defpattern'.
 
-The following additional pattern types are currently defined:\n"
+The following additional pattern types are currently defined:"
   (interactive (list (if (and (eq this-command last-command)
                               el-search-success)
                          el-search-current-pattern
@@ -714,22 +911,25 @@ The following additional pattern types are currently defined:\n"
                                         (not (eq (symbol-value pattern) pattern))))
                            (error "Please don't forget the quote when searching for a symbol"))
                          (el-search--wrap-pattern pattern)))))
-  (setq this-command 'el-search-pattern) ;in case we come from isearch
-  (setq el-search-current-pattern pattern)
-  (let ((opoint (point)))
-    (when (and (eq this-command last-command) el-search-success)
-      (el-search--skip-expression nil t))
-    (setq el-search-success nil)
-    (message "%s" (substitute-command-keys "Type \\[el-search-pattern] to repeat"))
-    (when (condition-case nil
-              (el-search--search-pattern pattern)
-            (end-of-buffer (message "No match")
-                           (goto-char opoint)
-                           (el-search-hl-remove)
-                           (ding)
-                           nil))
-      (setq el-search-success t)
-      (el-search-hl-sexp))))
+  (if (not (called-interactively-p 'any))
+      (el-search--search-pattern pattern)
+    (setq this-command 'el-search-pattern) ;in case we come from isearch
+    (setq el-search-current-pattern pattern)
+    (let ((opoint (point)))
+      (when (and (eq this-command last-command) el-search-success)
+        (el-search--skip-expression nil t))
+      (setq el-search-success nil)
+      (when (condition-case nil
+                (el-search--search-pattern pattern)
+              (end-of-buffer (message "No match")
+                             (goto-char opoint)
+                             (el-search-hl-remove)
+                             (ding)
+                             nil))
+        (setq el-search-success t)
+        (el-search-hl-sexp)
+        (unless (eq this-command last-command)
+          (el-search-hl-other-matches pattern))))))
 
 (defvar el-search-search-and-replace-help-string
   "\
@@ -747,29 +947,36 @@ s         Toggle splicing mode.  When splicing mode is
 Hit any key to proceed."
   "Help string for ? in `el-search-query-replace'.")
 
-(defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
+(defun el-search--search-and-replace-pattern (pattern replacement &optional splice to-input-string)
   (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
         (el-search-keep-hl t) (opoint (point))
-        (get-replacement (el-search--matcher pattern replacement)))
+        (get-replacement (el-search--matcher pattern replacement))
+        (skip-matches-in-replacement 'ask))
     (unwind-protect
         (while (and (not done) (el-search--search-pattern pattern t))
           (setq opoint (point))
-          (unless replace-all (el-search-hl-sexp))
-          (let* ((read-mapping (el-search--create-read-map))
-                 (region (list (point) (el-search--end-of-sexp)))
+          (unless replace-all
+            (el-search-hl-sexp)
+            (unless (eq this-command last-command)
+              (el-search-hl-other-matches pattern)))
+          (let* ((region (list (point) (el-search--end-of-sexp)))
                  (substring (apply #'buffer-substring-no-properties region))
                  (expr      (read substring))
                  (replaced-this nil)
                  (new-expr  (funcall get-replacement expr))
                  (get-replacement-string
-                  (lambda () (if (and splice (not (listp new-expr)))
-                            (error "Expression to splice in is an atom")
-                          (el-search--repair-replacement-layout
-                           (if splice
-                               (mapconcat #'el-search--print new-expr " ")
-                             (el-search--print new-expr))
-                           (append mapping read-mapping)))))
+                  (lambda () (el-search--format-replacement new-expr substring to-input-string splice)))
                  (to-insert (funcall get-replacement-string))
+                 (replacement-contains-another-match
+                  (with-temp-buffer
+                    (emacs-lisp-mode)
+                    (insert to-insert)
+                    (goto-char 1)
+                    (el-search--skip-expression new-expr)
+                    (condition-case nil
+                        (progn (el-search--ensure-sexp-start)
+                               (el-search--search-pattern pattern t))
+                      (end-of-buffer nil))))
                  (do-replace (lambda ()
                                (atomic-change-group
                                  (apply #'delete-region region)
@@ -785,7 +992,7 @@ Hit any key to proceed."
                 (funcall do-replace)
               (while (not (pcase (if replaced-this
                                      (read-char-choice "[SPC ! q]  (? for help)"
-                                                       '(?\ ?! ?q ?n ??))
+                                                       '(?\ ?! ?q ?\C-g ?n ??))
                                    (read-char-choice
                                     (concat "Replace this occurrence"
                                             (if (or (string-match-p "\n" to-insert)
@@ -794,7 +1001,7 @@ Hit any key to proceed."
                                             "? "
                                             (if splice "{splice} " "")
                                             "[y SPC r ! s q]  (? for help)" )
-                                    '(?y ?n ?r ?\ ?! ?q ?s ??)))
+                                    '(?y ?n ?r ?\ ?! ?q ?\C-g ?s ??)))
                             (?r (funcall do-replace)
                                 nil)
                             (?y (funcall do-replace)
@@ -809,11 +1016,31 @@ Hit any key to proceed."
                             (?s (cl-callf not splice)
                                 (setq to-insert (funcall get-replacement-string))
                                 nil)
-                            (?q (setq done t)
-                                t)
+                            ((or ?q ?\C-g)
+                             (setq done t)
+                             t)
                             (?? (ignore (read-char el-search-search-and-replace-help-string))
                                 nil)))))
-            (unless (or done (eobp)) (el-search--skip-expression nil t)))))
+            (unless (or done (eobp))
+              (cond
+               ((not (and replaced-this replacement-contains-another-match))
+                (el-search--skip-expression nil t))
+               ((eq skip-matches-in-replacement 'ask)
+                (if (setq skip-matches-in-replacement
+                          (yes-or-no-p "Match in replacement - always skip? "))
+                    (forward-sexp)
+                  (el-search--skip-expression nil t)
+                  (when replace-all
+                    (setq replace-all nil)
+                    (message "Falling back to interactive mode")
+                    (sit-for 3.))))
+               (skip-matches-in-replacement (forward-sexp))
+               (t
+                (el-search--skip-expression nil t)
+                (message "Replacement contains another match%s"
+                         (if replace-all " - falling back to interactive mode" ""))
+                (setq replace-all nil)
+                (sit-for 3.)))))))
     (el-search-hl-remove)
     (goto-char opoint)
     (message "Replaced %d matches%s"
@@ -821,27 +1048,31 @@ Hit any key to proceed."
              (if (zerop nbr-skipped)  ""
                (format "   (%d skipped)" nbr-skipped)))))
 
-(defun el-search-query-replace-read-args ()
+(defun el-search-query-replace--read-args ()
   (barf-if-buffer-read-only)
-  (let* ((from (el-search--read-pattern "Replace from: "))
+  (let* ((from (el-search--read-pattern "Query replace pattern: "))
          (to   (let ((el-search--initial-mb-contents nil))
                  (el-search--read-pattern "Replace with result of evaluation of: " from))))
-    (list (el-search--wrap-pattern (read from)) (read to)
-          (with-temp-buffer
-            (insert to)
-            (el-search--create-read-map 1)))))
+    (list (el-search--wrap-pattern (read from)) (read to) to)))
 
 ;;;###autoload
-(defun el-search-query-replace (from to &optional mapping)
-  "Replace some occurrences of FROM pattern with evaluated TO."
-  (interactive (el-search-query-replace-read-args))
+(defun el-search-query-replace (from-pattern to-expr &optional textual-to)
+  "Replace some matches of \"el-search\" pattern FROM-PATTERN.
+
+TO-EXPR is an Elisp expression that is evaluated repeatedly for
+each match with bindings created in FROM-PATTERN in effect to
+produce a replacement expression.
+
+As each match is found, the user must type a character saying
+what to do with it.  For directions, type ? at that time."
+  (interactive (el-search-query-replace--read-args))
   (setq this-command 'el-search-query-replace) ;in case we come from isearch
-  (setq el-search-current-pattern from)
+  (setq el-search-current-pattern from-pattern)
   (barf-if-buffer-read-only)
-  (el-search-search-and-replace-pattern from to mapping))
+  (el-search--search-and-replace-pattern from-pattern to-expr nil textual-to))
 
-(defun el-search--take-over-from-isearch ()
-  (let ((other-end isearch-other-end)
+(defun el-search--take-over-from-isearch (&optional goto-left-end)
+  (let ((other-end (and goto-left-end isearch-other-end))
         (input isearch-string))
     (isearch-exit)
     (when (and other-end (< other-end (point)))
@@ -861,7 +1092,7 @@ Hit any key to proceed."
 ;;;###autoload
 (defun el-search-replace-from-isearch ()
   (interactive)
-  (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
+  (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch t))))
     (call-interactively #'el-search-query-replace)))