Subject: Re: bug#63372: [PATCH] Add variable: eglot-apply-text-edits-function

From: Felician Nemeth <[email-masked]>
Date: Wed, 10 May 2023 21:35:12 +0200

João Távora <[email-masked]> writes:

>> >> 3. Eglot uses the eglot--apply-text-edits defun to apply server
>> >>    initiated edits.  There is an extension that allows the server to
>> >>    send the edits in a different format (snippet-text-edits).
>> >>
>> >>    Eglot-x puts an advise on eglot--apply-text-edits to check the format
>> >>    of the edits and act accordingly.
>> >>
>> >>    I don't know how to avoid this advice.
>> >
>> > Some hook or function variable, perhaps?  If they don't exist, perhaps
>> > they could be added?

> But then, I wonder, since most of Eglot's API is already CLOS
> based, isn't a generic function best?  A generic function can
> have :around methods, and if we follow the convention of
> passing the server as the first argument, third parties
> can make server-specific extensions.

You're right, I think.  In the attached patch, I simply changed
eglot--apply-text-edits to a cl-defgeneric and renamed it.  The patch is
straightforward, but I had to eliminate a cl-return-from call, because
generic functions don't seem to support that.

Thanks.

>From 4c5e62c901ada66a1ad59e4484cd0be17af64aa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= <[email-masked]>
Date: Wed, 10 May 2023 20:58:33 +0200
Subject: [PATCH] Eglot: Replace eglot--apply-text-edits with a public function

This allows third parties, for example, to cleanly implement the
non-standard snippet-text-edit feature.

* lisp/progmodes/eglot.el (eglot--apply-text-edits): Rename to ...
(eglot-apply-text-edits): ... this, also change to cl-defgeneric,
and relocate the code next to the other defgenerics.
(eglot--signal-textDocument/willSave, eglot-format)
(eglot-completion-at-point, eglot--apply-workspace-edit): Call
renamed function.
---
 lisp/progmodes/eglot.el | 81 +++++++++++++++++++++--------------------
 1 file changed, 42 insertions(+), 39 deletions(-)

diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 32626634b7..1c26acfe1f 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -859,6 +859,37 @@ eglot-workspace-folders
                      :name (abbreviate-file-name dir)))
              `(,(project-root project) ,@(project-external-roots project))))))
 
+(cl-defgeneric eglot-apply-text-edits (_server edits &optional version)
+  "Apply EDITS for current buffer if at VERSION, or if it's nil."
+  (when edits
+    (unless (or (not version) (equal version eglot--versioned-identifier))
+      (jsonrpc-error "Edits on `%s' require version %d, you have %d"
+                     (current-buffer) version eglot--versioned-identifier))
+    (atomic-change-group
+      (let* ((change-group (prepare-change-group))
+             (howmany (length edits))
+             (reporter (make-progress-reporter
+                        (format "[eglot] applying %s edits to `%s'..."
+                                howmany (current-buffer))
+                        0 howmany))
+             (done 0))
+        (mapc (pcase-lambda (`(,newText ,beg . ,end))
+                (let ((source (current-buffer)))
+                  (with-temp-buffer
+                    (insert newText)
+                    (let ((temp (current-buffer)))
+                      (with-current-buffer source
+                        (save-excursion
+                          (save-restriction
+                            (narrow-to-region beg end)
+                            (replace-buffer-contents temp)))
+                        (eglot--reporter-update reporter (cl-incf done)))))))
+              (mapcar (eglot--lambda ((TextEdit) range newText)
+                        (cons newText (eglot--range-region range 'markers)))
+                      (reverse edits)))
+        (undo-amalgamate-change-group change-group)
+        (progress-reporter-done reporter)))))
+
 (defclass eglot-lsp-server (jsonrpc-process-connection)
   ((project-nickname
     :documentation "Short nickname for the associated project."
@@ -2680,9 +2711,9 @@ eglot--signal-textDocument/willSave
       (jsonrpc-notify server :textDocument/willSave params))
     (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil)
       (ignore-errors
-        (eglot--apply-text-edits
-         (eglot--request server :textDocument/willSaveWaitUntil params
-                         :timeout 0.5))))))
+        (eglot-apply-text-edits
+         server (eglot--request server :textDocument/willSaveWaitUntil params
+                                :timeout 0.5))))))
 
 (defun eglot--signal-textDocument/didSave ()
   "Maybe send textDocument/didSave to server."
@@ -2945,11 +2976,13 @@ eglot-format
                    (:range ,(list :start (eglot--pos-to-lsp-position beg)
                                   :end (eglot--pos-to-lsp-position end)))))
                 (t
-                 '(:textDocument/formatting :documentFormattingProvider nil)))))
+                 '(:textDocument/formatting :documentFormattingProvider nil))))
+              (server (eglot--current-server-or-lose)))
     (eglot--server-capable-or-lose cap)
-    (eglot--apply-text-edits
+    (eglot-apply-text-edits
+     server
      (eglot--request
-      (eglot--current-server-or-lose)
+      server
       method
       (cl-list*
        :textDocument (eglot--TextDocumentIdentifier)
@@ -3172,7 +3205,7 @@ eglot-completion-at-point
                         (delete-region (- (point) (length proxy)) (point))
                         (funcall snippet-fn (or insertText label))))
                  (when (cl-plusp (length additionalTextEdits))
-                   (eglot--apply-text-edits additionalTextEdits)))
+                   (eglot-apply-text-edits server additionalTextEdits)))
                (eglot--signal-textDocument/didChange)))))))))
 
 (defun eglot--hover-info (contents &optional _range)
@@ -3351,37 +3384,6 @@ eglot-imenu
         (((SymbolInformation)) (eglot--imenu-SymbolInformation res))
         (((DocumentSymbol)) (eglot--imenu-DocumentSymbol res))))))
 
-(cl-defun eglot--apply-text-edits (edits &optional version)
-  "Apply EDITS for current buffer if at VERSION, or if it's nil."
-  (unless edits (cl-return-from eglot--apply-text-edits))
-  (unless (or (not version) (equal version eglot--versioned-identifier))
-    (jsonrpc-error "Edits on `%s' require version %d, you have %d"
-                   (current-buffer) version eglot--versioned-identifier))
-  (atomic-change-group
-    (let* ((change-group (prepare-change-group))
-           (howmany (length edits))
-           (reporter (make-progress-reporter
-                      (format "[eglot] applying %s edits to `%s'..."
-                              howmany (current-buffer))
-                      0 howmany))
-           (done 0))
-      (mapc (pcase-lambda (`(,newText ,beg . ,end))
-              (let ((source (current-buffer)))
-                (with-temp-buffer
-                  (insert newText)
-                  (let ((temp (current-buffer)))
-                    (with-current-buffer source
-                      (save-excursion
-                        (save-restriction
-                          (narrow-to-region beg end)
-                          (replace-buffer-contents temp)))
-                      (eglot--reporter-update reporter (cl-incf done)))))))
-            (mapcar (eglot--lambda ((TextEdit) range newText)
-                      (cons newText (eglot--range-region range 'markers)))
-                    (reverse edits)))
-      (undo-amalgamate-change-group change-group)
-      (progress-reporter-done reporter))))
-
 (defun eglot--apply-workspace-edit (wedit &optional confirm)
   "Apply the workspace edit WEDIT.  If CONFIRM, ask user first."
   (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit
@@ -3407,7 +3409,8 @@ eglot--apply-workspace-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))
+                    (eglot-apply-text-edits (eglot--current-server-or-lose)
+                                            edits version))
                finally (eldoc) (eglot--message "Edit successful!")))))
 
 (defun eglot-rename (newname)
-- 
2.30.2