]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/el-search/el-search.el
el-search--ensure-sexp-start: don't assume point-min==1
[gnu-emacs-elpa] / packages / el-search / el-search.el
index eba4a5df12889aba5d7c5536e354cf41010b2a33..3536f4981ac6cc13c113a1ae0a40986eb33f1a56 100644 (file)
 ;;    (define-key isearch-mode-map [(control ?S)] #'el-search-search-from-isearch)
 ;;    (define-key isearch-mode-map [(control ?%)] #'el-search-replace-from-isearch)
 ;;
+;;    (define-key el-search-read-expression-map [(control ?S)] #'exit-minibuffer)
+;;
 ;; The bindings in `isearch-mode-map' let you conveniently switch to
-;; "el-search" searching from isearch.
+;; "el-search" searching from isearch.  The binding in
+;; `el-search-read-expression-map' allows you to hit C-S twice to
+;; start a search for the last search pattern.
 ;;
 ;;
 ;; Bugs, Known Limitations
 ;;   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.
+;;
+;; - Make `el-search--format-replacement' work non-heuristically.
+;;   Idea: When replacing, for every variable V bound by the search
+;;   pattern that directly corresponds to some text T, provide some
+;;   "match data" V -> T.  Use this when formatting the replacement.
+;;   Maybe use a special marker to "paste" in expressions, like (paste
+;;   V), whereby the `paste' flag lands in the replacement and can be
+;;   replaced textually afterwards.
 
 
 
@@ -303,18 +315,20 @@ error."
     (define-key map [(control ?g)] #'abort-recursive-edit)
     (define-key map [up]   nil)
     (define-key map [down] nil)
-    (define-key map [(control meta backspace)] #'backward-kill-sexp)
-    (define-key map [(control ?S)] #'exit-minibuffer)
+    (define-key map [(control ?j)] #'newline)
     map)
   "Map for reading input with `el-search-read-expression'.")
 
 (defun el-search--setup-minibuffer ()
+  (let ((inhibit-read-only t))
+    (put-text-property 1 (minibuffer-prompt-end) 'font-lock-face 'minibuffer-prompt))
   (emacs-lisp-mode)
   (use-local-map el-search-read-expression-map)
   (setq font-lock-mode t)
   (funcall font-lock-function 1)
-  (backward-sexp)
-  (indent-sexp)
+  (goto-char (minibuffer-prompt-end))
+  (when (looking-at ".*\n")
+    (indent-sexp))
   (goto-char (point-max))
   (when-let ((this-sexp (with-current-buffer (window-buffer (minibuffer-selected-window))
                           (thing-at-point 'sexp))))
@@ -332,14 +346,34 @@ error."
                           (or hist 'read-expression-history) default)))
 
 (defvar el-search-history '()
-  "List of input strings.")
+  "List of search input strings.")
+
+(defvar el-search-query-replace-history '()
+  "List of input strings from `el-search-query-replace'.")
 
 (defvar el-search--initial-mb-contents nil)
 
-(defun el-search--read-pattern (prompt &optional default read)
+(defun el-search--pushnew-to-history (input histvar)
+  (let ((hist-head (car (symbol-value histvar))))
+    (unless (or (string-match-p "\\`\\'" input)
+                (and (stringp hist-head)
+                     (or (string= input hist-head)
+                         (ignore-errors (equal (read input) (read hist-head))))))
+      (push (if (string-match-p "\\`.+\n" input)
+                (with-temp-buffer
+                  (emacs-lisp-mode)
+                  (insert "\n" input)
+                  (indent-region 1 (point))
+                  (buffer-string))
+              input)
+            (symbol-value histvar)))))
+
+(defun el-search--read-pattern (prompt &optional default histvar)
+  (cl-callf or histvar 'el-search-history)
   (let ((input (el-search-read-expression
-                prompt el-search--initial-mb-contents 'el-search-history default read)))
-    (if (or read (not (string= input ""))) input (car el-search-history))))
+                prompt el-search--initial-mb-contents histvar default)))
+    (el-search--pushnew-to-history input histvar)
+    (if (not (string= input "")) input (car (symbol-value histvar)))))
 
 (defun el-search--end-of-sexp ()
   ;;Point must be at sexp beginning
@@ -355,7 +389,7 @@ and return it."
     (while not-done
       (let ((stop-here nil)
             (looking-at-from-back (lambda (regexp n)
-                                    (and (> (point) n)
+                                    (and (<= n (- (point) (point-min)))
                                          (save-excursion
                                            (backward-char n)
                                            (looking-at regexp))))))
@@ -502,6 +536,45 @@ point.  Optional second argument, if non-nil, means if fail just
 return nil (no error)."
   (el-search--search-pattern-1 (el-search--matcher pattern) noerror))
 
+(defun el-search--replace-hunk (region to-insert)
+  "Replace the text in REGION in current buffer with string TO-INSERT.
+Add line breaks before and after TO-INSERT when appropriate and
+reindent."
+  (atomic-change-group
+    (let* ((inhibit-message t)
+           (opoint (point))
+           (original-text (prog1 (apply #'buffer-substring-no-properties region)
+                            (goto-char (car region))
+                            (apply #'delete-region region)))
+           ;; care about other sexps in this line
+           (sexp-before-us (not (looking-back "\(\\|^\\s-*" (line-beginning-position))))
+           (sexp-after-us  (not (looking-at "\\s-*[;\)]\\|$")))
+           (insert-newline-before
+            (or
+             (and (string-match-p "\n" to-insert)
+                  (not (string-match-p "\n" original-text))
+                  (or (and sexp-before-us sexp-after-us)
+                      (looking-back
+                       (rx (or (syntax word) (syntax symbol))
+                           (+ blank)
+                           (or (syntax word) (syntax symbol))
+                           (* any))
+                       (line-beginning-position))))
+             ;; (and sexp-before-us
+             ;;      (> (+ (apply #'max (mapcar #'length (split-string to-insert "\n")))
+             ;;            (- (point) (line-beginning-position)))
+             ;;         fill-column))
+             ))
+           (insert-newline-after (and insert-newline-before sexp-after-us)))
+      (when insert-newline-before
+        (when (looking-back "\\s-+" (line-beginning-position))
+          (delete-region (match-beginning 0) (match-end 0)))
+        (insert "\n"))
+      (insert to-insert)
+      (when insert-newline-after
+        (insert "\n"))
+      (indent-region opoint (1+ (point))))))
+
 (defun el-search--format-replacement (replacement original replace-expr-input splice)
   ;; Return a printed representation of REPLACEMENT.  Try to reuse the
   ;; layout of subexpressions shared with the original (replaced)
@@ -550,26 +623,21 @@ return nil (no error)."
                           ((invalid-read-syntax end-of-buffer end-of-file) nil)))
                       (setq end (point))
                     (setq done t)))
+                ;; FIXME: there could be another occurrence of THIS-SEXP in ORIG-BUFFER with more
+                ;; subsequent equal expressions after it
                 (if orig-match-start
-                    (let ((match (with-current-buffer orig-buffer
-                                   (buffer-substring-no-properties orig-match-start
-                                                                   orig-match-end))))
-                      (delete-region start end)
-                      (goto-char start)
-                      (when (string-match-p "\n" match)
-                        (unless (looking-back "^[[:space:]\(]*" (line-beginning-position))
-                          (insert "\n"))
-                        (unless (looking-at "[[:space:]\)]*$")
-                          (insert "\n")
-                          (backward-char)))
-                      (insert match))
+                    (el-search--replace-hunk
+                     (list start end)
+                     (with-current-buffer orig-buffer
+                       (buffer-substring-no-properties orig-match-start orig-match-end)))
                   (goto-char start)
                   (el-search--skip-expression nil t))
                 (condition-case nil
                     (el-search--ensure-sexp-start)
                   (end-of-buffer (goto-char (point-max))))))
-            (delete-trailing-whitespace (point-min) (point-max)) ;FIXME: this should not be necessary
-            (let ((result (buffer-substring (point-min) (point-max))))
+            (goto-char 1)
+            (forward-sexp)
+            (let ((result (buffer-substring 1 (point))))
               (if (equal replacement (read result))
                   result
                 (error "Error in `el-search--format-replacement' - please make a bug report"))))
@@ -943,6 +1011,12 @@ Search current buffer for expressions that are matched by `pcase'
 PATTERN.  Use `read' to transform buffer contents into
 expressions.
 
+Use `emacs-lisp-mode' for reading input.  Some keys in the
+minibuffer have a special binding: to make it possible to edit
+multi line input, C-j inserts a newline, and up and down move the
+cursor vertically - see `el-search-read-expression-map' for more
+details.
+
 
 Additional `pcase' pattern types to be used with this command can
 be defined with `el-search-defpattern'.
@@ -951,16 +1025,18 @@ The following additional pattern types are currently defined:"
   (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: "
-                                                       (car el-search-history)
-                                                       t)))
+                       (let* ((input (el-search--read-pattern "Find pcase pattern: "
+                                                              (car el-search-history)))
+                              (pattern (read input)))
                          ;; 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"))
+                         ;; Make input available also in query-replace history
+                         (el-search--pushnew-to-history input 'el-search-query-replace-history)
+                         ;; and wrap the PATTERN
                          (el-search--wrap-pattern pattern)))))
   (if (not (called-interactively-p 'any))
       (el-search--search-pattern pattern no-error)
@@ -1011,12 +1087,12 @@ Hit any key to proceed."
             (unless (eq this-command last-command)
               (el-search-hl-other-matches pattern)))
           (let* ((region (list (point) (el-search--end-of-sexp)))
-                 (substring (apply #'buffer-substring-no-properties region))
-                 (expr      (read substring))
+                 (original-text (apply #'buffer-substring-no-properties region))
+                 (expr      (read original-text))
                  (replaced-this nil)
                  (new-expr  (funcall get-replacement expr))
                  (get-replacement-string
-                  (lambda () (el-search--format-replacement new-expr substring to-input-string splice)))
+                  (lambda () (el-search--format-replacement new-expr original-text to-input-string splice)))
                  (to-insert (funcall get-replacement-string))
                  (replacement-contains-another-match
                   (with-temp-buffer
@@ -1028,17 +1104,14 @@ Hit any key to proceed."
                         (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)
-                                 (let ((inhibit-message t)
-                                       (opoint (point)))
-                                   (insert to-insert)
-                                   (indent-region opoint (point))
-                                   (el-search-hl-sexp (list opoint (point)))
-                                   (goto-char opoint)))
-                               (cl-incf nbr-replaced)
-                               (setq replaced-this t))))
+                 (do-replace
+                  (lambda ()
+                    (save-excursion
+                      (el-search--replace-hunk (list (point) (el-search--end-of-sexp)) to-insert))
+                    (el-search--ensure-sexp-start) ;skip potentially newly added whitespace
+                    (el-search-hl-sexp (list opoint (point)))
+                    (cl-incf nbr-replaced)
+                    (setq replaced-this t))))
             (if replace-all
                 (funcall do-replace)
               (while (not (pcase (if replaced-this
@@ -1091,7 +1164,7 @@ Hit any key to proceed."
                 (message "Replacement contains another match%s"
                          (if replace-all " - falling back to interactive mode" ""))
                 (setq replace-all nil)
-                (sit-for 3.)))))))
+                (sit-for 2.)))))))
     (el-search-hl-remove)
     (goto-char opoint)
     (message "Replaced %d matches%s"
@@ -1101,9 +1174,47 @@ Hit any key to proceed."
 
 (defun el-search-query-replace--read-args ()
   (barf-if-buffer-read-only)
-  (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))))
+  (let ((from-input (let ((el-search--initial-mb-contents
+                           (or el-search--initial-mb-contents
+                               (and (eq last-command 'el-search-pattern)
+                                    (car el-search-history)))))
+                      (el-search--read-pattern "Query replace pattern: " nil
+                                               'el-search-query-replace-history)))
+        from to)
+    (with-temp-buffer
+      (emacs-lisp-mode)
+      (insert from-input)
+      (goto-char 1)
+      (forward-sexp)
+      (skip-chars-forward " \t\n\f")
+      ;; FIXME: maybe more sanity tests here...
+      (if (not (looking-at "->"))
+          (setq from from-input
+                to (let ((el-search--initial-mb-contents nil))
+                     (el-search--read-pattern "Replace with result of evaluation of: " from)))
+        (delete-char 2)
+        (goto-char 1)
+        (forward-sexp)
+        (setq from (buffer-substring 1 (point)))
+        (skip-chars-forward " \t\n\f")
+        (setq to (buffer-substring (point) (progn (forward-sexp) (point))))))
+    (unless (and el-search-query-replace-history
+                 (not (string= from from-input))
+                 (string= from-input (car el-search-query-replace-history)))
+      (push (with-temp-buffer
+              (emacs-lisp-mode)
+              (insert (let ((newline-in-from (string-match-p "\n" from))
+                            (newline-in-to   (string-match-p "\n" to)))
+                        (format "%s%s%s ->%s%s"
+                                (if (and (or newline-in-from newline-in-to)
+                                         (not (string-match-p "\\`\n" from))) "\n" "")
+                                (if     newline-in-from                       "\n" "" ) from
+                                (if (and (or newline-in-from newline-in-to)
+                                         (not (string-match-p "\\`\n" to)))   "\n" " ") to)))
+              (indent-region 1 (point-max))
+              (buffer-string))
+            el-search-query-replace-history))
+    (el-search--pushnew-to-history from 'el-search-history)
     (list (el-search--wrap-pattern (read from)) (read to) to)))
 
 ;;;###autoload
@@ -1116,7 +1227,15 @@ 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."
+what to do with it.  For directions, type ? at that time.
+
+As an alternative to enter FROM-PATTERN and TO-EXPR separately,
+you can also give an input of the form
+
+   FROM-PATTERN -> TO-EXPR
+
+to the first prompt and specify both expressions at once.  This
+format is also used for history entries."
   (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-pattern)