]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/el-search/el-search.el
prerequisites to allow replace with multiple expressions
[gnu-emacs-elpa] / packages / el-search / el-search.el
index d51f61bba92f7e203d9080970ece9e1feef8b2bd..05cbc74aff083fd1087505aacf5fa51607106eb1 100644 (file)
@@ -7,7 +7,7 @@
 ;; Created: 29 Jul 2015
 ;; Keywords: lisp
 ;; Compatibility: GNU Emacs 25
-;; Version: 0.0.3
+;; Version: 0.1
 ;; Package-Requires: ((emacs "25"))
 
 
 ;; ============
 ;;
 ;;
-;; The main user entry point is the command `el-search-pattern'.  It
+;; The main user entry point is `el-search-pattern'.  This command
 ;; prompts for a `pcase' pattern and searches the current buffer for
-;; expressions that are matched by it when read.  Point is put at the
-;; beginning of the expression found (unlike isearch).
+;; matching expressions by iteratively `read'ing buffer contents.  For
+;; any match, point is put at the beginning of the expression found
+;; (unlike isearch which puts point at the end of matches).
 ;;
 ;; It doesn't matter how the code is actually formatted.  Comments are
-;; ignored by the search, and strings are treated as objects, their
-;; contents are not being searched.
+;; ignored, and strings are treated as atomic objects, their contents
+;; are not being searched.
 ;;
 ;; Example 1: if you enter
 ;;
 ;;      ,(and s (guard (< 70 (length (car (split-string s "\n")))))))
 ;;
 ;;
+;; When a search pattern is processed, the searched buffer is current
+;; with point at the beginning of the currently tested expression.
+;;
+;;
+;; Example 3:
+;;
+;; I can be useful to use (guard EXP) patterns for side effects.
+;;
+;; 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
 ;; ===========
 ;;
-;; For expression input, the minibuffer prompts here uses
-;; `emacs-lisp-mode'.
+;; For pattern input, the minibuffer is put into `emacs-lisp-mode'.
+;;
+;; Any input PATTERN is silently transformed into (and exp PATTERN)
+;; so that you can always refer to the whole currently tested
+;; expression via the variable `exp'.
 ;;
-;; When reading a search pattern in the minibuffer, the input is
-;; 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
+;; Example 4:
+;;
+;; If you want to search a buffer for symbols that are defined in
+;; "cl-lib", you can use this pattern
 ;;
 ;;   (guard (and (symbolp exp)
 ;;               (when-let ((file (symbol-file exp)))
 ;;                 (string-match-p "cl-lib\\.elc?$" file))))
 ;;
-;; without binding the variable `exp'.
+;;
+;; ,----------------------------------------------------------------------
+;; | 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   |
+;; | only when you have a match.  Use the variable `exp' to refer to     |
+;; | the currently tested expression.  Just like in the last example!    |
+;; `----------------------------------------------------------------------
+;;
+;;
+;; It's cumbersome to write out the same complicated pattern
+;; constructs in the minibuffer again and again.  You can define your
+;; own pcase pattern types for the purpose of el-search with
+;; `el-search-defpattern'.  It is just like `pcase-defmacro', but the
+;; effect is limited to this package.  See C-h f `el-search-pattern'
+;; for a list of predefined additional pattern forms.
 ;;
 ;;
 ;; Replacing
 ;;
 ;; TODO:
 ;;
-;; - display something useful in the echo area.  or leave it for being
-;; able to `message' in the pattern?
+;; - change replace interface to include toggle(s)
+;;
+;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
+;;
+;; - highlight matches around point in a timer
 ;;
 ;; - 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
-;; . marker).  Or should this be abstracted into an own lib?  Could be
-;; named "files-session" or so.
+;;   file list is read in (or the user can specify an iterator as a
+;;   variable).  The state in the current buffer is just (buffer
+;;   . marker).  Or should this be abstracted into an own lib?  Could
+;;   be named "files-session" or so.
 
 
 
 (require 'cl-lib)
 (require 'elisp-mode)
 (require 'thingatpt)
+(require 'help-fns) ;el-search--make-docstring
 
 
 ;;;; Configuration stuff
@@ -238,12 +291,20 @@ prompt to refer to the value of the currently tested expression."
     (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)
-  (el-search-read-expression
-   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))
+  (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))))
 
 (defun el-search--end-of-sexp ()
   ;;Point must be at sexp beginning
@@ -282,13 +343,116 @@ Point must not be inside a string or comment."
         (error (forward-char))))
     res))
 
