aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2023-09-01 00:48:25 +0100
committerJoão Távora2023-09-01 01:00:19 +0100
commited5ccf9da227d0a9f22ce45eff6382feb8979912 (patch)
tree9b52f771b31f012fcf163e85fd4e0b7940014755
parentfad48a20e665e6b5b51c417e9c04946517a2aa2f (diff)
downloademacs-ed5ccf9da227d0a9f22ce45eff6382feb8979912.tar.gz
emacs-ed5ccf9da227d0a9f22ce45eff6382feb8979912.zip
Eglot: revamp confirmation model for server-proposed edits
bug#60338 The variable 'eglot-confirm-server-edits' replaces the obsolete 'eglot-confirm-server-initiated-edits' and brings about a new confirmation model, making it possible to have only certain commands require user confirmation. This was achieved careful usage of the 'this-command' and 'last-command' variables. There are now two types of confirmation: the usual minibuffer summary and a temporary 'diff-mode' buffer to display the proposed changes, so the user can apply them one by one. Thanks to Philip Kaludercic for the diff-mode idea and implementation. Co-authored-by: Philip Kaludercic <philipk@posteo.net> * doc/misc/eglot.texi (Eglot Variables): Describe 'eglot-confirm-server-edits'. * etc/EGLOT-NEWS (latest): Mention change. * lisp/progmodes/eglot.el (diff-mode): Require it. (eglot-confirm-server-initiated-edits): Obsolete it. (eglot-confirm-server-edits): New variable. (eglot-handle-request workspace/applyEdit): Use 'last-command' (eglot-execute t t): Use 'this-command'. (eglot--apply-workspace-edit): Rework. (eglot-rename): Use 'this-command'.
-rw-r--r--doc/misc/eglot.texi10
-rw-r--r--etc/EGLOT-NEWS9
-rw-r--r--lisp/progmodes/eglot.el121
3 files changed, 115 insertions, 25 deletions
diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi
index 6eb212ca841..3338756c63c 100644
--- a/doc/misc/eglot.texi
+++ b/doc/misc/eglot.texi
@@ -831,12 +831,14 @@ last buffer managed by it is killed. @xref{Shutting Down LSP Servers}.
831The default is @code{nil}; if you want to shut down a server, use 831The default is @code{nil}; if you want to shut down a server, use
832@kbd{M-x eglot-shutdown} (@pxref{Eglot Commands}). 832@kbd{M-x eglot-shutdown} (@pxref{Eglot Commands}).
833 833
834@item eglot-confirm-server-initiated-edits 834@item eglot-confirm-server-edits
835Various Eglot commands and code actions result in the language server 835Various Eglot commands and code actions result in the language server
836sending editing commands to Emacs. If this option's value is 836sending editing commands to Emacs. If this option's value is
837non-@code{nil} (the default), Eglot will ask for confirmation before 837non-@code{nil}, Eglot will ask for confirmation before performing
838performing edits initiated by the server or edits whose scope affects 838edits proposed by the language server. This option's value can be
839buffers other than the one where the user initiated the request. 839crafted to require this confirmation for specific commands or only
840when the edit affects files not yet visited by the user. Consult this
841option's docstring for more information.
840 842
841@item eglot-ignored-server-capabilities 843@item eglot-ignored-server-capabilities
842This variable's value is a list of language server capabilities that 844This variable's value is a list of language server capabilities that
diff --git a/etc/EGLOT-NEWS b/etc/EGLOT-NEWS
index 01f0498eb81..980b06030e8 100644
--- a/etc/EGLOT-NEWS
+++ b/etc/EGLOT-NEWS
@@ -20,6 +20,15 @@ https://github.com/joaotavora/eglot/issues/1234.
20 20
21* Changes in upcoming Eglot 21* Changes in upcoming Eglot
22 22
23** Diff previews of edits and new variable 'eglot-confirm-server-edits'
24
25The variable 'eglot-confirm-server-edits' replaces the obsolete
26'eglot-confirm-server-initiated-edits' and brings about a new
27confirmation model, making it possible to have only certain commands
28require user confirmation. The type of confirmation has also been
29enhanced. In particular it allows a temporary 'diff-mode' buffer to
30display the proposed changes, so the user can apply them one by one.
31
23** Optimized file-watching capability 32** Optimized file-watching capability
24 33
25Some servers, like the Pyright language server, issue too many file 34Some servers, like the Pyright language server, issue too many file
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 6f08f26857b..8d95019c3ed 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -108,6 +108,8 @@
108(require 'filenotify) 108(require 'filenotify)
109(require 'ert) 109(require 'ert)
110(require 'text-property-search nil t) 110(require 'text-property-search nil t)
111(require 'diff-mode)
112(require 'diff)
111 113
112;; These dependencies are also GNU ELPA core packages. Because of 114;; These dependencies are also GNU ELPA core packages. Because of
113;; bug#62576, since there is a risk that M-x package-install, despite 115;; bug#62576, since there is a risk that M-x package-install, despite
@@ -388,10 +390,35 @@ done by `eglot-reconnect'."
388 :type '(choice (const :tag "No limit" nil) 390 :type '(choice (const :tag "No limit" nil)
389 (integer :tag "Number of characters"))) 391 (integer :tag "Number of characters")))
390 392
391(defcustom eglot-confirm-server-initiated-edits 'confirm 393(define-obsolete-variable-alias 'eglot-confirm-server-initiated-edits
392 "Non-nil if server-initiated edits should be confirmed with user." 394 'eglot-confirm-server-edits "1.16")
393 :type '(choice (const :tag "Don't show confirmation prompt" nil) 395
394 (const :tag "Show confirmation prompt" confirm))) 396(defcustom eglot-confirm-server-edits '((eglot-rename . nil)
397 (t . maybe-summary))
398 "Control if changes proposed by LSP should be confirmed with user.
399
400If this variable's value is the symbol `diff', a diff buffer is
401popped up, allowing the user to apply each change individually.
402If the symbol `summary' or any other non-nil value a short
403summary of changes is presented to the user in a
404minibuffer-prompt. The symbols `maybe-diff' and `maybe-summary'
405are also accepted and mean that the confirmation is presented to
406the user if the changes target visited files only. A nil value
407means the change is applied directly to visited and non-visited
408files, without any confirmation.
409
410If this variable's value is a list, it should be an
411alist ((COMMAND . ACTION) ...) where COMMAND is a symbol
412designating a command, such as `eglot-rename',
413`eglot-code-actions', `eglot-code-action-quickfix', etc. ACTION
414is one of the symbols described above. The value `t' for COMMAND
415is accepted and its ACTION is the default value."
416 :type '(choice (const :tag "Use diff" diff)
417 (const :tag "Summarize and prompt" summary)
418 (const :tag "Maybe use diff" maybe-diff)
419 (const :tag "Maybe summarize and prompt" maybe-summary)
420 (const :tag "Don't confirm" nil)
421 (alist :tag "Per-command alist")))
395 422
396(defcustom eglot-extend-to-xref nil 423(defcustom eglot-extend-to-xref nil
397 "If non-nil, activate Eglot in cross-referenced non-project files." 424 "If non-nil, activate Eglot in cross-referenced non-project files."
@@ -753,7 +780,7 @@ ACTION is an LSP object of either `CodeAction' or `Command' type."
753 (if (and (null edit) (null command) data 780 (if (and (null edit) (null command) data
754 (eglot--server-capable :codeActionProvider :resolveProvider)) 781 (eglot--server-capable :codeActionProvider :resolveProvider))
755 (eglot-execute server (eglot--request server :codeAction/resolve action)) 782 (eglot-execute server (eglot--request server :codeAction/resolve action))
756 (when edit (eglot--apply-workspace-edit edit)) 783 (when edit (eglot--apply-workspace-edit edit this-command))
757 (when command (eglot--request server :workspace/executeCommand command))))))) 784 (when command (eglot--request server :workspace/executeCommand command)))))))
758 785
759(cl-defgeneric eglot-initialization-options (server) 786(cl-defgeneric eglot-initialization-options (server)
@@ -2379,7 +2406,7 @@ THINGS are either registrations or unregisterations (sic)."
2379(cl-defmethod eglot-handle-request 2406(cl-defmethod eglot-handle-request
2380 (_server (_method (eql workspace/applyEdit)) &key _label edit) 2407 (_server (_method (eql workspace/applyEdit)) &key _label edit)
2381 "Handle server request workspace/applyEdit." 2408 "Handle server request workspace/applyEdit."
2382 (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits) 2409 (eglot--apply-workspace-edit edit last-command)
2383 `(:applied t)) 2410 `(:applied t))
2384 2411
2385(cl-defmethod eglot-handle-request 2412(cl-defmethod eglot-handle-request
@@ -3431,8 +3458,42 @@ If SILENT, don't echo progress in mode-line."
3431 (when reporter 3458 (when reporter
3432 (progress-reporter-done reporter))))) 3459 (progress-reporter-done reporter)))))
3433 3460
3434(defun eglot--apply-workspace-edit (wedit &optional confirm) 3461(defun eglot--confirm-server-edits (origin _prepared)
3435 "Apply the workspace edit WEDIT. If CONFIRM, ask user first." 3462 (let (v)
3463 (cond ((symbolp eglot-confirm-server-edits) eglot-confirm-server-edits)
3464 ((setq v (assoc origin eglot-confirm-server-edits)) (cdr v))
3465 ((setq v (assoc t eglot-confirm-server-edits)) (cdr v)))))
3466
3467(defun eglot--propose-changes-as-diff (prepared)
3468 "Helper for `eglot--apply-workspace-edit'.
3469PREPARED is a list ((FILENAME EDITS VERSION)...)."
3470 (with-current-buffer (get-buffer-create "*EGLOT proposed server changes*")
3471 (buffer-disable-undo (current-buffer))
3472 (let ((buffer-read-only t))
3473 (diff-mode))
3474 (let ((inhibit-read-only t)
3475 (target (current-buffer)))
3476 (erase-buffer)
3477 (pcase-dolist (`(,path ,edits ,_) prepared)
3478 (with-temp-buffer
3479 (let ((diff (current-buffer)))
3480 (with-temp-buffer
3481 (insert-file-contents path)
3482 (eglot--apply-text-edits edits)
3483 (diff-no-select path (current-buffer)
3484 nil t diff))
3485 (with-current-buffer target
3486 (insert-buffer-substring diff))))))
3487 (setq-local buffer-read-only t)
3488 (buffer-enable-undo (current-buffer))
3489 (goto-char (point-min))
3490 (pop-to-buffer (current-buffer))
3491 (font-lock-ensure)))
3492
3493(defun eglot--apply-workspace-edit (wedit origin)
3494 "Apply the workspace edit WEDIT.
3495ORIGIN is a symbol designating the command that originated this
3496edit proposed by the server."
3436 (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit 3497 (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit
3437 (let ((prepared 3498 (let ((prepared
3438 (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) 3499 (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits)
@@ -3446,18 +3507,36 @@ If SILENT, don't echo progress in mode-line."
3446 ;; prefer documentChanges over changes. 3507 ;; prefer documentChanges over changes.
3447 (cl-loop for (uri edits) on changes by #'cddr 3508 (cl-loop for (uri edits) on changes by #'cddr
3448 do (push (list (eglot--uri-to-path uri) edits) prepared))) 3509 do (push (list (eglot--uri-to-path uri) edits) prepared)))
3449 (if (or confirm 3510 (cl-flet ((notevery-visited-p ()
3450 (cl-notevery #'find-buffer-visiting 3511 (cl-notevery #'find-buffer-visiting
3451 (mapcar #'car prepared))) 3512 (mapcar #'car prepared)))
3452 (unless (y-or-n-p 3513 (prompt ()
3453 (format "[eglot] Server wants to edit:\n %s\n Proceed? " 3514 (unless (y-or-n-p
3454 (mapconcat #'identity (mapcar #'car prepared) "\n "))) 3515 (format "[eglot] Server wants to edit:\n%sProceed? "
3455 (jsonrpc-error "User canceled server edit"))) 3516 (cl-loop
3456 (cl-loop for edit in prepared 3517 for (f eds _) in prepared
3457 for (path edits version) = edit 3518 concat (format
3458 do (with-current-buffer (find-file-noselect path) 3519 " %s (%d change%s)\n"
3459 (eglot--apply-text-edits edits version)) 3520 f (length eds)
3460 finally (eldoc) (eglot--message "Edit successful!"))))) 3521 (if (> (length eds) 1) "s" "")))))
3522 (jsonrpc-error "User canceled server edit")))
3523 (apply ()
3524 (cl-loop for edit in prepared
3525 for (path edits version) = edit
3526 do (with-current-buffer (find-file-noselect path)
3527 (eglot--apply-text-edits edits version))
3528 finally (eldoc) (eglot--message "Edit successful!"))))
3529 (let ((decision (eglot--confirm-server-edits origin prepared)))
3530 (cond
3531 ((or (eq decision 'diff)
3532 (and (eq decision 'maybe-diff) (notevery-visited-p)))
3533 (eglot--propose-changes-as-diff prepared))
3534 ((or (eq decision 'summary)
3535 (and (eq decision 'maybe-summary) (notevery-visited-p)))
3536 (prompt)
3537 (apply))
3538 (t
3539 (apply))))))))
3461 3540
3462(defun eglot-rename (newname) 3541(defun eglot-rename (newname)
3463 "Rename the current symbol to NEWNAME." 3542 "Rename the current symbol to NEWNAME."
@@ -3472,7 +3551,7 @@ If SILENT, don't echo progress in mode-line."
3472 (eglot--request (eglot--current-server-or-lose) 3551 (eglot--request (eglot--current-server-or-lose)
3473 :textDocument/rename `(,@(eglot--TextDocumentPositionParams) 3552 :textDocument/rename `(,@(eglot--TextDocumentPositionParams)
3474 :newName ,newname)) 3553 :newName ,newname))
3475 current-prefix-arg)) 3554 this-command))
3476 3555
3477(defun eglot--region-bounds () 3556(defun eglot--region-bounds ()
3478 "Region bounds if active, else bounds of things at point." 3557 "Region bounds if active, else bounds of things at point."