From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Juri Linkov Newsgroups: gmane.emacs.bugs Subject: bug#65854: Multi-file replacement diff Date: Sun, 10 Sep 2023 20:18:47 +0300 Organization: LINKOV.NET Message-ID: <86sf7mgd54.fsf@mail.linkov.net> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="31774"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/30.0.50 (x86_64-pc-linux-gnu) To: 65854@debbugs.gnu.org Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Sun Sep 10 19:24:10 2023 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1qfOAM-00087v-5Q for geb-bug-gnu-emacs@m.gmane-mx.org; Sun, 10 Sep 2023 19:24:10 +0200 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1qfOAC-0004N5-Ce; Sun, 10 Sep 2023 13:24:00 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qfOAA-0004Mt-LO for bug-gnu-emacs@gnu.org; Sun, 10 Sep 2023 13:23:58 -0400 Original-Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1qfOAA-0004wj-Cp for bug-gnu-emacs@gnu.org; Sun, 10 Sep 2023 13:23:58 -0400 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1qfOAD-0001jU-Rw for bug-gnu-emacs@gnu.org; Sun, 10 Sep 2023 13:24:01 -0400 X-Loop: help-debbugs@gnu.org Resent-From: Juri Linkov Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Sun, 10 Sep 2023 17:24:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: report 65854 X-GNU-PR-Package: emacs X-GNU-PR-Keywords: patch X-Debbugs-Original-To: bug-gnu-emacs@gnu.org Original-Received: via spool by submit@debbugs.gnu.org id=B.16943666256631 (code B ref -1); Sun, 10 Sep 2023 17:24:01 +0000 Original-Received: (at submit) by debbugs.gnu.org; 10 Sep 2023 17:23:45 +0000 Original-Received: from localhost ([127.0.0.1]:51217 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qfO9x-0001is-0x for submit@debbugs.gnu.org; Sun, 10 Sep 2023 13:23:45 -0400 Original-Received: from lists.gnu.org ([2001:470:142::17]:36898) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1qfO9u-0001if-Oc for submit@debbugs.gnu.org; Sun, 10 Sep 2023 13:23:43 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qfO9l-0004M8-HD for bug-gnu-emacs@gnu.org; Sun, 10 Sep 2023 13:23:33 -0400 Original-Received: from relay7-d.mail.gandi.net ([2001:4b98:dc4:8::227]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1qfO9i-0004wN-D3 for bug-gnu-emacs@gnu.org; Sun, 10 Sep 2023 13:23:33 -0400 Original-Received: by mail.gandi.net (Postfix) with ESMTPSA id 7DDEC20003 for ; Sun, 10 Sep 2023 17:23:24 +0000 (UTC) X-GND-Sasl: juri@linkov.net Received-SPF: pass client-ip=2001:4b98:dc4:8::227; envelope-from=juri@linkov.net; helo=relay7-d.mail.gandi.net X-Spam_score_int: -25 X-Spam_score: -2.6 X-Spam_bar: -- X-Spam_report: (-2.6 / 5.0 requ) BAYES_00=-1.9, RCVD_IN_DNSWL_LOW=-0.7, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:269986 Archived-At: --=-=-= Content-Type: text/plain Tags: patch As discussed on emacs-devel, here is the patch that implements a standalone command that reads a list of files and replacement strings, then shows a diff to review before applying replacements. Also provided the Dired integration to show the replacement diff on marked files. Later the same function could be used to show replacement diffs from the xref buffer and maybe from other packages as well. --=-=-= Content-Type: text/x-diff Content-Disposition: inline; filename=multi-file-replace-as-diff.patch diff --git a/lisp/dired-aux.el b/lisp/dired-aux.el index 28513a2c61a..f27abc645bc 100644 --- a/lisp/dired-aux.el +++ b/lisp/dired-aux.el @@ -3655,6 +3655,20 @@ dired-do-query-replace-regexp delimited) (fileloop-continue)) +;;;###autoload +(defun dired-do-replace-regexp-as-diff (from to &optional delimited) + "Do `replace-regexp' of FROM with TO as diff, on all marked files. +Third arg DELIMITED (prefix arg) means replace only word-delimited matches." + (interactive + (let ((common + (query-replace-read-args + "Replace regexp as diff in marked files" t t))) + (list (nth 0 common) (nth 1 common) (nth 2 common)))) + (dired-post-do-command) + (multi-file-replace-regexp-as-diff + (dired-get-marked-files nil nil #'dired-nondirectory-p) + from to delimited)) + (declare-function xref-query-replace-in-results "xref") (declare-function project--files-in-directory "project") diff --git a/lisp/misearch.el b/lisp/misearch.el index 9ac28c26c48..da70d708a9e 100644 --- a/lisp/misearch.el +++ b/lisp/misearch.el @@ -387,6 +387,120 @@ multi-isearch-files-regexp (goto-char (if isearch-forward (point-min) (point-max))) (isearch-forward-regexp nil t))) + +;;; Global multi-file and multi-buffer replacements as diff + +(defun multi-file-diff-no-select (old new &optional switches buf label-old label-new) + ;; Based on `diff-no-select' tailored to multi-file diffs. + "Compare the OLD and NEW file/buffer. +If the optional SWITCHES is nil, the switches specified in the +variable `diff-switches' are passed to the diff command, +otherwise SWITCHES is used. SWITCHES can be a string or a list +of strings. BUF should be non-nil. LABEL-OLD and LABEL-NEW +specify labels to use for file names." + (unless (bufferp new) (setq new (expand-file-name new))) + (unless (bufferp old) (setq old (expand-file-name old))) + (or switches (setq switches diff-switches)) ; If not specified, use default. + (setq switches (ensure-list switches)) + (diff-check-labels) + (let* ((old-alt (diff-file-local-copy old)) + (new-alt (diff-file-local-copy new)) + (command + (mapconcat #'identity + `(,diff-command + ;; Use explicitly specified switches + ,@switches + ,@(mapcar #'shell-quote-argument + (nconc + (and (or old-alt new-alt) + (eq diff-use-labels t) + (list "--label" + (cond ((stringp label-old) label-old) + ((stringp old) old) + ((prin1-to-string old))) + "--label" + (cond ((stringp label-new) label-new) + ((stringp new) new) + ((prin1-to-string new))))) + (list (or old-alt old) + (or new-alt new))))) + " "))) + (with-current-buffer buf + (insert command "\n") + (call-process shell-file-name nil buf nil + shell-command-switch command) + (if old-alt (delete-file old-alt)) + (if new-alt (delete-file new-alt))))) + +(defun multi-file-replace-as-diff (files-or-buffers from-string replacements regexp-flag delimited-flag) + (require 'diff) + (let ((inhibit-message t) + (diff-buffer (get-buffer-create "*replace-diff*"))) + (with-current-buffer diff-buffer + (buffer-disable-undo (current-buffer)) + (let ((inhibit-read-only t)) + (erase-buffer)) + (diff-mode)) + (dolist (file-or-buffer files-or-buffers) + (let ((file-name (if (bufferp file-or-buffer) buffer-file-name file-or-buffer))) + (when file-name + (with-temp-buffer + (if (bufferp file-or-buffer) + (insert-buffer-substring file-or-buffer) + (insert-file-contents file-or-buffer)) + (goto-char (point-min)) + (perform-replace from-string replacements nil regexp-flag delimited-flag) + (multi-file-diff-no-select file-or-buffer (current-buffer) nil diff-buffer + (concat file-name "~") file-name))))) + (with-current-buffer diff-buffer + (setq-local buffer-read-only t) + (setq-local revert-buffer-function + (lambda (_ignore-auto _noconfirm) + (multi-file-replace-as-diff + files-or-buffers from-string replacements regexp-flag delimited-flag))) + (diff-setup-whitespace) + (font-lock-ensure) + (buffer-enable-undo (current-buffer)) + (goto-char (point-min))) + (pop-to-buffer diff-buffer))) + +;;;###autoload +(defun multi-file-replace-regexp-as-diff (files regexp to-string &optional delimited) + "Show replacements in FILES matching REGEXP with TO-STRING as diff. +With a prefix argument, ask for a wildcard, and replace in files +whose file names match the specified wildcard." + (interactive + (let ((files (if current-prefix-arg + (multi-isearch-read-matching-files) + (multi-isearch-read-files))) + (common + (query-replace-read-args + (concat "Replace" + (if current-prefix-arg " word" "") + " regexp as diff in files") + t t))) + (list files (nth 0 common) (nth 1 common) (nth 2 common)))) + (multi-file-replace-as-diff files regexp to-string t delimited)) + +;;;###autoload +(defun multi-buffer-replace-regexp-as-diff (buffers regexp to-string &optional delimited) + "Show replacements in file BUFFERS matching REGEXP with TO-STRING as diff. +With a prefix argument, ask for a regexp, and replace in file buffers +whose names match the specified regexp." + (interactive + (let ((buffers (if current-prefix-arg + (multi-isearch-read-matching-buffers) + (multi-isearch-read-buffers))) + (common + (query-replace-read-args + (concat "Replace" + (if current-prefix-arg " word" "") + " regexp as diff in buffers") + t t))) + (list buffers (nth 0 common) (nth 1 common) (nth 2 common)))) + (multi-file-replace-as-diff buffers regexp to-string t delimited)) + + (defvar unload-function-defs-list) (defun multi-isearch-unload-function () --=-=-=--