]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/el-search/el-search.el
some doc tweaks
[gnu-emacs-elpa] / packages / el-search / el-search.el
index 3c7af293a06c34864cf78fccc942d5b2d9307ae7..1107a10d908550afb1f1a13904836a212bedd5dd 100644 (file)
@@ -32,7 +32,7 @@
 ;; Introduction
 ;; ============
 ;;
-;; 
+;;
 ;; The main user entry point is the command `el-search-pattern'.  It
 ;; prompts for a `pcase' pattern and searches the current buffer for
 ;; expressions that are matched by it when read.  Point is put at the
 ;; `emacs-lisp-mode'.
 ;;
 ;; When reading a search pattern in the minibuffer, the input is
-;; automatically wrapped into `(and expr ,(read input)).  So, if you
+;; automatically wrapped into `(and exp ,(read input)).  So, if you
 ;; want to search a buffer for symbols that are defined in "cl-lib",
 ;; you can use this pattern
 ;;
-;;   (guard (and (symbolp expr)
-;;               (when-let ((file (symbol-file expr)))
+;;   (guard (and (symbolp exp)
+;;               (when-let ((file (symbol-file exp)))
 ;;                 (string-match-p "cl-lib\\.elc?$" file))))
 ;;
-;; without binding the variable `expr'.
+;; without binding the variable `exp'.
 ;;
 ;;
 ;; Replacing
 ;; 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',
 ;; so that e.g.
-;; 
+;;
 ;;   (foo 'a (* 2 (+ 3 4)) t)
-;;   
+;;
 ;; becomes
-;; 
+;;
 ;;   (foo (* 2 (+ 3 4)) 'a t).
-;;   
+;;
 ;; This will do it:
 ;;
 ;;    M-x el-search-query-replace RET
 ;;
 ;; TODO:
 ;;
+;; - display something useful in the echo area.  or leave it for being
+;; able to `message' in the pattern?
+;;
+;; - implement backward searching
+;;
 ;; - improve docstrings
 ;;
 ;; - add more examples
 ;;
