Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eglot--apply-workspace-edit fails to rename a directory #1450

Open
adonovan opened this issue Sep 25, 2024 · 2 comments
Open

eglot--apply-workspace-edit fails to rename a directory #1450

adonovan opened this issue Sep 25, 2024 · 2 comments

Comments

@adonovan
Copy link

In Go's LSP server, an eglot-rename operation on a package name causes the server to respond with a directory rename. In other words, the workspace-edit's document-change's old and new URIs both refer to directories. This causes eglot--apply-workspace-edit to fail.

# Install gopls
$ go install golang.org/x/tools/gopls@latest

# create Go package
$ cat a/a.go 
package a

# Run Emacs with eglot
# M-x find-file a/a.go
# position over "a" in package a.
# M-x eglot-rename
# Choose new name "b".

Debugger entered--Lisp error: (wrong-type-argument stringp nil)
  expand-file-name(nil)
  find-file-noselect(nil)
  #f(compiled-function (prepared) #<bytecode -0xf6d62cc7c939f10>)(((("/Users/adonovan/w/xtools/a/a.go" [(:range (:start (:line 0 :character 8) :end (:line 0 :character 10)) :newText "b")] 11) (nil nil nil))))
  eglot--apply-workspace-edit((:documentChanges [(:textDocument (:version 11 :uri "file:///Users/adonovan/w/xtools/a/a.go") :edits [(:range (:start (:line 0 :character 8) :end (:line 0 :character 10)) :newText "b")]) (:kind "rename" :oldUri "file:///Users/adonovan/w/xtools/a" :newUri "file:///Users/adonovan/w/xtools/b")]) eglot-rename)
  eglot-rename("b")
  funcall-interactively(eglot-rename "b")
  call-interactively(eglot-rename record nil)
  command-execute(eglot-rename record)
  execute-extended-command(nil "eglot-rename" "eglot-ren")
  funcall-interactively(execute-extended-command nil "eglot-rename" "eglot-ren")
  call-interactively(execute-extended-command nil nil)
  command-execute(execute-extended-command)
@nemethf
Copy link
Collaborator

nemethf commented Sep 25, 2024

