;; `(defvar ,_)
;;
;; you search for all defvar forms that don't specify an init value.
-;;
+;;
;; The following will search for defvar forms with a docstring whose
;; first line is longer than 70 characters:
;;
;; (define-key isearch-mode-map [(control ?%)] #'el-search-replace-from-isearch)
;;
;; The bindings in `isearch-mode-map' let you conveniently switch to
-;; elisp searching from isearch.
+;; "el-search" searching from isearch.
;;
;;
;; Bugs, Known Limitations
;;
;; the comment will be lost.
;;
+;; FIXME: when we have resumable sessions, pause and warn about this case.
+;;
;;
;; Acknowledgments
;; ===============
;;
;; TODO:
;;
-;; - detect infloops when replacing automatically (e.g. for 1 -> '(1))
-;;
;; - implement backward searching
;;
+;; - Make `el-search-pattern' accept an &optional limit, at least for
+;; the non-interactive use case?
+;;
;; - improve docstrings
;;
;; - handle more reader syntaxes, e.g. #n, #n#
case-fold-search)))
(string-match-p regexp string)))
-(defun el-search--print (expr)
- (let ((print-quoted t)
- (print-length nil)
+(defun el-search--pp-to-string (expr)
+ (let ((print-length nil)
(print-level nil))
- (prin1-to-string expr)))
+ (pp-to-string expr)))
(defvar el-search-read-expression-map
(let ((map (make-sparse-keymap)))
(read-from-minibuffer prompt initial-contents el-search-read-expression-map read
(or hist 'read-expression-history) default)))
+(defvar el-search-history '()
+ "List of input strings.")
+
(defvar el-search--initial-mb-contents nil)
(defun el-search--read-pattern (prompt &optional default read)
Don't move if already at beginning of a sexp. Point must not be
inside a string or comment. `read' the expression at that point
and return it."
+ ;; This doesn't catch end-of-buffer to keep the return value non-ambiguous
(let ((not-done t) res)
(while not-done
(let ((stop-here nil)
(looking-at-from-back (lambda (regexp n)
- (save-excursion
- (backward-char n)
- (looking-at regexp)))))
+ (and (> (point) n)
+ (save-excursion
+ (backward-char n)
+ (looking-at regexp))))))
(while (not stop-here)
(cond
((eobp) (signal 'end-of-buffer nil))
(with-temp-buffer
(emacs-lisp-mode)
(insert (if splice
- (mapconcat #'el-search--print replacement " ")
- (el-search--print replacement)))
+ (mapconcat #'el-search--pp-to-string replacement " ")
+ (el-search--pp-to-string replacement)))
(goto-char 1)
(let (start this-sexp end orig-match-start orig-match-end done)
(while (and (< (point) (point-max))
type arg)))
args))
+(defvar el-search-current-pattern nil)
+
+(defvar el-search-success nil)
+
;;;; Additional pattern type definitions
((null (cdr patterns))
(let ((pattern (car patterns)))
`(app ,(apply-partially #'el-search--contains-p (el-search--matcher pattern))
- (,'\` (t (,'\, ,pattern))))))
+ (,'\` (t (,'\, ,pattern))))))
(t `(and ,@(mapcar (lambda (pattern) `(contains ,pattern)) patterns)))))
(el-search-defpattern not (pattern)
"argument not a string or vector")
`(pred (el-search--match-key-sequence ,key-sequence)))
-(defun el-search--s (expr)
+(defun el-search--transform-nontrivial-lpat (expr)
(cond
((symbolp expr) `(or (symbol ,(symbol-name expr))
(,'\` (,'quote (,'\, (symbol ,(symbol-name expr)))))
('_ '`(,_))
('_? '(or '() `(,_))) ;FIXME: useful - document? or should we provide a (? PAT)
;thing?
- (_ `(,'\` ((,'\, ,(el-search--s elt)))))))
+ (_ `(,'\` ((,'\, ,(el-search--transform-nontrivial-lpat elt)))))))
lpats)
,@(if match-end '() '(_)))))
+(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 always tests the complete expression in the search
+buffer, it is not possible to test subexpressions calculated in
+the search pattern."
+ `(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 always tests the complete expression in the search
+buffer, it is not possible to test subexpressions calculated in
+the search pattern."
+ `(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))))))
+
+(el-search-defpattern change ()
+ "Matches the object if it is part of a change.
+This is equivalent to (char-prop diff-hl-hunk).
+
+You need `diff-hl-mode' turned on, provided by the library
+\"diff-hl\" available in Gnu Elpa."
+ (or (bound-and-true-p diff-hl-mode)
+ (error "diff-hl-mode not enabled"))
+ '(char-prop diff-hl-hunk))
+
+(el-search-defpattern changed ()
+ "Matches the object if it contains a change.
+This is equivalent to (includes-prop diff-hl-hunk).
+
+You need `diff-hl-mode' turned on, provided by the library
+\"diff-hl\" available in Gnu Elpa."
+ (or (bound-and-true-p diff-hl-mode)
+ (error "diff-hl-mode not enabled"))
+ '(includes-prop diff-hl-hunk))
+
;;;; Highlighting
;;;; Core functions
-(defvar el-search-history '()
- "List of input strings.")
-
-(defvar el-search-success nil)
-(defvar el-search-current-pattern nil)
-
;;;###autoload
-(defun el-search-pattern (pattern)
+(defun el-search-pattern (pattern &optional no-error)
"Start new or resume last elisp search.
Search current buffer for expressions that are matched by `pcase'
(error "Please don't forget the quote when searching for a symbol"))
(el-search--wrap-pattern pattern)))))
(if (not (called-interactively-p 'any))
- (el-search--search-pattern pattern)
+ (el-search--search-pattern pattern no-error)
(setq this-command 'el-search-pattern) ;in case we come from isearch
(setq el-search-current-pattern pattern)
(let ((opoint (point)))
Hit any key to proceed."
"Help string for ? in `el-search-query-replace'.")
-(defun el-search-search-and-replace-pattern (pattern replacement &optional splice to-input-string)
+(defun el-search--search-and-replace-pattern (pattern replacement &optional splice to-input-string)
(let ((replace-all nil) (nbr-replaced 0) (nbr-skipped 0) (done nil)
(el-search-keep-hl t) (opoint (point))
- (get-replacement (el-search--matcher pattern replacement)))
+ (get-replacement (el-search--matcher pattern replacement))
+ (skip-matches-in-replacement 'ask))
(unwind-protect
(while (and (not done) (el-search--search-pattern pattern t))
(setq opoint (point))
(get-replacement-string
(lambda () (el-search--format-replacement new-expr substring to-input-string splice)))
(to-insert (funcall get-replacement-string))
+ (replacement-contains-another-match
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert to-insert)
+ (goto-char 1)
+ (el-search--skip-expression new-expr)
+ (condition-case nil
+ (progn (el-search--ensure-sexp-start)
+ (el-search--search-pattern pattern t))
+ (end-of-buffer nil))))
(do-replace (lambda ()
(atomic-change-group
(apply #'delete-region region)
(funcall do-replace)
(while (not (pcase (if replaced-this
(read-char-choice "[SPC ! q] (? for help)"
- '(?\ ?! ?q ?n ??))
+ '(?\ ?! ?q ?\C-g ?n ??))
(read-char-choice
(concat "Replace this occurrence"
(if (or (string-match-p "\n" to-insert)
"? "
(if splice "{splice} " "")
"[y SPC r ! s q] (? for help)" )
- '(?y ?n ?r ?\ ?! ?q ?s ??)))
+ '(?y ?n ?r ?\ ?! ?q ?\C-g ?s ??)))
(?r (funcall do-replace)
nil)
(?y (funcall do-replace)
(?s (cl-callf not splice)
(setq to-insert (funcall get-replacement-string))
nil)
- (?q (setq done t)
- t)
+ ((or ?q ?\C-g)
+ (setq done t)
+ t)
(?? (ignore (read-char el-search-search-and-replace-help-string))
nil)))))
- (unless (or done (eobp)) (el-search--skip-expression nil t)))))
+ (unless (or done (eobp))
+ (cond
+ ((not (and replaced-this replacement-contains-another-match))
+ (el-search--skip-expression nil t))
+ ((eq skip-matches-in-replacement 'ask)
+ (if (setq skip-matches-in-replacement
+ (yes-or-no-p "Match in replacement - always skip? "))
+ (forward-sexp)
+ (el-search--skip-expression nil t)
+ (when replace-all
+ (setq replace-all nil)
+ (message "Falling back to interactive mode")
+ (sit-for 3.))))
+ (skip-matches-in-replacement (forward-sexp))
+ (t
+ (el-search--skip-expression nil t)
+ (message "Replacement contains another match%s"
+ (if replace-all " - falling back to interactive mode" ""))
+ (setq replace-all nil)
+ (sit-for 3.)))))))
(el-search-hl-remove)
(goto-char opoint)
(message "Replaced %d matches%s"
(if (zerop nbr-skipped) ""
(format " (%d skipped)" nbr-skipped)))))
-(defun el-search-query-replace-read-args ()
+(defun el-search-query-replace--read-args ()
(barf-if-buffer-read-only)
- (let* ((from (el-search--read-pattern "Replace from: "))
+ (let* ((from (el-search--read-pattern "Query replace pattern: "))
(to (let ((el-search--initial-mb-contents nil))
(el-search--read-pattern "Replace with result of evaluation of: " from))))
(list (el-search--wrap-pattern (read from)) (read to) to)))
;;;###autoload
-(defun el-search-query-replace (from to &optional to-input-string)
- "Replace some occurrences of FROM pattern with evaluated TO."
- (interactive (el-search-query-replace-read-args))
+(defun el-search-query-replace (from-pattern to-expr &optional textual-to)
+ "Replace some matches of \"el-search\" pattern FROM-PATTERN.
+
+TO-EXPR is an Elisp expression that is evaluated repeatedly for
+each match with bindings created in FROM-PATTERN in effect to
+produce a replacement expression. Operate from point
+to (point-max).
+
+As each match is found, the user must type a character saying
+what to do with it. For directions, type ? at that time."
+ (interactive (el-search-query-replace--read-args))
(setq this-command 'el-search-query-replace) ;in case we come from isearch
- (setq el-search-current-pattern from)
+ (setq el-search-current-pattern from-pattern)
(barf-if-buffer-read-only)
- (el-search-search-and-replace-pattern from to nil to-input-string))
+ (el-search--search-and-replace-pattern from-pattern to-expr nil textual-to))
(defun el-search--take-over-from-isearch (&optional goto-left-end)
(let ((other-end (and goto-left-end isearch-other-end))