Subject: [PATCH] Add option to present server changes as diffs

From: Philip Kaludercic <[email-masked]>
Date: Mon, 26 Dec 2022 13:42:04 +0000

X-CC-Debbugs: João Távora <[email-masked]>

I'd like to propose adding an option that makes server modifications by
Eglot less invasive.  The current behaviour is to make the changes
directly in a buffer and open the remaining files to make the
modifications in those as well (?).  If `eglot-use-diffs' is enabled,
all confirmations are prepared as patches in a pop-up buffer that the
user can review and apply at will.  To my knowledge there is no general
`diff-apply-hunk' that will apply all the changes from a buffer, but
that is a separate issue that can be fixed in a separate patch.

(Note, I'm still testing emacs-29, so the patch was developed on that
branch.  But it should be applied to master)

>From 88ba620b47b7987d203fc5b42716fa08be3b1d8f Mon Sep 17 00:00:00 2001
From: Philip Kaludercic <[email-masked]>
Date: Mon, 26 Dec 2022 11:06:09 +0100
Subject: [PATCH] Add option to present server changes as diffs

* lisp/progmodes/eglot.el (eglot-use-diffs): Add new user option.
(eglot--apply-workspace-edit): Respect 'eglot-use-diffs'.
* doc/misc/eglot.texi (Customizing Eglot): Document 'eglot-use-diffs'.
---
 doc/misc/eglot.texi     |  7 ++++++
 lisp/progmodes/eglot.el | 54 ++++++++++++++++++++++++++++++++---------
 2 files changed, 49 insertions(+), 12 deletions(-)

diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi
index 2aff038b9a..9150e1a879 100644
--- a/doc/misc/eglot.texi
+++ b/doc/misc/eglot.texi
@@ -955,6 +955,13 @@ Customizing Eglot
 to use Eglot in your @code{eglot-managed-mode-hook} or via some other
 mechanism.
 
+@vindex eglot-use-diffs
+@item eglot-use-diffs
+If this option is enabled, any server modifications (renames, code
+actions, refactoring, @dots{}) will be presented as diffs that the
+user can selectively apply.  The default is that all modifications are
+made in place.
+
 @vindex eglot-report-progress
 @cindex progress
 @item eglot-report-progress
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 15cb1b6fad..74472b25ce 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -111,6 +111,8 @@
 (require 'ert)
 (require 'array)
 (require 'external-completion)
+(require 'diff-mode)
+(require 'diff)
 
 ;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are
 ;; using the latest version from GNU Elpa when we load eglot.el.  Use an
@@ -3160,6 +3162,11 @@ eglot--apply-text-edits
       (undo-amalgamate-change-group change-group)
       (progress-reporter-done reporter))))
 
+(defcustom eglot-use-diffs nil
+  "Non-nil means that server changes are presented as diffs."
+  :type 'boolean
+  :version "30.1")
+
 (defun eglot--apply-workspace-edit (wedit &optional confirm)
   "Apply the workspace edit WEDIT.  If CONFIRM, ask user first."
   (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit
@@ -3175,18 +3182,41 @@ eglot--apply-workspace-edit
         ;; prefer documentChanges over changes.
         (cl-loop for (uri edits) on changes by #'cddr
                  do (push (list (eglot--uri-to-path uri) edits) prepared)))
-      (if (or confirm
-              (cl-notevery #'find-buffer-visiting
-                           (mapcar #'car prepared)))
-          (unless (y-or-n-p
-                   (format "[eglot] Server wants to edit:\n  %s\n Proceed? "
-                           (mapconcat #'identity (mapcar #'car prepared) "\n  ")))
-            (jsonrpc-error "User canceled server edit")))
-      (cl-loop for edit in prepared
-               for (path edits version) = edit
-               do (with-current-buffer (find-file-noselect path)
-                    (eglot--apply-text-edits edits version))
-               finally (eldoc) (eglot--message "Edit successful!")))))
+      (if eglot-use-diffs
+          (with-current-buffer (get-buffer-create " *Server Changes*")
+            (buffer-disable-undo (current-buffer))
+            (let ((buffer-read-only t))
+              (diff-mode))
+            (let ((inhibit-read-only t)
+                  (target (current-buffer)))
+              (erase-buffer)
+              (pcase-dolist (`(,path ,edits ,_) prepared)
+                (with-temp-buffer
+                  (let ((diff (current-buffer)))
+                    (with-temp-buffer
+                      (insert-file-contents path)
+                      (eglot--apply-text-edits edits)
+                      (diff-no-select path (current-buffer)
+                                      nil t diff))
+                    (with-current-buffer target
+                      (insert-buffer-substring diff))))))
+            (setq-local buffer-read-only t)
+            (buffer-enable-undo (current-buffer))
+            (goto-char (point-min))
+            (pop-to-buffer (current-buffer))
+            (font-lock-ensure))
+        (if (or confirm
+                (cl-notevery #'find-buffer-visiting
+                             (mapcar #'car prepared)))
+            (unless (y-or-n-p
+                     (format "[eglot] Server wants to edit:\n  %s\n Proceed? "
+                             (mapconcat #'identity (mapcar #'car prepared) "\n  ")))
+              (jsonrpc-error "User canceled server edit")))
+        (cl-loop for edit in prepared
+                 for (path edits version) = edit
+                 do (with-current-buffer (find-file-noselect path)
+                      (eglot--apply-text-edits edits version))
+                 finally (eldoc) (eglot--message "Edit successful!"))))))
 
 (defun eglot-rename (newname)
   "Rename the current symbol to NEWNAME."
-- 
2.35.1