;; Created: 29 Jul 2015
;; Keywords: lisp
;; Compatibility: GNU Emacs 25
-;; Version: 0.1
+;; Version: 0.1.2
;; Package-Requires: ((emacs "25"))
;; any match, point is put at the beginning of the expression found
;; (unlike isearch which puts point at the end of matches).
;;
+;; Why is it based on `pcase'? Because pattern matching (and the
+;; ability to combine destructuring and condition testing) is well
+;; suited for this task. In addition, pcase allows to add specialized
+;; pattern types and to combine them with other patterns in a natural
+;; and transparent way out of the box.
+;;
;; It doesn't matter how the code is actually formatted. Comments are
;; ignored, and strings are treated as atomic objects, their contents
;; are not being searched.
;;
+;;
;; Example 1: if you enter
;;
;; 97
;;
;; Example 3:
;;
-;; I can be useful to use (guard EXP) patterns for side effects.
+;; I can be useful to use (guard EXP) patterns for side effects (note:
+;; this only works when applied to the top level expression).
;;
;; The following pattern will search for symbols defined in any
;; library whose name starts with "cl". As a side effect, it prints
;; y and n work like in isearch (meaning "yes" and "no") if you are
;; used to that.
;;
+;; It is possible to replace a match with multiple expressions using
+;; "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.
+;;
;;
;; Suggested key bindings
;; ======================
;;
;; TODO:
;;
+;; - When replacing like (progn A B C) -> A B C, the layout of the
+;; whole "group" A B C as a unit is lost. Instead of restoring layout
+;; as we do now (via "read mappings"), we could just make a backup of
+;; the original expression as a string, and use our search machinery
+;; to find occurrences in the replacement recursively.
+;;
+;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
+;;
+;; - highlight matches around point in a timer
+;;
;; - implement backward searching
;;
;; - improve docstrings
;; - 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.
(pcase-lambda (`(,symbol . ,fun))
(when-let ((doc (documentation fun)))
(insert "\n\n-- ")
- (setq doc (help-fns--signature symbol doc nil fun nil))
+ (setq doc (help-fns--signature symbol doc fun fun nil))
(insert "\n" (or doc "Not documented."))))
(reverse el-search--pcase-macros))
(let ((combined-doc (buffer-string)))
`(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))
,@body))
(defun el-search--matcher (pattern &rest body)
- (eval
+ (eval ;use `eval' to allow for user defined pattern types at run time
`(el-search--with-additional-pcase-macros
- (let ((warning-suppress-log-types '((bytecomp))))
- (byte-compile
- (lambda (expression)
- (pcase expression
- (,pattern ,@(or body (list t)))
- (_ nil))))))))
+ (let ((byte-compile-debug t) ;make undefined pattern types raise an error
+ (warning-suppress-log-types '((bytecomp)))
+ (pcase--dontwarn-upats (cons '_ pcase--dontwarn-upats)))
+ (byte-compile (lambda (expression)
+ (pcase expression
+ (,pattern ,@(or body (list t)))
+ (_ nil))))))))
(defun el-search--match-p (matcher expression)
(funcall matcher expression))
(save-excursion (insert old))))
(lambda () (buffer-substring (point-min) (point-max))))))
+(defun el-search--check-pattern-args (type args predicate &optional message)
+ "Check whether all ARGS fulfill PREDICATE.
+Raise an error if not. TYPE and optional argument MESSAGE are
+used to construct the error message."
+ (mapc (lambda (arg)
+ (unless (funcall predicate arg)
+ (error (concat "Pattern `%S': "
+ (or message (format "argument doesn't fulfill %S" predicate))
+ ": %S")
+ type arg)))
+ args))
+
+
+;;;; Additional pattern type definitions
+
+(defun el-search--split (matcher1 matcher2 list)
+ "Helper for the append pattern type.
+
+When a splitting of LIST into two lists L1, L2 exist so that Li
+is matched by MATCHERi, return (L1 L2) for such Li, else return
+nil."
+ (let ((try-match (lambda (list1 list2)
+ (when (and (el-search--match-p matcher1 list1)
+ (el-search--match-p matcher2 list2))
+ (list list1 list2))))
+ (list1 list) (list2 '()) (match nil))
+ ;; don't use recursion, this could hit `max-lisp-eval-depth'
+ (while (and (not (setq match (funcall try-match list1 list2)))
+ (consp list1))
+ (let ((last-list1 (last list1)))
+ (if-let ((cdr-last-list1 (cdr last-list1)))
+ ;; list1 is a dotted list. Then list2 must be empty.
+ (progn (setcdr last-list1 nil)
+ (setq list2 cdr-last-list1))
+ (setq list1 (butlast list1 1)
+ list2 (cons (car last-list1) list2)))))
+ match))
+
+(el-search-defpattern append (&rest patterns)
+ "Matches any list factorable into lists matched by PATTERNS in order.
+
+PATTERNS is a list of patterns P1..Pn. Match any list L for that
+lists L1..Ln exist that are matched by P1..Pn in order and L is
+equal to the concatenation of L1..Ln. Ln is allowed to be no
+list.
+
+When different ways of matching are possible, it is unspecified
+which one is chosen.
+
+Example: the pattern
+
+ (append '(1 2 3) x (app car-safe 7))
+
+matches the list (1 2 3 4 5 6 7 8 9) and binds `x' to (4 5 6)."
+ (if (null patterns)
+ '(pred null)
+ (pcase-let ((`(,pattern . ,more-patterns) patterns))
+ (cond
+ ((null more-patterns) pattern)
+ ((null (cdr more-patterns))
+ `(and (pred listp)
+ (app ,(apply-partially #'el-search--split
+ (el-search--matcher pattern)
+ (el-search--matcher (car more-patterns)))
+ (,'\` ((,'\, ,pattern)
+ (,'\, ,(car more-patterns)))))))
+ (t `(append ,pattern (append ,@more-patterns)))))))
+
+(el-search-defpattern string (&rest regexps)
+ "Matches any string that is matched by all REGEXPS."
+ (el-search--check-pattern-args 'string regexps #'stringp)
+ (let ((string (make-symbol "string"))
+ (regexp (make-symbol "regexp")))
+ `(and (pred stringp)
+ (pred (lambda (,string)
+ (cl-every
+ (lambda (,regexp) (string-match-p ,regexp ,string))
+ (list ,@regexps)))))))
+
+(el-search-defpattern symbol (&rest regexps)
+ "Matches any symbol whose name is matched by all REGEXPS."
+ (el-search--check-pattern-args 'symbol regexps #'stringp)
+ `(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."
+ (el-search--check-pattern-args 'source (list regexp) #'stringp)
+ `(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 descriptions of the KEY-SEQUENCE.
+KEY-SEQUENCE is a string or vector representing a key sequence,
+or an expression of the form (kbd STRING).
+
+Match any description of the same key sequence in any of these
+formats.
+
+Example: the pattern
+
+ (keys (kbd \"C-s\"))
+
+matches any of these expressions:
+
+ \"\\C-s\"
+ \"\C-s\"
+ (kbd \"C-s\")
+ [(control ?s)]"
+ (when (eq (car-safe key-sequence) 'kbd)
+ (setq key-sequence (kbd (cadr key-sequence))))
+ (el-search--check-pattern-args 'keys (list key-sequence) (lambda (x) (or (stringp x) (vectorp x)))
+ "argument not a string or vector")
+ `(pred (el-search--match-key-sequence ,key-sequence)))
+
;;;; Highlighting
(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))
(ding)
nil))
(setq el-search-success t)
- (el-search-hl-sexp-at-point))))
-
-(defun el-search-search-and-replace-pattern (pattern replacement &optional mapping)
+ (el-search-hl-sexp))))
+
+(defvar el-search-search-and-replace-help-string
+ "\
+y Replace this match and move to the next.
+SPC or n Skip this match and move to the next.
+r Replace this match but don't move.
+! Replace all remaining matches automatically.
+q Quit. To resume, use e.g. `repeat-complex-command'.
+? Show this help.
+s Toggle splicing mode. When splicing mode is
+ on (default off), the replacement expression must
+ evaluate to a list, and the result is spliced into the
+ buffer, instead of just inserted.
+
+Hit any key to proceed."
+ "Help string for ? in `el-search-query-replace'.")
+
+(defun el-search-search-and-replace-pattern (pattern replacement &optional mapping splice)
(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)
(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
(funcall do-replace)
(while (not (pcase (if replaced-this
- (read-char-choice "[SPC ! q]" '(?\ ?! ?q ?n))
+ (read-char-choice "[SPC ! q] (? for help)"
+ '(?\ ?! ?q ?n ??))
(read-char-choice
(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 ! s q] (? for help)" )
+ '(?y ?n ?r ?\ ?! ?q ?s ??)))
(?r (funcall do-replace)
nil)
(?y (funcall do-replace)
(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)))))
+ t)
+ (?? (ignore (read-char el-search-search-and-replace-help-string))
+ nil)))))
(unless (or done (eobp)) (el-search--skip-expression nil t)))))
(el-search-hl-remove)
(goto-char opoint)