Subject: Re: bug#70820: [PATCH] Editable grep buffers

From: Visuwesh <[email-masked]>
Date: Wed, 08 May 2024 10:41:10 +0530

[செவ்வாய் மே 07, 2024] Jim Porter wrote:

> On 5/7/2024 8:12 PM, Visuwesh wrote:
>> Thanks for your input.  If I missed something from wgrep that you use,
>> please let me known.  I will play around with wgrep in some time to
>> learn more about what it offers myself.
>
> In addition to the things you mentioned, here are the most important
> features from wgrep for me:

Thanks.

> * Mark all non-result parts of the buffer (the command string, file
>   names, line numbers, etc) as read-only. This is especially valuable
>   if you want to use 'query-replace' or similar to modify the
>   results. Then you can't inadvertently edit those bits.

This is now done.

> * Fontify any results with changes (both in the grep-mode buffer and
>   the original files). This is really useful for being able to see
>   what I've changed.

I agree that it would be nice to have this in *grep* buffer but I would
like to avoid editing the file on-the-fly since it introduces typing lag
IME with occur-edit-mode.  If you want to know the edits that were made,
you can use diff-buffer-with-file so I hope leaving out the highlighting
in the file would be okay.

> * Adding all the necessary hooks/functions so other grep-like modes
>   can use this. For example, see this file from wgrep, which lets you
>   use wgrep with ag.el:
>   <https://github.com/mhayashi1120/Emacs-wgrep/blob/master/wgrep-ag.el>. (This
>   matters to me partly because I'm the author of Urgrep - a "universal
>   recursive grep" mode that can use any grep-like program to do
>   searches. I've added my own support in Urgrep for wgrep.)

With grep-edit-minor-mode, as long as the compilation-message
text-property is present at the beginning of the line, you do not need
to do anything extra.  I look at this to gain the information about the
filename and the line number.

diff --git a/lisp/progmodes/grep.el b/lisp/progmodes/grep.el
index 657349cbdff..517d1bd5ecd 100644
--- a/lisp/progmodes/grep.el
+++ b/lisp/progmodes/grep.el
@@ -31,6 +31,7 @@
 
 (eval-when-compile (require 'cl-lib))
 (require 'compile)
+(require 'track-changes)
 
 (defgroup grep nil
   "Run `grep' and display the results."
@@ -295,6 +296,8 @@ grep-mode-map
     (define-key map "}" #'compilation-next-file)
     (define-key map "\t" #'compilation-next-error)
     (define-key map [backtab] #'compilation-previous-error)
+
+    (define-key map "e" #'grep-edit-minor-mode)
     map)
   "Keymap for grep buffers.
 `compilation-minor-mode-map' is a cdr of this.")
@@ -1029,6 +1032,86 @@ grep
 		       command-args)
 		     #'grep-mode))
 
+(defvar-local grep-edit--tracker nil
+  "ID of the `track-changes' tracker.")
+
+(defun grep-edit--track-changes-signal (_id &optional _distance)
+  ;; Empty because we want to simply accumulate the changes at end
+  ;; when the user calls the finish function.
+  nil)
+
+(defun grep-edit--track-changes-finalise (beg end _before)
+  (let ((grep-buf (current-buffer))
+        (cur-res beg))                  ; Point at current grep result.
+    (save-excursion
+      (while (<= cur-res end)
+        (goto-char cur-res)
+        (let* ((change-beg (next-single-char-property-change (pos-bol) 'compilation-message))
+               ;; `1-' is required to ignore the newline.
+               (change-end (1- (next-single-char-property-change (pos-eol) 'compilation-message)))
+               (loc (compilation--message->loc
+                     (get-text-property (pos-bol) 'compilation-message)))
+               (line (compilation--loc->line loc))
+               (file (caar (compilation--loc->file-struct loc))))
+          (with-current-buffer (find-file-noselect file)
+            (save-excursion
+              (goto-char (point-min))
+              (forward-line (1- line))
+              (delete-region (pos-bol) (pos-eol))
+              (insert-buffer-substring-no-properties grep-buf change-beg change-end))))
+        (setq cur-res  (next-single-property-change cur-res 'compilation-message))))))
+
+(defun grep-edit--mark-read-only ()
+  "Mark annotations and info regions read-only."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((inhibit-read-only t)
+          match)
+      (while (setq match (text-property-search-forward 'compilation-annotation))
+        (add-text-properties (prop-match-beginning match) (prop-match-end match)
+                             '(read-only t)))
+      (goto-char (point-min))
+      (while (setq match (text-property-search-forward 'compilation-message))
+        (add-text-properties (prop-match-beginning match) (prop-match-end match)
+                             '(read-only t))))))
+
+(define-minor-mode grep-edit-minor-mode
+  "Minor mode for editing *grep* buffers.
+In this mode, changes to the *grep* buffer are applied to the
+originating files upon saving using \\[grep-save-changes]."
+  :lighter " Grep-Edit"
+  (if (null grep-edit-minor-mode)
+      (progn
+        (setq buffer-read-only t)
+        (buffer-disable-undo)
+        (use-local-map grep-mode-map))
+    (grep-edit--mark-read-only)
+    (unless grep-edit--tracker
+      (use-local-map grep-edit-minor-mode-map)
+      (setq buffer-read-only nil)
+      (buffer-enable-undo)
+      (setq grep-edit--tracker
+            (track-changes-register #'grep-edit--track-changes-signal
+                                    :disjoint t)))
+    (message (substitute-command-keys
+              "Editing: \\[grep-edit-save-changes] to return to Grep mode."))))
+
+(defun grep-edit-save-changes ()
+  "Save the changes made to the *grep* buffer."
+  (interactive)
+  (when (and grep-edit-minor-mode grep-edit--tracker)
+    (track-changes-fetch grep-edit--tracker #'grep-edit--track-changes-finalise)
+    (message "Applied edits, switching to Grep mode.")
+    (track-changes-unregister grep-edit--tracker)
+    (setq grep-edit--tracker nil)
+    (grep-edit-minor-mode -1)))
+
+(defvar grep-edit-minor-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map text-mode-map)
+    (define-key map (kbd "C-c C-c") #'grep-edit-save-changes)
+    map)
+  "Keymap for `grep-edit-minor-mode'.")
 
 ;;;###autoload
 (defun grep-find (command-args)