+;; - handle more reader syntaxes, e.g. #n, #n#
+;;
 ;; - Implement sessions; add multi-file support based on iterators.  A
 ;; file list is read in (or the user can specify an iterator as a
 ;; variable).  The state in the current buffer is just (buffer
   "Expression based search and replace for `emacs-lisp-mode'."
   :group 'lisp)
 
-(defcustom el-search-this-expression-identifier 'expr
-  "Name of the identifier referring to the whole expression.
-The default value is `expr'.  You can use this variable in the
-search prompt to refer to value of the currently searched
-expression."
+(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."
   :type 'symbol)
 
 (defface el-search-match '((((background dark)) (:background "#0000A0"))
@@ -232,35 +238,41 @@ expression."
     (read-from-minibuffer prompt initial-contents el-search-read-expression-map read
                           (or hist 'read-expression-history) default)))
 
-(defun el-search--read-pattern (prompt &optional default initial-contents read)
+(defun el-search--read-pattern (prompt &optional default read)
   (el-search-read-expression
-   prompt initial-contents 'el-search-history
+   prompt el-search--initial-mb-contents 'el-search-history
    (or default (when-let ((this-sexp (sexp-at-point)))
                  (concat "'" (el-search--print this-sexp))))
    read))
 
-(defun el-search--goto-next-sexp ()
-  "Move point to the beginning of the next sexp.
-Don't move if already at beginning of a sexp."
+(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."
   (let ((not-done t) res)
     (while not-done
-      (let ((stop-here nil) syntax-here)
+      (let ((stop-here nil)
+            (looking-at-from-back (lambda (regexp n)
+                                    (save-excursion
+                                      (backward-char n)
+                                      (looking-at regexp)))))
         (while (not stop-here)
           (cond
            ((eobp) (signal 'end-of-buffer nil))
            ((looking-at (rx (and (* space) ";"))) (forward-line))
            ((looking-at (rx (+ (or space "\n")))) (goto-char (match-end 0)))
-           ((progn (setq syntax-here (syntax-ppss))
-                   (or (nth 4 syntax-here) (nth 8 syntax-here)))
-            (if (nth 4 syntax-here) (forward-line) (search-forward "\"")))
-           
+
            ;; FIXME: can the rest be done more generically?
            ((and (looking-at (rx (or (syntax symbol) (syntax word))))
                  (not (looking-at "\\_<"))
-                 (not (looking-back ",@" 2)))
+                 (not (funcall looking-at-from-back ",@" 2)))
             (forward-symbol 1))
-           ((or (and (looking-at "'") (looking-back "#" 1))
-                (and (looking-at "@") (looking-back "," 1)))
+           ((or (and (looking-at "'") (funcall looking-at-from-back "#" 1))
+                (and (looking-at "@") (funcall looking-at-from-back "," 1)))
             (forward-char))
            (t (setq stop-here t)))))
       (condition-case nil
@@ -270,57 +282,71 @@ Don't move if already at beginning of a sexp."
         (error (forward-char))))
     res))
 
-(defun el-search--match-p (pattern expression)
-  (funcall
-   `(lambda ()
-      (pcase ',expression
-        (,pattern t)
-        (_        nil)))))
-
-(defun el-search-expression-contains-match-p (pattern expression)
-  "Whether some subexp of EXPRESSION is matched by PATTERN."
-  (or (el-search--match-p pattern expression)
-      (and (consp expression)
-           (if (cdr (last expression))
-               ;; a dotted list
-               (or (el-search-expression-contains-match-p pattern (car expression))
-                   (el-search-expression-contains-match-p pattern (cdr expression)))
-             (cl-some (lambda (subexpr) (el-search-expression-contains-match-p pattern subexpr))
-                      expression)))))
-
-(defun el-search--maybe-wrap-pattern (pattern)
-  (if (el-search-expression-contains-match-p `',el-search-this-expression-identifier pattern)
-      `(and ,el-search-this-expression-identifier ,pattern)
-    pattern))
+(defun el-search--matcher (pattern &rest body)
+  (let ((warning-suppress-log-types '((bytecomp))))
+    (byte-compile
+     `(lambda (expression)
+        (pcase expression
+          (,pattern ,@(or body (list t)))
+          (_        nil))))))
+
+(defun el-search--match-p (matcher expression)
+  (funcall matcher expression))
+
+(defun el-search--wrap-pattern (pattern)
+  `(and ,el-search-this-expression-identifier ,pattern))
+
+(defun el-search--skip-expression (expression &optional read)
+  ;; Move forward at least one character.  Don't move into a string or
+  ;; comment.  Don't move further than the beginning of the next sexp.
+  ;; Try to move as far as possible.  Point must be at the beginning
+  ;; of an expression.
+  ;; If there are positions where `read' would succeed, but that do
+  ;; not represent a valid sexp start, move past them (e.g. when
+  ;; before "#'" move past both characters).
+  ;;
+  ;; EXPRESSION must be the (read) expression at point, but when READ
+  ;; is non-nil, ignore the first argument and read the expression at
+  ;; point instead.
+  (when read (setq expression (save-excursion (read (current-buffer)))))
+  (cond
+   ((or (null expression)
+        (equal [] expression)
+        (not (or (listp expression) (vectorp expression))))
+    (goto-char (el-search--end-of-sexp)))
+   ((looking-at (rx (or ",@" "," "#'" "'")))
+    (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)."
-  ;; For better performance we read complete top-level sexps and test
-  ;; for matches.  We enter top-level expressions in the buffer text
-  ;; only when the test was successful.
-  (let ((match-beg nil) (opoint (point)) current-expr)
+  
+  (let ((matcher (el-search--matcher pattern)) (match-beg nil) (opoint (point)) current-expr)
+
+    ;; when inside a string or comment, move past it
+    (let ((syntax-here (syntax-ppss)))
+      (when (nth 3 syntax-here) ;inside a string
+        (goto-char (nth 8 syntax-here))
+        (forward-sexp))
+      (when (nth 4 syntax-here) ;inside a comment
+        (forward-line 1)
+        (while (and (not (eobp)) (looking-at (rx (and (* space) ";"))))
+          (forward-line 1))))
+
     (if (catch 'no-match
           (while (not match-beg)
             (condition-case nil
-                (setq current-expr (el-search--goto-next-sexp))
+                (setq current-expr (el-search--ensure-sexp-start))
               (end-of-buffer
                (goto-char opoint)
                (throw 'no-match t)))
-            (if (and (zerop (car (syntax-ppss)))
-                     (not (el-search-expression-contains-match-p pattern current-expr)))
-                ;; nothing here; skip to next top level form
-                (let ((end-of-next-sexp (scan-sexps (point) 2)))
-                  (if (not end-of-next-sexp)
-                      (throw 'no-match t)
-                    (goto-char end-of-next-sexp)
-                    (backward-sexp)))
-              (if (el-search--match-p pattern current-expr)
+            (if (el-search--match-p matcher current-expr)
                 (setq match-beg (point)
                       opoint (point))
-                (forward-char)))))
+              (el-search--skip-expression current-expr))))
         (if noerror nil (signal 'end-of-buffer nil)))
     match-beg))
 
@@ -329,12 +355,12 @@ return nil (no error)."
   (save-excursion
     (goto-char pos)
     (condition-case nil
-        (while (or (not bound) (< (point) bound))
+        (while (< (point) (or bound (point-max)))
           (let* ((this-sexp-end (save-excursion (thing-at-point--end-of-sexp) (point)))
-                 (this-sexp (buffer-substring-no-properties (point) this-sexp-end)))
-            (funcall do-fun this-sexp this-sexp-end))
-          (forward-char)
-          (el-search--goto-next-sexp))
+                 (this-sexp-bounds (buffer-substring-no-properties (point) this-sexp-end)))
+            (funcall do-fun this-sexp-bounds this-sexp-end)
+            (el-search--skip-expression (read this-sexp-bounds))
+            (el-search--ensure-sexp-start)))
       (end-of-buffer))
     (when ret-fun (funcall ret-fun))))
 
@@ -370,8 +396,8 @@ return nil (no error)."
 
 (defvar el-search-keep-hl nil)
 
-(defun el-search-hl-sexp ()
-  (let ((bounds (list (point) (scan-sexps (point) 1))))
+(defun el-search-hl-sexp-at-point ()
+  (let ((bounds (list (point) (el-search--end-of-sexp))))
     (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))
@@ -403,26 +429,29 @@ return nil (no error)."
 
 ;;;###autoload
 (defun el-search-pattern (pattern)
-  "Do incremental elisp search forward."
-  (interactive (list (if (and (eq this-command last-command)
-                              el-search-success)
+  "Do incremental elisp search or resume last search."
+  (interactive (list (if (eq this-command last-command)
                          el-search-current-pattern
                        (let ((pattern
                               (el-search--read-pattern "Find pcase pattern: "
                                                        (car el-search-history)
-                                                       nil t)))
+                                                       t)))
                          ;; A very common mistake: input "foo" instead of "'foo"
                          (when (and (symbolp pattern)
                                     (not (eq pattern '_))
                                     (or (not (boundp pattern))
                                         (not (eq (symbol-value pattern) pattern))))
                            (error "Please don't forget the quote when searching for a symbol"))
-                         (el-search--maybe-wrap-pattern pattern)))))
+                         (el-search--wrap-pattern pattern)))))
+  (setq this-command 'el-search-pattern) ;in case we come from isearch
   (setq el-search-current-pattern pattern)
-  (setq el-search-success nil)
   (let ((opoint (point)))
     (when (eq this-command last-command)
-      (forward-char))
+      (if el-search-success
+          (el-search--skip-expression nil t)
+        ;; wrap search
+        (goto-char (point-min))))
+    (setq el-search-success nil)
     (when (condition-case nil
               (el-search--search-pattern pattern)
             (end-of-buffer (message "No match")
@@ -431,22 +460,22 @@ return nil (no error)."
                            (ding)
                            nil))
       (setq el-search-success t)
-      (el-search-hl-sexp)
-      (message "%s" (substitute-command-keys "Type \\[el-search-pattern] to repeat")))))
+      (el-search-hl-sexp-at-point))))
 
 (defun el-search-search-and-replace-pattern (pattern replacement &optional mapping)
   (let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
-        (el-search-keep-hl t) (opoint (point)))
+        (el-search-keep-hl t) (opoint (point))
+        (get-replacement (el-search--matcher pattern replacement)))
     (unwind-protect
         (while (and (not done) (el-search--search-pattern pattern t))
           (setq opoint (point))
-          (unless replace-all (el-search-hl-sexp))
+          (unless replace-all (el-search-hl-sexp-at-point))
           (let* ((read-mapping (el-search--create-read-map))
-                 (region (list (point)  (scan-sexps (point) 1)))
+                 (region (list (point) (el-search--end-of-sexp)))
                  (substring (apply #'buffer-substring-no-properties region))
                  (expr      (read substring))
                  (replaced-this nil)
-                 (new-expr  (funcall `(lambda () (pcase ',expr (,pattern ,replacement)))))
+                 (new-expr  (funcall get-replacement expr))
                  (to-insert (el-search--repair-replacement-layout
                              (el-search--print new-expr) (append mapping read-mapping)))
                  (do-replace (lambda ()
@@ -457,7 +486,7 @@ return nil (no error)."
                                    (insert to-insert)
                                    (indent-region opoint (point))
                                    (goto-char opoint)
-                                   (el-search-hl-sexp)))
+                                   (el-search-hl-sexp-at-point)))
                                (cl-incf nbr-replaced)
                                (setq replaced-this t))))
             (if replace-all
@@ -484,7 +513,7 @@ return nil (no error)."
                                 t)
                             (?q (setq done t)
                                 t)))))
