emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/el-search c086990 129/332: Extend the heuristic matchin


From: Stefan Monnier
Subject: [elpa] externals/el-search c086990 129/332: Extend the heuristic matching approach; complete review
Date: Tue, 1 Dec 2020 15:48:27 -0500 (EST)

branch: externals/el-search
commit c0869909cf3d85ff54fc4003d2c0032e029900c7
Author: Michael Heerdegen <michael_heerdegen@web.de>
Commit: Michael Heerdegen <michael_heerdegen@web.de>

    Extend the heuristic matching approach; complete review
---
 el-search-x.el |  97 ++++---
 el-search.el   | 820 +++++++++++++++++++++++++++++++++------------------------
 2 files changed, 547 insertions(+), 370 deletions(-)

diff --git a/el-search-x.el b/el-search-x.el
index 9a98c87..b01a93c 100644
--- a/el-search-x.el
+++ b/el-search-x.el
@@ -33,7 +33,9 @@
 
 ;;; Code:
 
-(eval-when-compile (require 'subr-x))
+(eval-when-compile
+  (require 'subr-x)
+  (require 'thunk))
 (require 'el-search)
 
 
@@ -97,6 +99,8 @@ matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 
6)."
      `(or (symbol ,symbol-name)
           `',(symbol  ,symbol-name)
           `#',(symbol ,symbol-name)))
+    (`',(and (pred symbolp) symbol)
+     `(or ',symbol '',symbol '#',symbol))
     ((pred stringp) `(string ,expr))
     (_ expr)))
 
@@ -112,6 +116,8 @@ 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.
+'SYMBOL Matches SYMBOL, 'SYMBOL and #'SYMBOL (so it's like the above
+        without regexp matching).
 STRING  Matches any string matched by STRING interpreted as a
         regexp
 _       Matches any list element
@@ -131,15 +137,16 @@ could use this pattern:
   (declare
    (heuristic-matcher
     (lambda (&rest lpats)
-      (lambda (atoms)
+      (lambda (file-name-or-buffer atom-thunk)
         (cl-every
          (lambda (lpat)
            (pcase lpat
              ((or '__ '_ '_? '^ '$) t)
              ((pred symbolp)
-              (funcall (el-search-heuristic-matcher `(symbol ,(symbol-name 
lpat))) atoms))
+              (funcall (el-search-heuristic-matcher `(symbol ,(symbol-name 
lpat)))
+                       file-name-or-buffer atom-thunk))
              (_ (funcall (el-search-heuristic-matcher 
(el-search--transform-nontrivial-lpat lpat))
-                         atoms))))
+                         file-name-or-buffer atom-thunk))))
          lpats)))))
   (let ((match-start nil) (match-end nil))
     (when (eq (car-safe lpats) '^)
@@ -165,16 +172,18 @@ could use this pattern:
 
 (defvar diff-hl-reference-revision)
 (declare-function diff-hl-changes "diff-hl")
-(declare-function vc-git-command "vc-git")
 (defvar-local el-search--cached-changes nil)
 
 
 (defcustom el-search-change-revision-transformer-function nil
   "Transformer function for the REVISION argument of `change' and `changed'.
 
-When specified, this function is called on the REVISION argument
-of `change' and `changed' before passing it to git.  The default
-value is nil."
+When specified, this function is called with two arguments: the
+REVISION argument passed to `change' or `changed', and the
+current file name, and the returned value is used instead of
+REVISION.
+
+The default value is nil."
   :group 'el-search
   :type '(choice (const :tag "No transformer" nil)
                  (function :tag "User specified function")))
@@ -195,28 +204,25 @@ Use variable `el-search--cached-changes' for caching."
       (widen)
       (save-excursion
         (let ((diff-hl-reference-revision
-               (funcall (or el-search-change-revision-transformer-function 
#'identity) revision))
+               (if el-search-change-revision-transformer-function
+                   (funcall el-search-change-revision-transformer-function
+                            revision
+                            buffer-file-name)
+                 revision))
               (current-line-nbr 1) change-beg)
           (goto-char 1)
           (cdr (setq el-search--cached-changes
                      (cons (list revision (visited-file-modtime))
-                           (and
-                            (let ((file-name buffer-file-name))
-                              (with-temp-buffer
-                                (vc-git-command
-                                 (current-buffer) 128 file-name
-                                 "log" "--ignore-missing" "-1"
-                                 diff-hl-reference-revision "--" file-name)
-                                (> (point-max) 1)))
-                            (delq nil (mapcar (pcase-lambda (`(,start-line 
,nbr-lines ,kind))
-                                                (if (eq kind 'delete) nil
-                                                  (forward-line (- start-line 
current-line-nbr))
-                                                  (setq change-beg (point))
-                                                  (forward-line (1- nbr-lines))
-                                                  (setq current-line-nbr (+ 
start-line nbr-lines -1))
-                                                  (cons (copy-marker 
change-beg)
-                                                        (copy-marker 
(line-end-position)))))
-                                              (ignore-errors 
(diff-hl-changes)))))))))))))
+                           (and (el-search--file-changed-p buffer-file-name 
diff-hl-reference-revision)
+                                (delq nil (mapcar (pcase-lambda (`(,start-line 
,nbr-lines ,kind))
+                                                    (if (eq kind 'delete) nil
+                                                      (forward-line (- 
start-line current-line-nbr))
+                                                      (setq change-beg (point))
+                                                      (forward-line (1- 
nbr-lines))
+                                                      (setq current-line-nbr 
(+ start-line nbr-lines -1))
+                                                      (cons (copy-marker 
change-beg)
+                                                            (copy-marker 
(line-end-position)))))
+                                                  (ignore-errors 
(diff-hl-changes)))))))))))))
 
 (defun el-search--change-p (posn &optional revision)
   ;; Non-nil when sexp after POSN is part of a change
@@ -241,22 +247,47 @@ Use variable `el-search--cached-changes' for caching."
       (and changes
            (< (caar changes) (scan-sexps posn 1))))))
 
+(defun el-search--file-changed-p (file rev)
+  (cl-callf file-truename file)
+  (when-let ((backend (vc-backend file)))
+    (ignore-errors
+      (let ((default-directory (file-name-directory file)))
+        (and
+         (with-temp-buffer
+           (= 1 (vc-call-backend backend 'diff (list file) nil rev 
(current-buffer))))
+         (with-temp-buffer
+           (= 1 (vc-call-backend backend 'diff (list file) rev nil 
(current-buffer)))))))))
+
+(defun el-search-change--heuristic-matcher (&optional revision)
+  (lambda (file-name-or-buffer _)
+    (require 'vc)
+    (when-let ((file (if (stringp file-name-or-buffer)
+                         file-name-or-buffer
+                       (buffer-file-name file-name-or-buffer))))
+      (let ((default-directory (file-name-directory file)))
+        (el-search--file-changed-p
+         file
+         (funcall el-search-change-revision-transformer-function
+                  (or revision "HEAD") file))))))
+
 (el-search-defpattern change (&optional revision)
   "Matches the object if its text is part of a file change.
 
 Requires library \"diff-hl\".  REVISION defaults to the file's
-repository's HEAD commit and is a git revision string.  Customize
+repository's HEAD commit and is a revision string.  Customize
 `el-search-change-revision-transformer-function' to control how
 REVISION is interpreted."
+  (declare (heuristic-matcher #'el-search-change--heuristic-matcher))
   `(guard (el-search--change-p (point) ,(or revision "HEAD"))))
 
 (el-search-defpattern changed (&optional revision)
   "Matches the object if its text contains a file change.
 
 Requires library \"diff-hl\".  REVISION defaults to the file's
-repository's HEAD commit and is a git revision string.  Customize
+repository's HEAD commit and is a revision string.  Customize
 `el-search-change-revision-transformer-function' to control how
 REVISION is interpreted."
+  (declare (heuristic-matcher #'el-search-change--heuristic-matcher))
   `(guard (el-search--changed-p (point) ,(or revision "HEAD"))))
 
 
@@ -295,9 +326,9 @@ matches any of these expressions:
   `(pred (el-search--match-key-sequence ,key-sequence)))
 
 
-;;;; `but-not-parent' and `top-level'
+;;;; `outermost' and `top-level'
 
-(el-search-defpattern but-not-parent (pattern &optional not-pattern)
+(el-search-defpattern outermost (pattern &optional not-pattern)
     "Matches when PATTERN matches but the parent sexp does not.
 For toplevel expressions, this is equivalent to PATTERN.
 
@@ -308,10 +339,10 @@ NOT-PATTERN.
 
 This pattern is useful to match only the outermost expression
 when subexpressions would match recursively.  For
-example, (but-not-parent _) matches only top-level expressions.
+example, (outermost _) matches only top-level expressions.
 Another example: For the `change' pattern, any subexpression of a
 match is typically also an according change.  Wrapping the
-`change' pattern into `but-not-parent' prevents el-search from
+`change' pattern into `outermost' prevents el-search from
 descending into any found expression - only the outermost
 expression matching the `change' pattern will be matched."
     `(and ,pattern
@@ -326,7 +357,7 @@ expression matching the `change' pattern will be matched."
 
 (el-search-defpattern top-level ()
   "Matches any toplevel expression."
-  '(but-not-parent _))
+  '(outermost _))
 
 
 ;;; Patterns for stylistic rewriting
diff --git a/el-search.el b/el-search.el
index 2dbd022..e544be6 100644
--- a/el-search.el
+++ b/el-search.el
@@ -7,7 +7,7 @@
 ;; Created: 29 Jul 2015
 ;; Keywords: lisp
 ;; Compatibility: GNU Emacs 25
-;; Version: 1.1.2
+;; Version: 1.2
 ;; Package-Requires: ((emacs "25") (stream "2.2.3"))
 
 
@@ -32,18 +32,20 @@
 ;; Suggested key bindings
 ;; ======================
 ;;
-;; You can eval the following key definitions to try things out while
-;; reading this introduction.  These are the bindings I use
-;; personally:
+;; After loading this file, you can eval the following key definitions
+;; to try things out while reading this introduction.  These are the
+;; bindings I use personally:
 ;;
 ;;   (define-key emacs-lisp-mode-map [(control ?S)] #'el-search-pattern)
 ;;   (define-key emacs-lisp-mode-map [(control ?%)] #'el-search-query-replace)
 ;;   (define-key global-map          [(control ?J)] 
#'el-search-jump-to-search-head)
 ;;   (define-key global-map          [(control ?N)] 
#'el-search-continue-in-next-buffer)
+;;   (define-key global-map          [(control ?O)] #'el-search-overview)
 ;;
 ;;   (define-key el-search-read-expression-map [(control ?S)] 
#'exit-minibuffer)
 ;;
 ;;   (define-key isearch-mode-map [(control ?S)] 
#'el-search-search-from-isearch)
+;;   (define-key isearch-mode-map [(control ?%)] 
#'el-search-replace-from-isearch)
 ;;
 ;;   (with-eval-after-load 'dired
 ;;     (define-key dired-mode-map [(control ?S)] 
#'el-search-dired-marked-files))
@@ -52,8 +54,8 @@
 ;; official bindings that fit better into the Emacs ecosystem, please
 ;; mail me).
 ;;
-;; The binding in `isearch-mode-map' lets you switch to "el-search"
-;; from isearch reusing already the given input.  The binding in
+;; The bindings in `isearch-mode-map' let you switch to "el-search"
+;; commands from isearch reusing already given input.  The binding in
 ;; `el-search-read-expression-map' allows you to hit C-S twice to
 ;; start a search using the last search pattern, similar to isearch.
 ;;
@@ -85,10 +87,10 @@
 ;;
 ;;    97
 ;;
-;; at the prompt, this will find any occurrence of the number 97 in
-;; the code, but not 977 or (+ 90 7) or "My string containing 97".
-;; But it will find anything `eq' to 97 after reading, e.g. #x61 or
-;; ?a.
+;; at the prompt, el-search will find any occurrence of the number 97
+;; in the code, but not 977 or (+ 90 7) or "My string containing 97"
+;; or symbol_97.  But it will find anything `equal' to 97 after
+;; reading, e.g. #x61 or ?a.
 ;;
 ;;
 ;; Example 2: If you enter the pattern
@@ -128,8 +130,8 @@
 ;; ,----------------------------------------------------------------------
 ;; | Q: "But I hate `pcase'!  Can't we just do without?"                 |
 ;; |                                                                     |
-;; | A: Respect that you kept up until here! Just use (guard CODE), where|
-;; | CODE is any normal Elisp expression that returns non-nil when and   |
+;; | A: Respect that you kept up until here! Just use (guard EXPR), where|
+;; | EXPR is any normal Elisp expression that returns non-nil when and   |
 ;; | only when you have a match.  Use the variable `exp' to refer to     |
 ;; | the currently tested expression.  Just like in the last example!    |
 ;; `----------------------------------------------------------------------
@@ -141,7 +143,7 @@
 ;; `el-search-defpattern'.  It is just like `pcase-defmacro', but the
 ;; effect is limited to this package (i.e. it uses a separate name
 ;; space).  See C-h f `el-search-pattern' for a list of predefined
-;; pattern forms.
+;; pattern types.
 ;;
 ;; Some additional pattern definitions can be found in the file
 ;; "el-search-x" which is part of this package.
@@ -153,8 +155,8 @@
 ;; You can replace expressions with command `el-search-query-replace'.
 ;; You are queried for a (pcase) pattern and a replacement expression.
 ;; For each match of the pattern, the replacement expression is
-;; evaluated with the bindings created by the pcase matching in
-;; effect, and printed to a string to produce the replacement.
+;; evaluated with the bindings created by pattern matching in effect,
+;; and printed to a string to produce the replacement.
 ;;
 ;; Example: In some buffer you want to swap the two expressions at the
 ;; places of the first two arguments in all calls of function `foo',
@@ -182,7 +184,7 @@
 ;; "splicing mode".  When it is active, the replacement expression
 ;; must evaluate to a list, and is spliced instead of inserted into
 ;; the buffer for any replaced match.  Use s to toggle splicing mode
-;; in a `el-search-query-replace' session.
+;; in an `el-search-query-replace' session.
 ;;
 ;;
 ;; Multi Searching
@@ -199,49 +201,39 @@
 ;; every search is internally a multi search.
 ;;
 ;; You can pause any (multi) search by just doing something different,
-;; the state of the search is automatically saved. You can continue
+;; the state of the search is automatically saved.  You can continue
 ;; searching by calling `el-search-jump-to-search-head': this command
 ;; jumps to the last match and re-activates the search.
 
 ;; `el-search-continue-in-next-buffer' skips all remaining matches in
 ;; the current buffer and continues searching in the next buffer.
+;; `el-search-skip-directory' even skips all subsequent files under a
+;; specified directory.
 ;;
 ;; Matches found in the current buffer are recorded; use
 ;; `el-search-previous-match' to revisit them in reverse order (this
 ;; is actually the poor-man's version of a backward search, since a
 ;; real backward el-search would be slow).
 ;;
-;; There is no multi query-replace currently implemented; I don't know
-;; if it would be that useful as a separate command anyway.  If you
-;; want to query-replace in multiple buffers or files, call an
+;; This package automatically uses techniques to speed up (multi)
+;; searching (without an impact on the matches you get, of course).
+;; The degree of possible optimizations varies very much depending on
+;; the nature of the search pattern, so the search speed can vary
+;; greatly.
+;;
+;; There are no special multi query-replace commands currently
+;; implemented; I don't know if it would be that useful anyway.  If
+;; you want to query-replace in multiple buffers or files, call an
 ;; appropriate multi-search command, and every time a first match is
 ;; found in any buffer, start an ordinary `el-search-query-replace';
 ;; after finishing, check that everything is ok, save etc, and resume
 ;; the multi search with one of the above commands.
 ;;
-;; There is currently nothing like `occur' for el-search.  However,
-;; you can get a list of matches in the form
-;; (file-name-or-buffer . match-position) with
-;;
-;;  (el-search-all-matches (el-search-make-search pattern stream))
-;;
-;; where PATTERN is the search pattern and STREAM is a stream of
-;; buffers or files (typical ways to construct such a STREAM are to
-;; call the `stream' function on a list of buffers, or to use
-;; `el-search-stream-of-directory-files').
-;;
-;; For example,
-;;
-;;   (el-search-all-matches
-;;    (el-search-make-search
-;;     ''require
-;;     (seq-filter
-;;      (lambda (buffer)
-;;         (with-current-buffer buffer (derived-mode-p 'emacs-lisp-mode)))
-;;      (stream (buffer-list)))))
-;;
-;; would return a list of matches for the symbol require in all elisp
-;; mode buffers.
+;; I've not yet implemented a real "occur" for el-search.  For now,
+;; there is the command `el-search-overview' (C-O in the suggested key
+;; bindings above).  It will display an overview for the current
+;; search in a separate window showing a complete count of matches per
+;; file/buffer.
 ;;
 ;;
 ;; Multiple multi searches
@@ -254,7 +246,8 @@
 ;; position where this search had been suspended.
 ;;
 ;; There is no special command to restart a prior search from the
-;; beginning.  I suggest to use `repeat-complex-command'.
+;; beginning.  I suggest to use the pattern input history or
+;; `repeat-complex-command'.
 ;;
 ;;
 ;; Writing replacement rules for semi-automatic code rewriting
@@ -281,9 +274,8 @@
 ;;             (let ,new `(dolist (,,var ,,list) . ,,body)))))
 ;;
 ;; The first condition in the `and' performs the matching and binds
-;; the essential parts of the `mapc' form to variables.  The second,
-;; the `let' part, binds the pattern specified argument NEW (as said,
-;; typically just a variable to receive the rewritten code) to the
+;; the essential parts of the `mapc' form to helper variables.  The
+;; second, the `let' part, binds the specified variable NEW to the
 ;; rewritten expression - in our case, a `dolist' form is constructed
 ;; with the remembered code parts filled in.
 ;;
@@ -292,14 +284,6 @@
 ;;
 ;;   (el-search-mapc->dolist replacement) -> replacement
 ;;
-;; And when you want to replace in multiple buffers or files, call an
-;; appropriate multi el-search command, e.g. `el-search-directory',
-;; and specify
-;;
-;;   (el-search-mapc->dolist replacement)
-;;
-;; as search pattern.
-;;
 ;;
 ;;
 ;; Bugs, Known Limitations
@@ -361,7 +345,7 @@
 ;;   that it's possible to replace also occurrences of a symbol in
 ;;   docstrings?
 ;;
-;; - Implement an occur like interface?
+;; - Implement an occur like interface
 ;;
 ;; - Port this to non Emacs Lisp modes?  How?  Would it already
 ;;   work using only syntax tables, sexp scanning and font-lock?
@@ -388,6 +372,7 @@
 (require 'cl-lib)
 (require 'elisp-mode)
 (require 'thingatpt)
+(require 'thunk)
 (require 'stream)
 (require 'help-fns) ;el-search--make-docstring
 (require 'ring)     ;el-search-history
@@ -412,7 +397,8 @@ When turned on, use a fast pre-processing algorithm to sort 
out
 buffers that can be proved to not contain a match.
 
 Setting this to nil should not have any effect apart from making
-multi-buffer searching slower in most cases.")
+multi-buffer searching slower in most cases, so this is only
+useful for debugging.")
 
 (defface el-search-match '((((background dark))
                             ;; (:background "#0000A0")
@@ -442,7 +428,7 @@ The value influences the behavior of the commands that 
perform
 directory searches like `el-search-directory' or
 `el-search-dired-marked-files'.  It is consulted by all streams
 `el-search-stream-of-directory-files' returns."
-  :type '(choice (repeat :tag "Ignored directories" regexp)
+  :type '(choice (repeat :tag "Regexps for ignored directories" regexp)
                 (const  :tag "No ignored directories" nil)))
 
 (defvar el-search-map
@@ -491,28 +477,32 @@ directory searches like `el-search-directory' or
   "Ignore the arguments and return t."
   t)
 
+(defun el-search-with-short-term-memory (function)
+  "Wrap FUNCTION to cache the last arguments/result pair."
+  (let ((cached nil))
+    (lambda (&rest args)
+      (pcase cached
+        (`(,(pred (equal args)) . ,result) result)
+        (_ (cdr (setq cached (cons args (apply function args)))))))))
+
 (defun el-search--message-no-log (format-string &rest args)
   "Like `message' but with `message-log-max' bound to nil."
   (let ((message-log-max nil))
     (apply #'message format-string args)))
 
-(defun el-search--string-match-p (eregexp string)
-  "Non-nil when extended regexp EREGEXP matches the STRING."
+(defun el-search--string-matcher (eregexp)
+  "Return a compiled match predicate for EREGEXP.
+That's a predicate returning non-nil when extended regexp EREGEXP
+matches the (only) argument (that should be a string)."
   (let ((match-bindings ()) regexp)
     (pcase eregexp
       ((pred stringp) (setq regexp eregexp))
       (`(,binds ,real-regexp)
        (setq regexp real-regexp)
        (setq match-bindings binds)))
-    (setq match-bindings
-          (mapcar (lambda (binding)
-                    (pcase binding
-                      ((pred symbolp)                  (list binding nil))
-                      (`(,(and (pred symbolp) symbol)) (list symbol nil))
-                      (_                               binding)))
-                  match-bindings))
-    (cl-progv (mapcar #'car match-bindings) (mapcar #'cadr match-bindings)
-      (string-match-p regexp string))))
+    (byte-compile
+     (let ((string (make-symbol "string")))
+       `(lambda (,string) (let ,match-bindings (string-match-p ,regexp 
,string)))))))
 
 (defun el-search--pp-to-string (expr)
   (let ((print-length nil)
@@ -577,9 +567,8 @@ directory searches like `el-search-directory' or
 (defun el-search--maybe-warn-about-unquoted-symbol (pattern)
   (when (and (symbolp pattern)
              (not (eq pattern '_))
-             (or (not (boundp pattern))
-                 (not (eq (symbol-value pattern) pattern))))
-    (error "Unbound symbol: %S" pattern)))
+             (not (keywordp pattern)))
+    (error "Free symbol: `%S' (missing a quote?)" pattern)))
 
 (defun el-search--read-pattern (prompt &optional default histvar)
   (cl-callf or histvar 'el-search-pattern-history)
@@ -688,54 +677,65 @@ a string or comment."
 Keys are pattern names (i.e. symbols), and values the associated
 heuristic matcher functions.")
 
+(defvar el-search--inverse-heuristic-matchers ())
+
 (defmacro el-search-defpattern (name args &rest body)
   "Like `pcase-defmacro', but for defining el-search patterns.
 
-The semantics is exactly that of `pcase-defmacro', but the scope
-of the definitions is limited to \"el-search\", using a separate
-name space.  The expansion is allowed to use any defined `pcase'
-pattern as well as any defined el-search pattern.
+The semantics is very similar to that of `pcase-defmacro', but
+the scope of the definitions is limited to \"el-search\", using a
+separate name space.  The expansion is allowed to use any defined
+`pcase' pattern as well as any defined el-search pattern.
 
 The docstring may be followed by a `defun' style declaration list
-DECL.  There is only one respected specification, it has the form
+DECL.  There is currently only one respected specification, it
+has the form
 
   \(heuristic-matcher MATCHER-FUNCTION\)
 
-and specifies the heuristic MATCHER-FUNCTION to be associated
-with the defined pattern NAME.
-
-The purpose of a heuristic matcher function is to speed up multi
-buffer searching.  When specified, the MATCHER-FUNCTION should be
-a function accepting the same arguments as the defined pattern.
-When called with the ARGS, this function should return a function
-that accepts a list of atoms, which is the complete list of atoms
-found in the buffer to search, and that returns non-nil when this
-buffer could contain a match for the pattern (NAME . ARGS), and
-nil when we can be sure that it contains no match (whereby an
-atom here is anything whose parts aren't searched by
-el-searching, like integers or strings, but unlike arrays).  When
-in doubt, this function must return non-nil.  When el-searching
-is started with a certain PATTERN, a heuristic matcher function
-is constructed by recursively destructuring PATTERN and combining
-the heuristic matchers of the subpatterns.  The resulting
-function is then used to dismiss any buffer for that can be
-proved that it can not contain any match.
+and specifies a heuristic MATCHER-FUNCTION to be associated with
+the defined pattern type NAME.
+
+The idea of heuristic matching is to speed up multi buffer
+searching without altering the matching behavior by discarding
+files and buffers which can't contain a match.  When specified,
+the MATCHER-FUNCTION should be a function accepting the same
+arguments as the defined pattern.  When called with arguments
+ARGS, this function should return either nil (meaning that for
+these specific arguments no heuristic matching should be done and
+normal matching should be used) or a (fast!) function that
+accepts two arguments: a file-name or buffer, and a thunk of a
+complete list of atoms in that file or buffer, and that returns
+non-nil when this file or buffer could contain a match for the
+pattern (NAME . ARGS), and nil when we can be sure that it
+contains no match (an atom here means anything whose parts aren't
+searched by el-searching, like integers or strings, but unlike
+arrays).  When in doubt, this returned function must return
+non-nil.
+
+When el-searching is started with a certain PATTERN, a heuristic
+matcher function is constructed by recursively destructuring
+PATTERN and combining the heuristic matchers of the subpatterns.
+The resulting function is then used to dismiss any buffer that
+can be proven that it can not contain any match.
 
 \(fn NAME ARGLIST &optional DOCSTRING DECL &rest BODY)"
   (declare (indent 2) (debug defun))
-  (let ((doc nil) (set-heuristic-matcher ()))
+  (let ((doc nil) (declaration-list ()))
     (when (stringp (car body))
-      (setq doc       (car body)
+      (setq doc  (car body)
             body (cdr body)))
     (pcase (car body)
-      (`(declare (heuristic-matcher ,heuristic-matcher))
-       (setq set-heuristic-matcher
-             `((setf (alist-get ',name el-search--heuristic-matchers) 
,heuristic-matcher)))
-       (setq body (cdr body))))
+      (`(declare . ,declarations)
+       (setq body (cdr body)
+             declaration-list declarations)))
     `(progn
-       ,@set-heuristic-matcher
+       (setf (alist-get ',name el-search--heuristic-matchers)
+             ,(car (alist-get 'heuristic-matcher declaration-list)))
+       (setf (alist-get ',name el-search--inverse-heuristic-matchers)
+             ,(car (alist-get 'inverse-heuristic-matcher declaration-list)))
        (setf (alist-get ',name el-search--pcase-macros)
-             (lambda ,args ,doc ,@body)))))
+             (lambda ,args ,@(and doc `(,doc)) ,@body)))))
 
 (defmacro el-search--with-additional-pcase-macros (&rest body)
   `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun)) `((get ',symbol 
'pcase-macroexpander) #',fun))
@@ -748,8 +748,9 @@ 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'."
+Return PATTERN if it is no el-search pattern, i.e. if there is no
+expander for this pattern type found in
+`el-search--pcase-macros'."
   (if-let ((expander (alist-get (car-safe pattern) el-search--pcase-macros)))
       (apply expander (cdr pattern))
     pattern))
@@ -809,8 +810,8 @@ Return PATTERN if this pattern type was not defined with
 (defun el-search-forward (pattern &optional noerror)
   "Search for el-search PATTERN in current buffer from point.
 Set point to the beginning of the occurrence found and return the
-new value of point.  Optional second argument, if non-nil, means
-if fail just return nil (no error)."
+new value of point.  Optional second argument NOERROR, if
+non-nil, means if fail just return nil (no error)."
   (el-search--search-pattern-1 (el-search--matcher pattern) noerror))
 
 
@@ -830,18 +831,24 @@ MESSAGE are used to construct the error message."
        (file-exists-p file)
        (not (file-directory-p file))))
 
-(cl-defstruct el-search-object
+(cl-defstruct (el-search-object (:copier copy-el-search-object--1))
   head       ;an `el-search-head' instance, modified ("moved") while searching
   matches    ;the stream of matches in the form (buffer position file)
   last-match ;position of last match found
   )
 
+(defun copy-el-search-object (search)
+  (let ((copy (copy-el-search-object--1 search)))
+    (cl-callf copy-el-search-head (el-search-object-head copy))
+    copy))
+
 (cl-defstruct el-search-head
+  get-buffer-stream        ;a function of zero args returning a stream of 
files and/or buffers to search
   matcher                  ;for the search pattern
   heuristic-buffer-matcher ;for the search pattern
   buffer                   ;currently searched buffer, or nil meaning 
"continue in next buffer"
-  file                     ;name of currently searched file, or nil
   position                 ;where to continue search in this buffer
+  file                     ;name of currently searched file, or nil
   buffers                  ;stream of buffers and/or files yet to search
   )
 
@@ -854,62 +861,99 @@ MESSAGE are used to construct the error message."
                   (and not-current-buffer (eq buffer (current-buffer))))
         (kill-buffer buffer)))))
 
+
 (defun el-search-heuristic-matcher (pattern)
   "Return a heuristic matcher for PATTERN.
-This is a predicate accepting a list of a file's or buffer's
-atoms and returns nil when we can be sure that this file or
-buffer can't contain a match for PATTERN, and non-nil else."
+This is a predicate accepting two arguments.  The first argument
+is a file name or a buffer.  The second argument is a thunk (see
+\"thunk.el\") of a list of all of this file's or buffer's atoms.
+The predicate returns nil when we can be sure that this file or
+buffer can't contain a match for the PATTERN, and must return
+non-nil else."
   (pcase pattern
     ((pred symbolp) #'el-search-true)
-    ((pred pcase--self-quoting-p) (apply-partially #'member pattern))
+    ((pred pcase--self-quoting-p) (lambda (_ atoms-thunk) (member pattern 
(thunk-force atoms-thunk))))
     (`',tree
      (pcase (el-search--flatten-tree tree)
-       (`(,tree)  (apply-partially #'member tree))
+       (`(,tree)  (lambda (_ atoms-thunk) (member tree (thunk-force 
atoms-thunk))))
        (flattened (let ((matchers (mapcar (lambda (atom) 
(el-search-heuristic-matcher `',atom))
                                           flattened)))
-                    (lambda (atoms) (cl-every (lambda (matcher) (funcall 
matcher atoms)) matchers))))))
+                    (lambda (file-name-or-buffer atoms-thunk)
+                      (cl-every (lambda (matcher) (funcall matcher 
file-name-or-buffer atoms-thunk))
+                                matchers))))))
     (``,qpat
      (cond
       ((eq (car-safe qpat) '\,) (el-search-heuristic-matcher (cadr qpat)))
       ((vectorp qpat)
        (let ((matchers (mapcar (lambda (inner-qpat) 
(el-search-heuristic-matcher (list '\` inner-qpat)))
                                qpat)))
-         (lambda (atoms) (cl-every (lambda (matcher) (funcall matcher atoms)) 
matchers))))
+         (lambda (file-name-or-buffer atoms-thunk)
+           (cl-every (lambda (matcher) (funcall matcher file-name-or-buffer 
atoms-thunk))
+                     matchers))))
       ((consp qpat)
        (el-search-heuristic-matcher
         `(and
           ,(list '\` (car qpat))
           ,(if (cdr qpat) (list '\` (cdr qpat)) '_))))
-      ((or (stringp qpat) (integerp qpat) (symbolp qpat)) (apply-partially 
#'member qpat))
+      ((or (stringp qpat) (integerp qpat) (symbolp qpat))
+       (lambda (_ atoms-thunk) (member qpat (thunk-force atoms-thunk))))
       (t #'el-search-true)))
     (`(and . ,patterns)
      (let ((matchers (mapcar #'el-search-heuristic-matcher patterns)))
-       (lambda (atoms) (cl-every (lambda (matcher) (funcall matcher atoms)) 
matchers))))
+       (lambda (file-name-or-buffer atoms-thunk)
+         (cl-every (lambda (matcher) (funcall matcher file-name-or-buffer 
atoms-thunk))
+                   matchers))))
     (`(or . ,patterns)
      (let ((matchers (mapcar #'el-search-heuristic-matcher patterns)))
-       (lambda (atoms) (cl-some (lambda (matcher) (funcall matcher atoms)) 
matchers))))
+       (lambda (file-name-or-buffer atoms-thunk)
+         (cl-some (lambda (matcher) (funcall matcher file-name-or-buffer 
atoms-thunk))
+                  matchers))))
     (`(,(or 'app 'let 'pred 'guard) . ,_) #'el-search-true)
     ((and `(,name . ,args)
-          (let matcher (alist-get name el-search--heuristic-matchers)) (guard 
matcher))
-     (ignore name) ;quite byte compiler
-     (apply matcher args))
+          (let heuristic-matcher (alist-get name 
el-search--heuristic-matchers))
+          (guard heuristic-matcher)
+          (let this-heuristic-matcher (apply heuristic-matcher args))
+          (guard this-heuristic-matcher))
+     (ignore name args heuristic-matcher) ;quite byte compiler
+     this-heuristic-matcher)
     ((and (app el-search--macroexpand-1 expanded)
           (guard (not (eq expanded pattern))))
      (el-search-heuristic-matcher expanded))
     (_ #'el-search-true)))
 
-(defun el-search-atom-list (buffer)
-  (with-current-buffer buffer
-    (apply #'append
-           (mapcar #'el-search--flatten-tree
-                   (save-excursion
-                     (goto-char (point-min))
-                     (let ((forms ()))
-                       (condition-case err
-                           (while t (push (read (current-buffer)) forms))
-                         (end-of-file forms)
-                         (error "Unexpected error whilst reading %s position 
%s: %s"
-                                buffer (point) err))))))))
+(defvar el-search--atom-list-cache (make-hash-table :test #'equal :size 1000))
+
+(defun el-search-atom-list (file-name-or-buffer)
+  "Return a list of el-search-atomic expressions in FILE-NAME-OR-BUFFER."
+  (let ((get-atoms
+         (lambda () (apply #'append
+                      (mapcar #'el-search--flatten-tree
+                              (save-excursion
+                                (goto-char (point-min))
+                                (let ((forms ()))
+                                  (condition-case err
+                                      (while t (push (read (current-buffer)) 
forms))
+                                    (end-of-file forms)
+                                    (error "Unexpected error whilst reading %s 
position %s: %s"
+                                           buffer (point) err))))))))
+        (buffer (if (bufferp file-name-or-buffer)
+                    file-name-or-buffer
+                  (get-file-buffer file-name-or-buffer))))
+    (if buffer
+        (if (buffer-live-p buffer)
+            (with-current-buffer buffer (funcall get-atoms))
+          ())
+      (let ((file-name file-name-or-buffer))
+        (if-let ((hash-entry (gethash file-name el-search--atom-list-cache))
+                 (its-usable (equal (nth 5 (file-attributes file-name)) (car 
hash-entry))))
+            (cdr hash-entry)
+          (let ((atom-list (with-temp-buffer
+                             (insert-file-contents file-name-or-buffer)
+                             (funcall get-atoms))))
+            (puthash file-name
+                     (cons (nth 5 (file-attributes file-name)) atom-list)
+                     el-search--atom-list-cache)
+            atom-list))))))
 
 (defun el-search--flatten-tree (tree)
   (let ((elements ()))
@@ -931,16 +975,13 @@ buffer can't contain a match for PATTERN, and non-nil 
else."
 (defun el-search-heuristic-buffer-matcher (pattern)
   (let ((heuristic-matcher (el-search-heuristic-matcher pattern)))
     (lambda (file-name-or-buffer)
-      (el-search--message-no-log "Searching in %s"
+      (el-search--message-no-log "%s"
                                  (if (stringp file-name-or-buffer)
                                      file-name-or-buffer
                                    (buffer-name file-name-or-buffer)))
-      (if (bufferp file-name-or-buffer)
-          (and (buffer-live-p file-name-or-buffer)
-               (funcall heuristic-matcher (el-search-atom-list 
(current-buffer))))
-        (with-temp-buffer
-          (insert-file-contents file-name-or-buffer)
-          (funcall heuristic-matcher (el-search-atom-list 
(current-buffer))))))))
+      (funcall heuristic-matcher
+               file-name-or-buffer
+               (thunk-delay (el-search-atom-list file-name-or-buffer))))))
 
 (defvar warning-minimum-level)
 (defun el-search--next-buffer (search &optional predicate)
@@ -997,68 +1038,85 @@ buffer can't contain a match for PATTERN, and non-nil 
else."
   (el-search--next-buffer el-search--current-search predicate)
   (el-search-continue-search))
 
-(defun el-search-make-search (pattern stream)
+(defun el-search--setup-matches-stream (search)
+  (let ((head (el-search-object-head search)))
+    (setf (el-search-object-matches search)
+          (seq-filter
+           #'identity ;we use `nil' as a "skip" tag
+           (funcall
+            (letrec ((get-stream
+                      (lambda ()
+                        (stream-make
+                         (if-let ((buffer (or (el-search-head-buffer head)
+                                              (el-search--next-buffer 
search))))
+                             (with-current-buffer buffer
+                               (save-excursion
+                                 (goto-char (el-search-head-position head))
+                                 (el-search--message-no-log "%s"
+                                                            (or 
(el-search-head-file head)
+                                                                
(el-search-head-buffer head)))
+                                 (if-let ((match (el-search--search-pattern-1
+                                                  (el-search-head-matcher 
head) t)))
+                                     (progn
+                                       (setf (el-search-object-last-match 
search)
+                                             (copy-marker (point)))
+                                       (el-search--skip-expression nil t)
+                                       (setf (el-search-head-position head)
+                                             (copy-marker (point)))
+                                       (cons
+                                        (list (el-search-head-buffer head)
+                                              match
+                                              (el-search-head-file head))
+                                        (funcall get-stream)))
+                                   (setf (el-search-head-buffer head) nil
+                                         (el-search-head-file   head) nil)
+                                   (el-search--next-buffer search)
+                                   ;; retry with the next buffer
+                                   (cons nil (funcall get-stream)))))
+                           ;; end of stream (no buffers left to search in)
+                           nil)))))
+              get-stream))))
+    search))
+
+(defun el-search-make-search (pattern get-buffer-stream)
   "Create and return a new `el-search-object' instance.
-MATCHER is the result of calling `el-search--matcher' on the
-pattern to search.  STREAM is a stream of buffers and/or files to
-search."
+PATTERN is the pattern to search, and GET-BUFFER-STREAM a
+function that returns a stream of buffers and/or files to search
+in, in order, when called with no arguments."
   (let* ((matcher (el-search--matcher pattern))
          (head (make-el-search-head
+                :get-buffer-stream get-buffer-stream
                 :matcher matcher
-                :buffers stream
-                :heuristic-buffer-matcher (el-search-heuristic-buffer-matcher 
pattern))))
-    (letrec ((search
-              (make-el-search-object
-               :head head
-               :matches
-               (seq-filter
-                #'identity ;we use `nil' as a "skip" tag
-                (funcall
-                 (letrec ((get-stream
-                           (lambda ()
-                             (stream-make
-                              (if-let ((buffer (or (el-search-head-buffer head)
-                                                   (el-search--next-buffer 
search))))
-                                  (with-current-buffer buffer
-                                    (save-excursion
-                                      ;; Widening already happens in 
`el-search-continue-search'
-                                      (goto-char (el-search-head-position 
head))
-                                      (el-search--message-no-log "Searching in 
%s"
-                                                                 (or 
(el-search-head-file head)
-                                                                     
(el-search-head-buffer head)))
-                                      (if-let ((match 
(el-search--search-pattern-1
-                                                       (el-search-head-matcher 
head) t)))
-                                          (progn
-                                            (setf (el-search-object-last-match 
search)
-                                                  (copy-marker (point)))
-                                            (el-search--skip-expression nil t)
-                                            (setf (el-search-head-position 
head)
-                                                  (copy-marker (point)))
-                                            (cons
-                                             (list (el-search-head-buffer head)
-                                                   match
-                                                   (el-search-head-file head))
-                                             (funcall get-stream)))
-                                        (setf (el-search-head-buffer head) nil
-                                              (el-search-head-file   head) nil)
-                                        (el-search--next-buffer search)
-                                        ;; retry with the next buffer
-                                        (cons nil (funcall get-stream)))))
-                                ;; end of stream (no buffers left to search in)
-                                nil)))))
-                   get-stream))))))
-      search)))
-
-(defun el-search-setup-search (pattern stream &optional from-here)
-  "Create and start a new search.
-PATTERN is the search pattern.  STREAM is a stream of buffers
-and/or files (i.e. file names) to search in.
-
-With optional FROM-HERE non-nil, the first buffer in STREAM
+                :buffers (funcall get-buffer-stream)
+                :heuristic-buffer-matcher (el-search-heuristic-buffer-matcher 
pattern)))
+         (search (make-el-search-object :head head)))
+    (el-search--setup-matches-stream search)
+    search))
+
+(defun el-search-reset-search (search)
+  "Return a reset copy of SEARCH."
+  (let* ((copy (copy-el-search-object search))
+         (head (el-search-object-head copy)))
+    (setf (el-search-head-buffers head)
+          (funcall (el-search-head-get-buffer-stream head)))
+    (setf (el-search-head-buffer head)   nil)
+    (setf (el-search-head-file head)     nil)
+    (setf (el-search-head-position head) nil)
+    (el-search--setup-matches-stream copy)
+    copy))
+
+(defun el-search-setup-search (pattern get-buffer-stream &optional from-here)
+  "Create and start a new el-search.
+PATTERN is the search pattern.  GET-BUFFER-STREAM is a function
+of no arguments that should return a stream of buffers and/or
+files (i.e. file names) to search in.
+
+With optional FROM-HERE non-nil, the first buffer in this stream
 should be the current buffer, and searching will start at the
 current buffer's point instead of its beginning."
   (setq el-search--success nil)
-  (setq el-search--current-search (el-search-make-search 
(el-search--wrap-pattern pattern) stream))
+  (setq el-search--current-search
+        (el-search-make-search (el-search--wrap-pattern pattern) 
get-buffer-stream))
   (setq el-search--current-matcher
         (el-search-head-matcher (el-search-object-head 
el-search--current-search)))
   (setq el-search--current-pattern pattern)
@@ -1110,19 +1168,17 @@ matching \"foo\", but not \"Foo\" even when 
`case-fold-search' is
 currently enabled."
   (declare (heuristic-matcher
             (lambda (&rest eregexps)
-              (let ((matchers
-                     (mapcar (lambda (eregexp) (apply-partially 
#'el-search--string-match-p eregexp))
-                             eregexps)))
-                (lambda (atoms)
+              (let ((eregexp-matchers (mapcar #'el-search--string-matcher 
eregexps)))
+                (lambda (_ atoms-thunk)
                   (cl-some
                    (lambda (atom)
                      (and (stringp atom)
-                          (cl-every (lambda (matcher) (funcall matcher atom)) 
matchers)))
-                   atoms))))))
+                          (cl-every (lambda (matcher) (funcall matcher atom)) 
eregexp-matchers)))
+                   (thunk-force atoms-thunk)))))))
   (el-search-defpattern--check-args "string" regexps #'el-search--eregexp-p
                                     "argument not a regexp")
   `(and (pred stringp)
-        ,@(mapcar (lambda (thing) `(pred (el-search--string-match-p ',thing)))
+        ,@(mapcar (lambda (regexp) `(pred ,(el-search--string-matcher regexp)))
                   regexps)))
 
 (el-search-defpattern symbol (&rest regexps)
@@ -1131,32 +1187,42 @@ Any of the REGEXPS can be an extended regexp of the form
 \(bindings regexp\) like in the \"string\" pattern."
   (declare (heuristic-matcher
             (lambda (&rest eregexps)
-              (let ((matchers
-                     (mapcar (lambda (eregexp) (apply-partially 
#'el-search--string-match-p eregexp))
-                             eregexps)))
-                (lambda (atoms)
+              (let ((eregexp-matchers
+                     (mapcar #'el-search--string-matcher eregexps)))
+                (lambda (_ atoms-thunk)
                   (cl-some
                    (lambda (atom)
                      (when-let ((symbol-name (and (symbolp atom) (symbol-name 
atom))))
-                       (cl-every (lambda (matcher) (funcall matcher 
symbol-name)) matchers)))
-                   atoms))))))
+                       (cl-every (lambda (matcher) (funcall matcher 
symbol-name)) eregexp-matchers)))
+                   (thunk-force atoms-thunk)))))))
   (el-search-defpattern--check-args "symbol" regexps #'el-search--eregexp-p
                                     "argument not a regexp")
   `(and (pred symbolp) (app symbol-name (string ,@regexps))))
 
-(defun el-search--contains-p (matcher exp)
-  "Return non-nil when expression 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)
+(defun el-search--contains-p (matcher expr)
+  "Return non-nil when expression tree EXPR contains a match for MATCHER.
+MATCHER is a matcher for the el-search pattern to match.  Recurse
+on all types of sequences el-search does not treat as atomic.
+Matches are not restricted to atoms; for example
+
+  (el-search--contains-p (el-search--matcher ''(2 3)) '(1 (2 3)))
+
+succeeds.
+
+In the positive case the return value is (t elt), where ELT is a
+matching element found in EXPR."
+  (if (el-search--match-p matcher expr)
+      (list t expr)
+    (and (sequencep expr)
          (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))))))
+           (if (consp expr)
+               (or (funcall try-match (car expr))
+                   (funcall try-match (cdr expr))) ;(1)
+             (cl-some try-match expr))))))
+;; (1) This means we consider (a b c) to "contain" (b c).  Because we
+;; want (a . (b c)) [such a style makes sense e.g. for alists] to
+;; "contain" (b c), and we don't want recursion to depend on actual
+;; reader syntax.
 
 (el-search-defpattern contains (&rest patterns)
   "Matches expressions that contain a match for all PATTERNs.
@@ -1174,70 +1240,121 @@ by \(contains 1\)."
   (declare (heuristic-matcher
             (lambda (&rest patterns)
               (let ((matchers (mapcar #'el-search-heuristic-matcher patterns)))
-                (lambda (atoms) (cl-every (lambda (matcher) (funcall matcher 
atoms)) matchers))))))
+                (lambda (file-name-or-buffer atoms-thunk)
+                  (cl-every (lambda (matcher) (funcall matcher 
file-name-or-buffer atoms-thunk))
+                            matchers))))))
   (cond
    ((null patterns) '_)
    ((null (cdr patterns))
     (let ((pattern (car patterns)))
       `(app ,(apply-partially #'el-search--contains-p (el-search--matcher 
pattern))
-            `(t ,,pattern))))
+            `(t ,,pattern)))) ; Match again to establish bindings PATTERN 
should create
    (t `(and ,@(mapcar (lambda (pattern) `(contains ,pattern)) patterns)))))
 
 (el-search-defpattern not (pattern)
-  "Matches any object that is not matched by PATTERN."
-  (declare (heuristic-matcher ;We can't just negate the hm of the PATTERN!
-            (lambda (_pattern) #'el-search-true)))
+  "Matches anything that is not matched by PATTERN."
+  (declare
+   (heuristic-matcher ;We can't just negate the hm of the PATTERN...
+    (lambda (pattern)
+      (pcase pattern
+        ((and `(,name . ,args)
+              (let inverse-heuristic-matcher (alist-get name 
el-search--inverse-heuristic-matchers))
+              (guard inverse-heuristic-matcher))
+         (if (eq t inverse-heuristic-matcher)
+             (when-let ((heuristic-matcher
+                         (apply (alist-get name el-search--heuristic-matchers) 
args)))
+               (lambda (file-name-or-buffer atoms-thunk)
+                 (not (funcall heuristic-matcher file-name-or-buffer 
atoms-thunk))))
+           (apply inverse-heuristic-matcher args)))))))
   `(app ,(apply-partially #'el-search--match-p (el-search--matcher pattern))
         (pred not)))
 
-(defun el-search--match-symbol-file (eregexp symbol)
-  (when-let ((symbol-file (and (symbolp symbol) (symbol-file symbol))))
-    (el-search--string-match-p
-     eregexp
-     (file-name-sans-extension (file-name-nondirectory symbol-file)))))
-
-(el-search-defpattern symbol-file (regexp)
-  "Matches any symbol whose `symbol-file' is matched by REGEXP.
-
-This pattern matches when the object is a symbol for that
-`symbol-file' returns a (non-nil) FILE-NAME so that
+(defalias 'el-search--symbol-file-matcher
+  (el-search-with-short-term-memory
+   (lambda (_current-load-history eregexp-or-predicate)
+     ;; We enclosure a prepared hash table containing all the symbols "in"
+     (let ((table (make-hash-table))
+           (file-name-matches-p
+            (if (functionp eregexp-or-predicate)
+                eregexp-or-predicate
+              (let ((string-matcher (el-search--string-matcher 
eregexp-or-predicate)))
+                (lambda (file-name) (funcall string-matcher 
(file-name-sans-extension
+                                                        
(file-name-nondirectory file-name))))))))
+       (pcase-dolist (`(,file-name . ,definitions) load-history)
+         (when (and (stringp file-name)
+                    (funcall file-name-matches-p file-name))
+           (dolist (definition definitions)
+             (pcase definition
+               ((or (and (pred symbolp) symbol)
+                    (and `(,type . ,symbol)
+                         (guard (not (memq type '(autoload require)))))
+                    `(cl-defmethod ,symbol . ,_))
+                (ignore type)
+                (puthash symbol t table))))))
+       (lambda (symbol) (and (symbolp symbol) (gethash symbol table)))))))
+
+(el-search-defpattern symbol-file (regexp-or-predicate)
+  "Matches any symbol whose `symbol-file' is matched by REGEXP-OR-PREDICATE.
+
+When REGEXP-OR-PREDICATE is a regexp, this pattern matches when
+the object is a symbol for that `symbol-file' returns a (non-nil)
+FILE-NAME so that
 
    (file-name-sans-extension (file-name-nondirectory FILENAME)))
 
-is matched by the REGEXP."
-  (el-search-defpattern--check-args "symbol-file" (list regexp) 
#'el-search--eregexp-p
-                                    "argument not a regexp")
-  `(pred (el-search--match-symbol-file ',regexp)))
-
-(el-search-defpattern char-prop (property)
-  "Matches the object if completely covered with PROPERTY.
-This pattern matches the object if its representation in the
-search buffer is completely covered with the character property
-PROPERTY.
-
-This pattern should only be used to match against the current
-object (so it can't be used inside an `app' pattern for
-example)."
-  `(guard (and (get-char-property (point) ',property)
-               ,(macroexp-let2 nil limit '(scan-sexps (point) 1)
-                  `(= (next-single-char-property-change
-                       (point) ',property nil ,limit)
-                      ,limit)))))
-
-(el-search-defpattern includes-prop (property)
-  "Matches the object if partly covered with PROPERTY.
-This pattern matches the object if its representation in the
-search buffer is partly covered with the character property
-PROPERTY.
-
-This pattern should only be used to match against the current
-object (so it can't be used inside an `app' pattern for
-example)."
-  `(guard (or (get-char-property (point) ',property)
-              ,(macroexp-let2 nil limit '(scan-sexps (point) 1)
-                 `(not (= (next-single-char-property-change
-                           (point) ',property nil ,limit)
-                          ,limit))))))
+is matched by it.  If REGEXP-OR-PREDICATE is a function
+expression, the absolute FILE-NAME is tested."
+  (declare
+   (heuristic-matcher
+    (lambda (regexp-or-predicate)
+      (lambda (_ atoms-thunk)
+        (cl-some (el-search--symbol-file-matcher (copy-sequence load-history) 
regexp-or-predicate)
+                 (thunk-force atoms-thunk))))))
+  (el-search-defpattern--check-args "symbol-file" (list regexp-or-predicate)
+                                    (lambda (arg) (or (el-search--eregexp-p 
arg) (functionp arg)))
+                                    "argument not a regexp or predicate")
+  (let ((this (make-symbol "this")))
+    `(and ,this
+          (guard (funcall (el-search--symbol-file-matcher (copy-sequence 
load-history)
+                                                          
',regexp-or-predicate)
+                          ,this)))))
+
+(defun el-search-file--matcher (&optional regexp-or-predicate)
+  ;; Return a file name matcher according to REGEXP-OR-PREDICATE.  This
+  ;; is a predicate accepting two arguments that returns non-nil when
+  ;; the first argument is a file name (i.e. a string) that is matched
+  ;; by/fulfills the REGEXP-OR-PREDICATE.  It ignores the second
+  ;; argument.
+  (let ((get-file-name (lambda (file-name-or-buffer)
+                         (if (bufferp file-name-or-buffer)
+                             (buffer-file-name file-name-or-buffer)
+                           file-name-or-buffer)))
+        (file-name-matcher (pcase regexp-or-predicate
+                             ('nil)
+                             ((pred stringp) (apply-partially #'string-match-p 
regexp-or-predicate))
+                             ((pred functionp) regexp-or-predicate)
+                             (_ (error "Pattern `file': illegal argument: %S" 
regexp-or-predicate)))))
+    (if (not regexp-or-predicate)
+        (lambda (file-name-or-buffer _) (funcall get-file-name 
file-name-or-buffer))
+      (let ((test-file-name-or-buffer
+             (el-search-with-short-term-memory
+              (lambda (file-name-or-buffer)
+                (when-let ((file-name (funcall get-file-name 
file-name-or-buffer)))
+                  (funcall file-name-matcher file-name))))))
+        (lambda (file-name-or-buffer _) (funcall test-file-name-or-buffer 
file-name-or-buffer))))))
+
+(el-search-defpattern file (&optional function-or-regexp)
+  "Matches anything when the searched buffer has an associated file.
+
+With REGEXP-OR-PATTERN given, the file's absolute name must be
+matched by it."
+  (declare (heuristic-matcher #'el-search-file--matcher)
+           (inverse-heuristic-matcher t))
+  (let ((file-name-matcher (el-search-file--matcher function-or-regexp)))
+    ;; We can't expand to just t because this would not work with `not'.
+    ;; `el-search-file--matcher' caches the result, so this is still a
+    ;; pseudo constant
+    `(guard (funcall ',file-name-matcher buffer-file-name nil))))
 
 
 ;;;; Highlighting
@@ -1353,28 +1470,13 @@ local binding of `window-scroll-functions'."
 (defun el-search-continue-in-next-buffer ()
   "Skip current search buffer and continue with the next."
   (interactive)
-  (el-search--skip-to-next-buffer
-   (lambda (buffer-or-file)
-     (not (if (bufferp buffer-or-file)
-              (eq buffer-or-file (current-buffer))
-            (file-equal-p buffer-or-file buffer-file-name))))))
-
-(defun el-search-all-matches (search)
-  "Perform SEARCH non-interactively and return a list of all matches.
-
-SEARCH is an `el-search-object'.  Execute SEARCH
-non-interactively until finished and return a list of matches in
-the form \(file-name-or-buffer . match-position)."
-  (mapcar
-   (pcase-lambda (`(,buffer ,position ,file))
-     (cons (if (buffer-live-p buffer) buffer file) position))
-   (seq-into-sequence (el-search-object-matches search))))
+  (el-search--skip-to-next-buffer))
 
 (defun el-search-jump-to-search-head (&optional previous-search)
-  (interactive "P")
   "Switch to current search buffer and go to the last match.
 With prefix arg, prompt for a prior search to resume, and make
 that the current search."
+  (interactive "P")
   (when previous-search
     (let ((entry (ring-ref
                   el-search-history
@@ -1415,7 +1517,9 @@ that the current search."
             (el-search-hl-sexp)
             (el-search-hl-other-matches el-search--current-matcher)
             (set-transient-map el-search-map))))
-    (error "Last search finished")))
+    (when (y-or-n-p "Last search finished; restart? ")
+      (cl-callf el-search-reset-search el-search--current-search)
+      (el-search-continue-search))))
 
 (defun el-search-continue-search (&optional from-here)
   "Continue or resume the current search.
@@ -1440,7 +1544,7 @@ continued."
          ((and current-search-buffer (buffer-live-p current-search-buffer))
           (error "Please resume from buffer %s" (buffer-name 
current-search-buffer)))
          (current-search-buffer
-          (error "Invalid search head: buffer killed")))))
+          (error "Search head points to a killed buffer")))))
     (unwind-protect
         (let ((stream-of-matches (el-search-object-matches 
el-search--current-search)))
           (if (not (stream-empty-p stream-of-matches))
@@ -1483,25 +1587,21 @@ continued."
 (defun el-search-pattern (pattern)
   "Start new or resume last elisp buffer search.
 
-Search current buffer for expressions that are matched by `pcase'
-PATTERN.  Use `read' to transform buffer contents into
-expressions.
-
-When called from the current search's current search buffer,
-continue that search from point.  Otherwise or when a new PATTERN
-is given, start a new single-buffer search from point.
-
-Use `emacs-lisp-mode' for reading the input pattern.  Some keys
-in the minibuffer have a special binding: to make it possible to
-edit multi line input, C-j inserts a newline, and up and down
-move the cursor vertically - see `el-search-read-expression-map'
-for details.
-
-
-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:"
+Search current buffer for expressions that are matched by
+PATTERN.  When called from the current search's current search
+buffer, continue that search from point.  Otherwise or when a new
+PATTERN is given, start a new single-buffer search from point.
+
+The minibuffer is put into `emacs-lisp-mode' for reading the
+input pattern, and there are some special key bindings:
+\\<el-search-read-expression-map>\\[newline] inserts a newline,
+and <up> and <down> are unbound to let you move the cursor
+vertically - see `el-search-read-expression-map' for details.
+
+PATTERN is an \"el-search\" pattern - which means, either a
+`pcase' pattern or complying with one of the additional pattern
+types defined with `el-search-defpattern'.  The following
+additional pattern types are currently defined:"
   (interactive (list (if (and (eq this-command last-command)
                               el-search--success)
                          el-search--current-pattern
@@ -1517,7 +1617,11 @@ The following additional pattern types are currently 
defined:"
              (el-search-head-buffer (el-search-object-head 
el-search--current-search))))
     (el-search-continue-search 'from-here))
    (t ;; create a new search single-buffer search
-    (el-search-setup-search pattern (stream (list (current-buffer))) 
'from-here))))
+    (el-search-setup-search
+     pattern
+     (let ((current-buffer (current-buffer)))
+       (lambda () (stream (list current-buffer))))
+     'from-here))))
 
 (put 'el-search-pattern 'function-documentation '(el-search--make-docstring 
'el-search-pattern))
 
@@ -1547,15 +1651,52 @@ The following additional pattern types are currently 
defined:"
             (el-search-hl-sexp)
             (set-transient-map el-search-map)))))))
 
+(defun el-search--occur (search)
+  ;; This is a poorly written stub!
+  (cl-letf ((occur-buffer (generate-new-buffer "*El Occur*"))
+            (last nil) (matches nil) (overall-matches 0)
+            (el-search-keep-hl t)
+            ((symbol-function 'el-search-hl-remove) #'ignore))
+    (setq this-command 'el-search-pattern)
+    (setq-local el-search--temp-buffer-flag nil)
+    (with-selected-window (display-buffer
+                           occur-buffer
+                           '((display-buffer-pop-up-window 
display-buffer-use-some-window)))
+      (let ((done nil))
+        (unwind-protect
+            (progn
+              (seq-do
+               (pcase-lambda (`(,buffer ,_position ,file))
+                 (when buffer (cl-incf overall-matches))
+                 (if (equal last (list buffer file))
+                     (cl-incf matches)
+                   (when matches (insert (format "%3d matches in %S\n" matches 
(or (cadr last) (car last)))))
+                   (redisplay)
+                   (setq last (list buffer file))
+                   (setq matches 1)))
+               (stream-append (el-search-object-matches 
(el-search-reset-search search))
+                              (stream (list (list nil nil nil)))))
+              (insert (format "\n\n%d matches in total." overall-matches))
+              (setq done t))
+          (unless done (insert "\n\nAborted."))
+          (el-search--message-no-log ""))))))
+
+(defun el-search-overview ()
+  "Display an overview of matches of the current search."
+  (interactive)
+  (el-search--occur el-search--current-search))
+
+
 ;;;###autoload
 (defun el-search-buffers (pattern)
   "Search all live elisp buffers for PATTERN."
   (interactive (list (el-search--read-pattern-for-interactive)))
   (el-search-setup-search
    pattern
-   (seq-filter
-    (lambda (buffer) (with-current-buffer buffer (derived-mode-p 
'emacs-lisp-mode)))
-    (stream (buffer-list)))))
+   (lambda ()
+     (seq-filter
+      (lambda (buffer) (with-current-buffer buffer (derived-mode-p 
'emacs-lisp-mode)))
+      (stream (buffer-list))))))
 
 ;;;###autoload
 (defun el-search-directory (pattern directory &optional recursively)
@@ -1569,7 +1710,7 @@ With prefix arg RECURSIVELY non-nil, search 
subdirectories recursively."
                      current-prefix-arg))
   (el-search-setup-search
    pattern
-   (el-search-stream-of-directory-files directory recursively)))
+   (lambda () (el-search-stream-of-directory-files directory recursively))))
 
 ;;;###autoload
 (defun el-search-emacs-elisp-sources (pattern)
@@ -1579,39 +1720,43 @@ This command recursively searches all elisp files under
   (interactive (list (el-search--read-pattern-for-interactive)))
   (el-search-setup-search
    pattern
-   (el-search-stream-of-directory-files
-    (expand-file-name "lisp/" source-directory)
-    t)))
+   (lambda ()
+     (el-search-stream-of-directory-files
+      (expand-file-name "lisp/" source-directory)
+      t))))
 
 ;;;###autoload
 (defun el-search-load-path (pattern)
-  "Search PATTERN in the elisp files in all directories of `load-path'.
+  "Search PATTERN in all elisp files in all directories in `load-path'.
 nil elements in `load-path' (standing for `default-directory')
 are ignored."
   (interactive (list (el-search--read-pattern-for-interactive)))
   (el-search-setup-search
    pattern
-   (stream-concatenate
-    (seq-map (lambda (path) (el-search-stream-of-directory-files path nil))
-             (stream (delq nil load-path))))))
+   (lambda ()
+     (stream-concatenate
+      (seq-map (lambda (path) (el-search-stream-of-directory-files path nil))
+               (stream (delq nil load-path)))))))
 
 (declare-function dired-get-marked-files "dired")
 
 ;;;###autoload
 (defun el-search-dired-marked-files (pattern &optional recursively)
-  "el-search marked files and directories in dired.
+  "El-search marked files and directories in dired.
 With RECURSIVELY given (the prefix arg in an interactive call),
 search directories recursively."
   (interactive (list (el-search--read-pattern-for-interactive) 
current-prefix-arg))
   (el-search-setup-search
    pattern
-   (stream-concatenate
-    (seq-map
-     (lambda (file)
-       (if (file-directory-p file)
-           (el-search-stream-of-directory-files file recursively)
-         (stream (list file))))
-     (stream (dired-get-marked-files))))))
+   (let ((files (dired-get-marked-files)))
+     (lambda ()
+       (stream-concatenate
+        (seq-map
+         (lambda (file)
+           (if (file-directory-p file)
+               (el-search-stream-of-directory-files file recursively)
+             (stream (list file))))
+         (stream files)))))))
 
 
 ;;;; Query-replace