+(defvar el-search--pcase-macros '()
+  "List of additional \"el-search\" pcase macros.")
+
+(defun el-search--make-docstring ()
+  ;; code mainly from `pcase--make-docstring'
+  (let* ((main (documentation (symbol-function 'el-search-pattern) 'raw))
+         (ud (help-split-fundoc main 'pcase)))
+    (with-temp-buffer
+      (insert (or (cdr ud) main))
+      (mapc
+       (pcase-lambda (`(,symbol . ,fun))
+         (when-let ((doc (documentation fun)))
+           (insert "\n\n-- ")
+           (setq doc (help-fns--signature symbol doc nil fun nil))
+           (insert "\n" (or doc "Not documented."))))
+       (reverse el-search--pcase-macros))
+      (let ((combined-doc (buffer-string)))
+        (if ud (help-add-fundoc-usage combined-doc (car ud)) combined-doc)))))
+
+(put 'el-search-pattern 'function-documentation '(el-search--make-docstring))
+
+(defmacro el-search-defpattern (name args &rest body)
+  "Like `pcase-defmacro', but limited to el-search patterns.
+The semantics is exactly that of `pcase-defmacro', but the scope
+of the definitions is limited to \"el-search\"."
+  (declare (indent 2) (debug defun))
+  `(setf (alist-get ',name el-search--pcase-macros)
+         (lambda ,args ,@body)))
+
+(el-search-defpattern string (&rest regexps)
+  "Matches any string that is matched by all REGEXPS."
+  (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-defpattern symbol (&rest regexps)
+  "Matches any symbol whose name is matched by all REGEXPS."
+  `(and (pred symbolp)
+        (app symbol-name (string ,@regexps))))
+
+(defun el-search--match-symbol-file (regexp symbol)
+  (when-let ((symbol-file (and (symbolp symbol)
+                               (symbol-file symbol))))
+    (string-match-p
+     (if (symbolp regexp) (concat "\\`" (symbol-name regexp) "\\'") regexp)
+     (file-name-sans-extension (file-name-nondirectory symbol-file)))))
+
+(el-search-defpattern source (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 that fulfills
+  (string-match-p REGEXP (file-name-sans-extension
+                           (file-name-nondirectory FILENAME)))
+
+REGEXP can also be a symbol, in which case
+
+  (concat \"^\" (symbol-name regexp) \"$\")
+
+is used as regular expression."
+  `(pred (el-search--match-symbol-file ,regexp)))
+
+(defun el-search--match-key-sequence (keys expr)
+  (when-let ((expr-keys (pcase expr
+                          ((or (pred stringp) (pred vectorp))  expr)
+                          (`(kbd ,(and (pred stringp) string)) (ignore-errors (kbd string))))))
+    (apply #'equal
+           (mapcar (lambda (keys) (ignore-errors (key-description keys)))
+                   (list keys expr-keys)))))
+
+(el-search-defpattern keys (key-sequence)
+  "Matches any description of the KEY-SEQUENCE.
+KEY-SEQUENCE is a key description in a format that Emacs
+understands.
+
+This pattern matches any description of the same key sequence.
+
+Example: the pattern
+
+    (keys (kbd \"C-s\"))
+
+matches any of these expressions:
+
+    (kbd \"C-s\")
+    [(control ?s)]
+    \"\\C-s\"
+      
+Any of these could be used as equivalent KEY-SEQUENCE in terms of
+this pattern type."
+  `(pred (el-search--match-key-sequence ,key-sequence)))
+
+(defmacro el-search--with-additional-pcase-macros (&rest body)
+  `(cl-letf ,(mapcar (pcase-lambda (`(,symbol . ,fun))
+                       `((get ',symbol 'pcase-macroexpander) #',fun))
+                     el-search--pcase-macros)
+     ,@body))
+
 (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))))))
+  (eval
+   `(el-search--with-additional-pcase-macros
+     (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))
@@ -351,7 +515,11 @@ return nil (no error)."
     match-beg))
 
 (defun el-search--do-subsexps (pos do-fun &optional ret-fun bound)
-  ;; bound -> nil means till end of buffer
+  ;; 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
@@ -396,27 +564,25 @@ return nil (no error)."
 
 (defvar el-search-keep-hl nil)
 
-(defun el-search-hl-sexp-at-point ()
-  (let ((bounds (list (point) (el-search--end-of-sexp))))
+(defun el-search-hl-sexp (&optional bounds)
+  (let ((bounds (or 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))
                    'face 'el-search-match)))
-  (add-hook 'post-command-hook (el-search-hl-post-command-fun (current-buffer)) t))
+  (add-hook 'post-command-hook #'el-search-hl-post-command-fun t t))
 
 (defun el-search-hl-remove ()
   (when (overlayp el-search-hl-overlay)
     (delete-overlay el-search-hl-overlay)))
 
-(defun el-search-hl-post-command-fun (buf)
-  (lambda ()
-    (when (buffer-live-p buf)
-      (unless (or el-search-keep-hl
-                  (eq this-command 'el-search-query-replace)
-                  (eq this-command 'el-search-pattern))
-        (with-current-buffer buf
-          (el-search-hl-remove)
-          (remove-hook 'post-command-hook #'el-search-hl-post-command-fun t))))))
+(defun el-search-hl-post-command-fun ()
+  (unless (or el-search-keep-hl
+              (eq this-command 'el-search-query-replace)
+              (eq this-command 'el-search-pattern))
+    (el-search-hl-remove)
+    (remove-hook 'post-command-hook 'el-search-hl-post-command-fun t)))
 
 
 ;;;; Core functions
@@ -429,8 +595,19 @@ return nil (no error)."
 
 ;;;###autoload
 (defun el-search-pattern (pattern)
-  "Do incremental elisp search or resume last search."
-  (interactive (list (if (eq this-command last-command)
+  "Start new or resume last elisp search.
+
+Search current buffer for expressions that are matched by `pcase'
+PATTERN.  Use `read' to transform buffer contents into
+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"
+  (interactive (list (if (and (eq this-command last-command)
+                              el-search-success)
                          el-search-current-pattern
                        (let ((pattern
                               (el-search--read-pattern "Find pcase pattern: "
@@ -446,12 +623,10 @@ return nil (no error)."
   (setq this-command 'el-search-pattern) ;in case we come from isearch
   (setq el-search-current-pattern pattern)
   (let ((opoint (point)))
-    (when (eq this-command last-command)
-      (if el-search-success
-          (el-search--skip-expression nil t)
-        ;; wrap search
-        (goto-char (point-min))))
+    (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")
@@ -460,24 +635,31 @@ return nil (no error)."
                            (ding)
                            nil))
       (setq el-search-success t)
-      (el-search-hl-sexp-at-point))))
+      (el-search-hl-sexp))))
 
-(defun el-search-search-and-replace-pattern (pattern replacement &optional mapping)
+(defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
   (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)))
     (unwind-protect
         (while (and (not done) (el-search--search-pattern pattern t))
           (setq opoint (point))
-          (unless replace-all (el-search-hl-sexp-at-point))
+          (unless replace-all (el-search-hl-sexp))
           (let* ((read-mapping (el-search--create-read-map))
                  (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))
-                 (to-insert (el-search--repair-replacement-layout
-                             (el-search--print new-expr) (append mapping read-mapping)))
+                 (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)))))
+                 (to-insert (funcall get-replacement-string))
                  (do-replace (lambda ()
                                (atomic-change-group
                                  (apply #'delete-region region)
@@ -485,8 +667,8 @@ return nil (no error)."
                                        (opoint (point)))
                                    (insert to-insert)
                                    (indent-region opoint (point))
-                                   (goto-char opoint)
-                                   (el-search-hl-sexp-at-point)))
+                                   (el-search-hl-sexp (list opoint (point)))
+                                   (goto-char opoint)))
                                (cl-incf nbr-replaced)
                                (setq replaced-this t))))
             (if replace-all
@@ -494,12 +676,14 @@ return nil (no error)."
               (while (not (pcase (if replaced-this
                                      (read-char-choice "[SPC ! q]" '(?\ ?! ?q ?n))
                                    (read-char-choice
-                                    (concat "Replace this occurence"
+                                    (concat "Replace this occurrence"
                                             (if (or (string-match-p "\n" to-insert)
                                                     (< 40 (length to-insert)))
                                                 "" (format " with `%s'" to-insert))
-                                            "? [y SPC r ! q]" )
-                                    '(?y ?n ?r ?\ ?! ?q)))
+                                            "? "
+                                            (if splice "{splice} " "")
+                                            "[y SPC r ! q]" )
+                                    '(?y ?n ?r ?\ ?! ?q ?s)))
                             (?r (funcall do-replace)
                                 nil)
                             (?y (funcall do-replace)
@@ -511,6 +695,9 @@ return nil (no error)."
                                   (funcall do-replace))
                                 (setq replace-all t)
                                 t)
+                            (?s (cl-callf not splice)
+                                (setq to-insert (funcall get-replacement-string))
+                                nil)
                             (?q (setq done t)
                                 t)))))
             (unless (or done (eobp)) (el-search--skip-expression nil t)))))
@@ -521,10 +708,6 @@ return nil (no error)."
              (if (zerop nbr-skipped)  ""
                (format "   (%d skipped)" nbr-skipped)))))
 
-;; 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: "))