-            (unless (or done (eobp)) (forward-char 1)))))
+            (unless (or done (eobp)) (el-search--skip-expression nil t)))))
     (el-search-hl-remove)
     (goto-char opoint)
     (message "Replaced %d matches%s"
@@ -492,11 +521,16 @@ return nil (no error)."
              (if (zerop nbr-skipped)  ""
                (format "   (%d skipped)" nbr-skipped)))))
 
-(defun el-search-query-replace-read-args (&optional initial-contents)
+;; We need a variable for the initial contents because we want to `call-interactively '
+;; `el-search-query-replace-read-args'
+(defvar el-search--initial-mb-contents nil)
+
+(defun el-search-query-replace-read-args ()
   (barf-if-buffer-read-only)
-  (let* ((from (el-search--read-pattern "Replace from: " nil initial-contents))
-         (to   (el-search--read-pattern "Replace with result of evaluation of: " from)))
-    (list (el-search--maybe-wrap-pattern (read from)) (read to)
+  (let* ((from (el-search--read-pattern "Replace from: "))
+         (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)))))
@@ -505,6 +539,7 @@ return nil (no error)."
 (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))
+  (setq this-command 'el-search-query-replace) ;in case we come from isearch
   (setq el-search-current-pattern from)
   (barf-if-buffer-read-only)
   (el-search-search-and-replace-pattern from to mapping))
@@ -519,20 +554,19 @@ return nil (no error)."
 
 ;;;###autoload
 (defun el-search-search-from-isearch ()
-  ;; FIXME: an interesting alternative would be to really integrate it with
-  ;; Isearch, using `isearch-search-fun'.
+  ;; FIXME: an interesting alternative would be to really integrate it
+  ;; with Isearch, using `isearch-search-fun-function'.
+  ;; Alas, this is not trivial if we want to transfer our optimizations.
   (interactive)
-  (el-search-pattern
-   (el-search--read-pattern
-    "Find pcase pattern: " nil (concat "'" (el-search--take-over-from-isearch)) t))
-  (setq this-command 'el-search-pattern))
+  (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
+    ;; use `call-interactively' so we get recorded in `extended-command-history'
+    (call-interactively #'el-search-pattern)))
 
 ;;;###autoload
 (defun el-search-replace-from-isearch ()
   (interactive)
-  (let ((this-command 'el-search-query-replace))
-    (apply #'el-search-query-replace
-           (el-search-query-replace-read-args (concat "'" (el-search--take-over-from-isearch))))))
+  (let ((el-search--initial-mb-contents (concat "'" (el-search--take-over-from-isearch))))
+    (call-interactively #'el-search-query-replace)))