@@ -1926,7 +2071,7 @@ you can also give an input of the form
 \(\">\" and \"=>\" are also allowed as a separator) to the first
 prompt and specify both expressions at once.  This format is also
 used for history entries."
-  (interactive (el-search-query-replace--read-args))
+  (interactive (el-search-query-replace--read-args)) ;this binds the optional 
argument
   (setq this-command 'el-search-query-replace) ;in case we come from isearch
   (setq el-search--current-pattern from-pattern)
   (barf-if-buffer-read-only)
@@ -1952,12 +2097,13 @@ Reuse already given input."
     ;; use `call-interactively' so we get recorded in 
`extended-command-history'
     (call-interactively #'el-search-pattern)))
 
-;; Is this useful at all?
-;; ;;;###autoload
-;; (defun el-search-replace-from-isearch ()
-;;   (interactive)
-;;   (let ((el-search--initial-mb-contents (concat "'" 
(el-search--take-over-from-isearch t))))
-;;     (call-interactively #'el-search-query-replace)))
+;;;###autoload
+(defun el-search-replace-from-isearch ()
+  "Switch to `el-search-query-replace' from isearch.
+Reuse already given input."
+  (interactive)
+  (let ((el-search--initial-mb-contents (concat "'" 
(el-search--take-over-from-isearch t))))
+    (call-interactively #'el-search-query-replace)))
 
 
 (provide 'el-search)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]