As far as I know, Eglot does not send any resourceOperations among its client capabilities, so it is a mistake from the server to send a rename workspace edit (e.g.: (:kind "rename" :oldUri ...). Maybe the client should handle the situation better, though.

https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceEditClientCapabilities

@elken
Copy link

elken commented Dec 27, 2024

This is indeed because eglot doesn't support file rename yet. I've hit the same issue on clojure-lsp, and the below patch resolves it for me. Let me know if it helps @adonovan

diff -u /var/folders/wk/_mbcwk1x7gn6bxqx97__h2c80000gp/T/buffer-content-gGFDOp /var/folders/wk/_mbcwk1x7gn6bxqx97__h2c80000gp/T/buffer-content-RYgFdZ
--- /var/folders/wk/_mbcwk1x7gn6bxqx97__h2c80000gp/T/buffer-content-gGFDOp	2024-12-27 13:35:01
+++ /var/folders/wk/_mbcwk1x7gn6bxqx97__h2c80000gp/T/buffer-content-RYgFdZ	2024-12-27 13:35:01
@@ -635,7 +635,9 @@
       (ParameterInformation (:label) (:documentation))
       (Position (:line :character))
       (Range (:start :end))
+      (RenameFile (:oldUri) (:newUri) nil)
       (Registration (:id :method) (:registerOptions))
+      (ResourceOperation (:kind))
       (ResponseError (:code :message) (:data))
       (ShowMessageParams (:type :message))
       (ShowMessageRequestParams (:type :message) (:actions))
@@ -3662,36 +3664,56 @@
 ORIGIN is a symbol designating the command that originated this
 edit proposed by the server."
   (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit
-    (let ((prepared
-           (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits)
-                     (eglot--dbind ((VersionedTextDocumentIdentifier) uri version)
-                         textDocument
-                       (list (eglot-uri-to-path uri) edits version)))
-                   documentChanges)))
-      (unless (and changes documentChanges)
-        ;; We don't want double edits, and some servers send both
-        ;; changes and documentChanges.  This unless ensures that we
-        ;; prefer documentChanges over changes.
+    (let ((prepared-text-edits '())
+          (prepared-resource-ops '()))
+      ;; Handle documentChanges for file rename
+      (when (and documentChanges
+                 (vectorp documentChanges)
+                 (= (length documentChanges) 1)
+                 (string= (plist-get (aref documentChanges 0) :kind) "rename"))
+        (let* ((rename-op (aref documentChanges 0))
+               (old-path (eglot-uri-to-path (plist-get rename-op :oldUri)))
+               (new-path (eglot-uri-to-path (plist-get rename-op :newUri))))
+          (push (list 'rename old-path new-path) prepared-resource-ops)))
+
+      ;; Handle changes for symbol updates
+      (when changes
         (cl-loop for (uri edits) on changes by #'cddr
-                 do (push (list (eglot-uri-to-path uri) edits) prepared)))
+                 do (push (list (eglot-uri-to-path uri) edits)
+                         prepared-text-edits)))
+
       (cl-flet ((notevery-visited-p ()
                   (cl-notevery #'find-buffer-visiting
-                               (mapcar #'car prepared)))
+                              (mapcar #'car prepared-text-edits)))
                 (accept-p ()
                   (y-or-n-p
-                   (format "[eglot] Server wants to edit:\n%sProceed? "
-                           (cl-loop
-                            for (f eds _) in prepared
-                            concat (format
-                                    "  %s (%d change%s)\n"
-                                    f (length eds)
-                                    (if (> (length eds) 1) "s" ""))))))
+                   (format "[eglot] Server wants to make the following changes:\n%s%sProceed? "
+                           (cl-loop for (f eds) in prepared-text-edits
+                                   concat (format "  %s (%d text change%s)\n"
+                                                f (length eds)
+                                                (if (> (length eds) 1) "s" "")))
+                           (cl-loop for (op old new) in prepared-resource-ops
+                                   concat (format "  %s: %s -> %s\n"
+                                                op old new)))))
                 (apply ()
-                  (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!"))))
-        (let ((decision (eglot--confirm-server-edits origin prepared)))
+                  ;; First apply text edits
+                  (dolist (edit prepared-text-edits)
+                    (let ((path (car edit))
+                          (edits (cadr edit)))
+                      (with-current-buffer (find-file-noselect path)
+                        (eglot--apply-text-edits edits nil))))
+                  ;; Then apply resource operations
+                  (dolist (op prepared-resource-ops)
+                    (pcase op
+                      (`(rename ,old-path ,new-path)
+                       (let ((buf (find-buffer-visiting old-path)))
+                         (when buf
+                           (with-current-buffer buf
+                             (set-visited-file-name new-path)
+                             (save-buffer)))
+                         (rename-file old-path new-path t)))))
+                  (eldoc)
+                  (eglot--message "Edit successful!")))
+        (let ((decision (eglot--confirm-server-edits origin prepared-text-edits)))
           (cond
            ((or (eq decision 'diff)
                 (and (eq decision 'maybe-diff) (notevery-visited-p)))
-            (eglot--propose-changes-as-diff prepared))
+            (eglot--propose-changes-as-diff prepared-text-edits))
            ((or (memq decision '(t summary))
                 (and (eq decision 'maybe-summary) (notevery-visited-p)))
             (when (accept-p) (apply)))
@@ -3712,11 +3734,28 @@
           nil nil nil nil
           (symbol-name (symbol-at-point)))))
   (eglot-server-capable-or-lose :renameProvider)
-  (eglot--apply-workspace-edit
-   (eglot--request (eglot--current-server-or-lose)
-                   :textDocument/rename `(,@(eglot--TextDocumentPositionParams)
-                                          :newName ,newname))
-   this-command))
+  (let* ((old-name (symbol-name (symbol-at-point)))
+         (locations (eglot--request (eglot--current-server-or-lose)
+                                    :textDocument/references
+                                    `(:context (:includeDeclaration t)
+                                      ,@(eglot--TextDocumentPositionParams))))
+         (changes nil)
+         (rename-response (eglot--request (eglot--current-server-or-lose)
+                                          :textDocument/rename
+                                          `(,@(eglot--TextDocumentPositionParams)
+                                            :newName ,newname))))
+    ;; Process references for symbol updates
+    (dotimes (i (length locations))
+      (let* ((loc (aref locations i))
+             (uri (plist-get loc :uri))
+             (range (plist-get loc :range)))
+        (push `(:range ,range :newText ,newname) (plist-get changes uri))
+        (setq changes (plist-put changes uri (plist-get changes uri)))))
+    ;; Apply both the file rename and symbol updates
+    (eglot--apply-workspace-edit 
+     `(:documentChanges ,(plist-get rename-response :documentChanges)
+       :changes ,changes)
+     this-command)))
 
 (defun eglot--code-action-bounds ()
   "Calculate appropriate bounds depending on region and point."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants