X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/bb98a1df932aa66a4364539708f32c34d832c70d..b09bb1b61d57fee70a6b83f576ead0fee0f329bd:/packages/el-search/el-search.el diff --git a/packages/el-search/el-search.el b/packages/el-search/el-search.el index c7b349904..eba4a5df1 100644 --- a/packages/el-search/el-search.el +++ b/packages/el-search/el-search.el @@ -65,7 +65,7 @@ ;; `(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: ;; @@ -163,7 +163,7 @@ ;; (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 @@ -185,6 +185,8 @@ ;; ;; the comment will be lost. ;; +;; FIXME: when we have resumable sessions, pause and warn about this case. +;; ;; ;; Acknowledgments ;; =============== @@ -194,10 +196,11 @@ ;; ;; 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# @@ -289,11 +292,10 @@ error." 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))) @@ -329,6 +331,9 @@ error." (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) @@ -345,13 +350,15 @@ error." 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)) @@ -510,8 +517,8 @@ return nil (no error)." (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)) @@ -580,6 +587,10 @@ MESSAGE are used to construct the error message." type arg))) args)) +(defvar el-search-current-pattern nil) + +(defvar el-search-success nil) + ;;;; Additional pattern type definitions @@ -680,7 +691,7 @@ of any kind matched by all PATTERNs are also matched. ((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) @@ -744,7 +755,7 @@ matches any of these expressions: "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))))) @@ -795,10 +806,60 @@ could use this pattern: ('_ '`(,_)) ('_? '(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 @@ -874,14 +935,8 @@ could use this pattern: ;;;; 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' @@ -908,7 +963,7 @@ The following additional pattern types are currently defined:" (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))) @@ -943,10 +998,11 @@ s Toggle splicing mode. When splicing mode is 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)) @@ -962,6 +1018,16 @@ Hit any key to proceed." (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) @@ -977,7 +1043,7 @@ Hit any key to proceed." (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) @@ -986,7 +1052,7 @@ Hit any key to proceed." "? " (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) @@ -1001,11 +1067,31 @@ Hit any key to proceed." (?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" @@ -1013,21 +1099,29 @@ Hit any key to proceed." (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))