Eglot: suggest code actions at point
* lisp/progmodes/eglot.el (eglot-code-action-indicator-face): New face. (eglot-code-action-indications, eglot-code-action-indicator): New defcustoms. (eglot--highlights): Move up here. (eglot--managed-mode): Rework. (eglot--server-menu-map, eglot--main-menu-map): Extract maps into variables (avoids odd mode-line bug). (eglot--mode-line-format): Rework. (eglot--code-action-params): New helper. (eglot-code-actions): Rework. (eglot--read-execute-code-action): Tweak. (eglot-code-action-suggestion): New function. * etc/EGLOT-NEWS: Mention new feature. * doc/misc/eglot.texi (Eglot Features): Mention new feature. (Customization Variables): Mention new variables.
This commit is contained in:
parent
7f0ef9655c
commit
d6a502fc7a
3 changed files with 200 additions and 35 deletions
|
@ -438,6 +438,13 @@ Enhanced completion of symbol at point by the @code{completion-at-point}
|
|||
command (@pxref{Symbol Completion,,, emacs, GNU Emacs Manual}). This
|
||||
uses the language-server's parser data for the completion candidates.
|
||||
|
||||
@item
|
||||
Server-suggested code refactorings. The ElDoc package is also leveraged
|
||||
to retrieve so-called @dfn{code actions} nearby point. When such
|
||||
suggestions are available they are annotated with a special indication
|
||||
and can be easily invoked by the user with the @code{eglot-code-action}
|
||||
command (@pxref{Eglot Commands}).
|
||||
|
||||
@item
|
||||
On-the-fly succinct informative annotations, so-called @dfn{inlay
|
||||
hints}. Eglot adds special intangible text nearby certain identifiers,
|
||||
|
@ -926,6 +933,31 @@ Setting this variable to true causes Eglot to send special cancellation
|
|||
notification for certain stale client request. This may help some LSP
|
||||
servers avoid doing costly but ultimately useless work on behalf of the
|
||||
client, improving overall performance.
|
||||
|
||||
@item eglot-code-action-indications
|
||||
This variable controls the indication of code actions available at
|
||||
point. Value is a list of symbols, more than one can be specified:
|
||||
|
||||
@itemize @minus
|
||||
@item
|
||||
@code{eldoc-hint}: ElDoc is used to hint about at-point actions.
|
||||
@item
|
||||
@code{margin}: A special indicator appears in the margin of the line
|
||||
that point is currently on. This indicator is not interactive (you
|
||||
cannot click on it with the mouse).
|
||||
@item
|
||||
@code{nearby}: An interactive special indicator appears near point.
|
||||
@item
|
||||
@code{mode-line}: An interactive special indicator appears in the mode
|
||||
line.
|
||||
@end itemize
|
||||
|
||||
@code{margin} and @code{nearby} are incompatible. If the list is empty,
|
||||
ElDoc will not hint about at-point actions.
|
||||
|
||||
@item eglot-code-action-indicator
|
||||
This variable is a string determining what the special indicator looks
|
||||
like.
|
||||
@end vtable
|
||||
|
||||
@node Other Variables
|
||||
|
@ -1004,6 +1036,8 @@ about an identifier.
|
|||
signature information.
|
||||
@item @code{eglot-highlight-eldoc-function}, to highlight nearby
|
||||
manifestations of an identifier.
|
||||
@item @code{eglot-code-action-suggestion}, to retrieve relevant code
|
||||
actions at point.
|
||||
@end itemize
|
||||
|
||||
A simple tweak to remove at-point identifier information for
|
||||
|
|
|
@ -26,6 +26,14 @@ Tweaking this variable may help some LSP servers avoid doing costly but
|
|||
ultimately useless work on behalf of the client, improving overall
|
||||
performance.
|
||||
|
||||
** Suggests code actions at point
|
||||
|
||||
A commonly requested feature, Eglot will use ElDoc to ask the server for
|
||||
code actions available at point, indicating to the user, who may use
|
||||
execute them quickly via the usual 'eglot-code-actions' command.
|
||||
Customize with 'eglot-code-action-indications' and
|
||||
'eglot-code-action-indicator'.
|
||||
|
||||
|
||||
* Changes in Eglot 1.18 (20/1/2025)
|
||||
|
||||
|
|
|
@ -579,6 +579,45 @@ notification is implementation defined, and is only useful for some
|
|||
servers."
|
||||
:type 'boolean)
|
||||
|
||||
(defface eglot-code-action-indicator-face
|
||||
'((t (:inherit font-lock-escape-face :weight bold)))
|
||||
"Face used for code action suggestions.")
|
||||
|
||||
(defcustom eglot-code-action-indications
|
||||
'(eldoc-hint mode-line margin)
|
||||
"How Eglot indicates there's are code actions available at point.
|
||||
Value is a list of symbols, more than one can be specified:
|
||||
|
||||
- `eldoc-hint': ElDoc is used to hint about at-point actions.
|
||||
- `margin': A special indicator appears in the margin.
|
||||
- `nearby': A special indicator appears near point.
|
||||
- `mode-line': A special indicator appears in the mode-line.
|
||||
|
||||
`margin' and `nearby' are incompatible. `margin's indicator is not
|
||||
interactive. If the list is empty, Eglot will not hint about code
|
||||
actions at point."
|
||||
:type '(set
|
||||
:tag "Tick the ones you're interested in"
|
||||
(const :tag "ElDoc textual hint" eldoc-hint)
|
||||
(const :tag "Right besides point" nearby)
|
||||
(const :tag "In mode line" mode-line)
|
||||
(const :tag "In margin" margin))
|
||||
:package-version '(Eglot . "1.19"))
|
||||
|
||||
(defcustom eglot-code-action-indicator
|
||||
(cl-loop for c in '(? ?⚡?✓ ?α ??)
|
||||
when (char-displayable-p c)
|
||||
return (make-string 1 c))
|
||||
"Indicator string for code action suggestions."
|
||||
:type (let ((basic-choices
|
||||
(cl-loop for c in '(? ?⚡?✓ ?α ??)
|
||||
when (char-displayable-p c)
|
||||
collect `(const :tag ,(format "Use `%c'" c)
|
||||
,(make-string 1 c)))))
|
||||
`(choice ,@basic-choices
|
||||
(string :tag "Specify your own")))
|
||||
:package-version '(Eglot . "1.19"))
|
||||
|
||||
(defvar eglot-withhold-process-id nil
|
||||
"If non-nil, Eglot will not send the Emacs process id to the language server.
|
||||
This can be useful when using docker to run a language server.")
|
||||
|
@ -2015,6 +2054,11 @@ For example, to keep your Company customization, add the symbol
|
|||
"A hook run by Eglot after it started/stopped managing a buffer.
|
||||
Use `eglot-managed-p' to determine if current buffer is managed.")
|
||||
|
||||
(defvar eglot--highlights nil "Overlays for `eglot-highlight-eldoc-function'.")
|
||||
|
||||
(defvar-local eglot--suggestion-overlay (make-overlay 0 0)
|
||||
"Overlay for `eglot-code-action-suggestion'.")
|
||||
|
||||
(define-minor-mode eglot--managed-mode
|
||||
"Mode for source buffers managed by some Eglot project."
|
||||
:init-value nil :lighter nil :keymap eglot-mode-map :interactive nil
|
||||
|
@ -2056,15 +2100,16 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
|
|||
#'eglot-imenu))
|
||||
(unless (eglot--stay-out-of-p 'flymake) (flymake-mode 1))
|
||||
(unless (eglot--stay-out-of-p 'eldoc)
|
||||
(add-hook 'eldoc-documentation-functions #'eglot-hover-eldoc-function
|
||||
nil t)
|
||||
(add-hook 'eldoc-documentation-functions #'eglot-signature-eldoc-function
|
||||
nil t)
|
||||
(add-hook 'eldoc-documentation-functions #'eglot-highlight-eldoc-function
|
||||
nil t)
|
||||
(dolist (f (list #'eglot-signature-eldoc-function
|
||||
#'eglot-hover-eldoc-function
|
||||
#'eglot-highlight-eldoc-function
|
||||
#'eglot-code-action-suggestion))
|
||||
(add-hook 'eldoc-documentation-functions f t t))
|
||||
(eldoc-mode 1))
|
||||
(cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server))))
|
||||
(t
|
||||
(mapc #'delete-overlay eglot--highlights)
|
||||
(delete-overlay eglot--suggestion-overlay)
|
||||
(remove-hook 'after-change-functions #'eglot--after-change t)
|
||||
(remove-hook 'before-change-functions #'eglot--before-change t)
|
||||
(remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
|
||||
|
@ -2080,9 +2125,11 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
|
|||
(remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t)
|
||||
(remove-hook 'post-self-insert-hook #'eglot--post-self-insert-hook t)
|
||||
(remove-hook 'pre-command-hook #'eglot--pre-command-hook t)
|
||||
(remove-hook 'eldoc-documentation-functions #'eglot-hover-eldoc-function t)
|
||||
(remove-hook 'eldoc-documentation-functions #'eglot-signature-eldoc-function t)
|
||||
(remove-hook 'eldoc-documentation-functions #'eglot-highlight-eldoc-function t)
|
||||
(dolist (f (list #'eglot-hover-eldoc-function
|
||||
#'eglot-signature-eldoc-function
|
||||
#'eglot-highlight-eldoc-function
|
||||
#'eglot-code-action-suggestion))
|
||||
(remove-hook 'eldoc-documentation-functions f t))
|
||||
(cl-loop for (var . saved-binding) in eglot--saved-bindings
|
||||
do (set (make-local-variable var) saved-binding))
|
||||
(remove-function (local 'imenu-create-index-function) #'eglot-imenu)
|
||||
|
@ -2265,6 +2312,16 @@ Uses THING, FACE, DEFS and PREPEND."
|
|||
keymap ,map help-echo ,(concat prepend blurb)
|
||||
mouse-face mode-line-highlight))))
|
||||
|
||||
(defconst eglot--main-menu-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map [mode-line down-mouse-1] eglot-menu)
|
||||
map))
|
||||
|
||||
(defconst eglot--server-menu-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map [mode-line down-mouse-1] eglot-server-menu)
|
||||
map))
|
||||
|
||||
(defun eglot--mode-line-format ()
|
||||
"Compose Eglot's mode-line."
|
||||
(let* ((server (eglot-current-server))
|
||||
|
@ -2277,9 +2334,7 @@ Uses THING, FACE, DEFS and PREPEND."
|
|||
'face 'eglot-mode-line
|
||||
'mouse-face 'mode-line-highlight
|
||||
'help-echo "Eglot: Emacs LSP client\nmouse-1: Display minor mode menu"
|
||||
'keymap (let ((map (make-sparse-keymap)))
|
||||
(define-key map [mode-line down-mouse-1] eglot-menu)
|
||||
map)))
|
||||
'keymap eglot--main-menu-map))
|
||||
(when nick
|
||||
`(":"
|
||||
,(propertize
|
||||
|
@ -2287,9 +2342,7 @@ Uses THING, FACE, DEFS and PREPEND."
|
|||
'face 'eglot-mode-line
|
||||
'mouse-face 'mode-line-highlight
|
||||
'help-echo (format "Project '%s'\nmouse-1: LSP server control menu" nick)
|
||||
'keymap (let ((map (make-sparse-keymap)))
|
||||
(define-key map [mode-line down-mouse-1] eglot-server-menu)
|
||||
map))
|
||||
'keymap eglot--server-menu-map)
|
||||
,@(when last-error
|
||||
`("/" ,(eglot--mode-line-props
|
||||
"error" 'compilation-mode-line-fail
|
||||
|
@ -2310,7 +2363,11 @@ still unanswered LSP requests to the server\n")))
|
|||
'eglot-mode-line
|
||||
nil
|
||||
(format "(%s) %s %s" (nth 1 pr)
|
||||
(nth 2 pr) (nth 3 pr))))))))))
|
||||
(nth 2 pr) (nth 3 pr)))))
|
||||
,@(when (and
|
||||
(memq 'mode-line eglot-code-action-indications)
|
||||
(overlay-buffer eglot--suggestion-overlay))
|
||||
`("/" ,(overlay-get eglot--suggestion-overlay 'eglot--suggestion-tooltip))))))))
|
||||
|
||||
(add-to-list 'mode-line-misc-info
|
||||
`(eglot--managed-mode (" [" eglot--mode-line-format "] ")))
|
||||
|
@ -3513,8 +3570,6 @@ for which LSP on-type-formatting should be requested."
|
|||
:deferred :textDocument/hover))
|
||||
t))
|
||||
|
||||
(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.")
|
||||
|
||||
(defun eglot-highlight-eldoc-function (_cb &rest _ignored)
|
||||
"A member of `eldoc-documentation-functions', for highlighting symbols'."
|
||||
;; Obviously, we're not using ElDoc for documentation, but merely its
|
||||
|
@ -3760,6 +3815,20 @@ edit proposed by the server."
|
|||
(t
|
||||
(list (point) (point))))))
|
||||
|
||||
(cl-defun eglot--code-action-params (&key (beg (point)) (end beg)
|
||||
only triggerKind)
|
||||
(list :textDocument (eglot--TextDocumentIdentifier)
|
||||
:range (list :start (eglot--pos-to-lsp-position beg)
|
||||
:end (eglot--pos-to-lsp-position end))
|
||||
:context
|
||||
`(:diagnostics
|
||||
[,@(cl-loop for diag in (flymake-diagnostics beg end)
|
||||
when (cdr (assoc 'eglot-lsp-diag
|
||||
(eglot--diag-data diag)))
|
||||
collect it)]
|
||||
,@(when only `(:only [,only]))
|
||||
,@(when triggerKind `(:triggerKind ,triggerKind)))))
|
||||
|
||||
(defun eglot-code-actions (beg &optional end action-kind interactive)
|
||||
"Find LSP code actions of type ACTION-KIND between BEG and END.
|
||||
Interactively, offer to execute them.
|
||||
|
@ -3776,29 +3845,31 @@ at point. With prefix argument, prompt for ACTION-KIND."
|
|||
t))
|
||||
(eglot-server-capable-or-lose :codeActionProvider)
|
||||
(let* ((server (eglot--current-server-or-lose))
|
||||
(shortcut (and interactive
|
||||
(not (listp last-nonmenu-event)) ;; not run by mouse
|
||||
(overlayp eglot--suggestion-overlay)
|
||||
(overlay-buffer eglot--suggestion-overlay)
|
||||
(= beg (overlay-start eglot--suggestion-overlay))
|
||||
(= end (overlay-end eglot--suggestion-overlay))))
|
||||
(actions
|
||||
(eglot--request
|
||||
server
|
||||
:textDocument/codeAction
|
||||
(list :textDocument (eglot--TextDocumentIdentifier)
|
||||
:range (list :start (eglot--pos-to-lsp-position beg)
|
||||
:end (eglot--pos-to-lsp-position end))
|
||||
:context
|
||||
`(:diagnostics
|
||||
[,@(cl-loop for diag in (flymake-diagnostics beg end)
|
||||
when (cdr (assoc 'eglot-lsp-diag
|
||||
(eglot--diag-data diag)))
|
||||
collect it)]
|
||||
,@(when action-kind `(:only [,action-kind]))))))
|
||||
(if shortcut
|
||||
(overlay-get eglot--suggestion-overlay 'eglot--actions)
|
||||
(eglot--request
|
||||
server
|
||||
:textDocument/codeAction
|
||||
(eglot--code-action-params :beg beg :end end :only action-kind))))
|
||||
;; Redo filtering, in case the `:only' didn't go through.
|
||||
(actions (cl-loop for a across actions
|
||||
when (or (not action-kind)
|
||||
;; github#847
|
||||
(string-prefix-p action-kind (plist-get a :kind)))
|
||||
collect a)))
|
||||
(if interactive
|
||||
(eglot--read-execute-code-action actions server action-kind)
|
||||
actions)))
|
||||
(cond
|
||||
((and shortcut actions (null (cdr actions)))
|
||||
(eglot-execute server (car actions)))
|
||||
(interactive
|
||||
(eglot--read-execute-code-action actions server action-kind))
|
||||
(t actions))))
|
||||
|
||||
(defalias 'eglot-code-actions-at-mouse (eglot--mouse-call 'eglot-code-actions)
|
||||
"Like `eglot-code-actions', but intended for mouse events.")
|
||||
|
@ -3826,7 +3897,8 @@ at point. With prefix argument, prompt for ACTION-KIND."
|
|||
default-action)
|
||||
menu-items nil t nil nil default-action)
|
||||
menu-items))))))
|
||||
(eglot-execute server chosen)))
|
||||
(when chosen
|
||||
(eglot-execute server chosen))))
|
||||
|
||||
(defmacro eglot--code-action (name kind)
|
||||
"Define NAME to execute KIND code action."
|
||||
|
@ -3841,6 +3913,57 @@ at point. With prefix argument, prompt for ACTION-KIND."
|
|||
(eglot--code-action eglot-code-action-rewrite "refactor.rewrite")
|
||||
(eglot--code-action eglot-code-action-quickfix "quickfix")
|
||||
|
||||
(defun eglot-code-action-suggestion (cb &rest _ignored)
|
||||
"A member of `eldoc-documentation-functions', for suggesting actions."
|
||||
(when (and (eglot-server-capable :codeActionProvider)
|
||||
eglot-code-action-indications)
|
||||
(let ((buf (current-buffer))
|
||||
(bounds (eglot--code-action-bounds))
|
||||
(use-text-p (memq 'eldoc-hint eglot-code-action-indications))
|
||||
tooltip blurb)
|
||||
(jsonrpc-async-request
|
||||
(eglot--current-server-or-lose)
|
||||
:textDocument/codeAction
|
||||
(eglot--code-action-params :beg (car bounds) :end (cadr bounds)
|
||||
:triggerKind 2)
|
||||
:success-fn
|
||||
(lambda (actions)
|
||||
(eglot--when-buffer-window buf
|
||||
(delete-overlay eglot--suggestion-overlay)
|
||||
(when (cl-plusp (length actions))
|
||||
(setq blurb
|
||||
(substitute-command-keys
|
||||
(eglot--format "\\[eglot-code-actions]: %s"
|
||||
(plist-get (aref actions 0) :title))))
|
||||
(if (>= (length actions) 2)
|
||||
(setq blurb (concat blurb (format " (and %s more actions)"
|
||||
(1- (length actions))))))
|
||||
(setq tooltip
|
||||
(propertize eglot-code-action-indicator
|
||||
'face 'eglot-code-action-indicator-face
|
||||
'help-echo blurb
|
||||
'mouse-face 'highlight
|
||||
'keymap eglot-diagnostics-map))
|
||||
(save-excursion
|
||||
(goto-char (car bounds))
|
||||
(let ((ov (make-overlay (car bounds) (cadr bounds))))
|
||||
(overlay-put ov 'eglot--actions actions)
|
||||
(overlay-put ov 'eglot--suggestion-tooltip tooltip)
|
||||
(overlay-put
|
||||
ov
|
||||
'before-string
|
||||
(cond ((memq 'nearby eglot-code-action-indications)
|
||||
tooltip)
|
||||
((memq 'margin eglot-code-action-indications)
|
||||
(propertize "⚡"
|
||||
'display
|
||||
`((margin left-margin)
|
||||
,tooltip)))))
|
||||
(setq eglot--suggestion-overlay ov)))))
|
||||
(when use-text-p (funcall cb blurb)))
|
||||
:deferred :textDocument/codeAction)
|
||||
(and use-text-p t))))
|
||||
|
||||
|
||||
;;; Dynamic registration
|
||||
;;;
|
||||
|
|
Loading…
Add table
Reference in a new issue