diff options
| author | João Távora | 2023-09-01 00:48:25 +0100 |
|---|---|---|
| committer | João Távora | 2023-09-01 01:00:19 +0100 |
| commit | ed5ccf9da227d0a9f22ce45eff6382feb8979912 (patch) | |
| tree | 9b52f771b31f012fcf163e85fd4e0b7940014755 | |
| parent | fad48a20e665e6b5b51c417e9c04946517a2aa2f (diff) | |
| download | emacs-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.texi | 10 | ||||
| -rw-r--r-- | etc/EGLOT-NEWS | 9 | ||||
| -rw-r--r-- | lisp/progmodes/eglot.el | 121 |
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}. | |||
| 831 | The default is @code{nil}; if you want to shut down a server, use | 831 | The 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 |
| 835 | Various Eglot commands and code actions result in the language server | 835 | Various Eglot commands and code actions result in the language server |
| 836 | sending editing commands to Emacs. If this option's value is | 836 | sending editing commands to Emacs. If this option's value is |
| 837 | non-@code{nil} (the default), Eglot will ask for confirmation before | 837 | non-@code{nil}, Eglot will ask for confirmation before performing |
| 838 | performing edits initiated by the server or edits whose scope affects | 838 | edits proposed by the language server. This option's value can be |
| 839 | buffers other than the one where the user initiated the request. | 839 | crafted to require this confirmation for specific commands or only |
| 840 | when the edit affects files not yet visited by the user. Consult this | ||
| 841 | option's docstring for more information. | ||
| 840 | 842 | ||
| 841 | @item eglot-ignored-server-capabilities | 843 | @item eglot-ignored-server-capabilities |
| 842 | This variable's value is a list of language server capabilities that | 844 | This 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 | |||
| 25 | The variable 'eglot-confirm-server-edits' replaces the obsolete | ||
| 26 | 'eglot-confirm-server-initiated-edits' and brings about a new | ||
| 27 | confirmation model, making it possible to have only certain commands | ||
| 28 | require user confirmation. The type of confirmation has also been | ||
| 29 | enhanced. In particular it allows a temporary 'diff-mode' buffer to | ||
| 30 | display 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 | ||
| 25 | Some servers, like the Pyright language server, issue too many file | 34 | Some 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 | |||
| 400 | If this variable's value is the symbol `diff', a diff buffer is | ||
| 401 | popped up, allowing the user to apply each change individually. | ||
| 402 | If the symbol `summary' or any other non-nil value a short | ||
| 403 | summary of changes is presented to the user in a | ||
| 404 | minibuffer-prompt. The symbols `maybe-diff' and `maybe-summary' | ||
| 405 | are also accepted and mean that the confirmation is presented to | ||
| 406 | the user if the changes target visited files only. A nil value | ||
| 407 | means the change is applied directly to visited and non-visited | ||
| 408 | files, without any confirmation. | ||
| 409 | |||
| 410 | If this variable's value is a list, it should be an | ||
| 411 | alist ((COMMAND . ACTION) ...) where COMMAND is a symbol | ||
| 412 | designating a command, such as `eglot-rename', | ||
| 413 | `eglot-code-actions', `eglot-code-action-quickfix', etc. ACTION | ||
| 414 | is one of the symbols described above. The value `t' for COMMAND | ||
| 415 | is 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'. | ||
| 3469 | PREPARED 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. | ||
| 3495 | ORIGIN is a symbol designating the command that originated this | ||
| 3496 | edit 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." |