aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2025-01-26 23:26:51 +0000
committerJoão Távora2025-01-28 11:04:21 +0000
commitd6a502fc7a69dfa11aa100da5966a6962a82f613 (patch)
treeb3b50646ad931a0fe9d04fb3b3922a71fd8b0e24
parent7f0ef9655cdc28c3b8055e32c9e84ea57339b139 (diff)
downloademacs-d6a502fc7a69dfa11aa100da5966a6962a82f613.tar.gz
emacs-d6a502fc7a69dfa11aa100da5966a6962a82f613.zip
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.
-rw-r--r--doc/misc/eglot.texi34
-rw-r--r--etc/EGLOT-NEWS8
-rw-r--r--lisp/progmodes/eglot.el193
3 files changed, 200 insertions, 35 deletions
diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi
index 47649455cec..93675210c59 100644
--- a/doc/misc/eglot.texi
+++ b/doc/misc/eglot.texi
@@ -439,6 +439,13 @@ command (@pxref{Symbol Completion,,, emacs, GNU Emacs Manual}). This
439uses the language-server's parser data for the completion candidates. 439uses the language-server's parser data for the completion candidates.
440 440
441@item 441@item
442Server-suggested code refactorings. The ElDoc package is also leveraged
443to retrieve so-called @dfn{code actions} nearby point. When such
444suggestions are available they are annotated with a special indication
445and can be easily invoked by the user with the @code{eglot-code-action}
446command (@pxref{Eglot Commands}).
447
448@item
442On-the-fly succinct informative annotations, so-called @dfn{inlay 449On-the-fly succinct informative annotations, so-called @dfn{inlay
443hints}. Eglot adds special intangible text nearby certain identifiers, 450hints}. Eglot adds special intangible text nearby certain identifiers,
444be it the type of a variable, or the name of a formal parameter in a 451be it the type of a variable, or the name of a formal parameter in a
@@ -926,6 +933,31 @@ Setting this variable to true causes Eglot to send special cancellation
926notification for certain stale client request. This may help some LSP 933notification for certain stale client request. This may help some LSP
927servers avoid doing costly but ultimately useless work on behalf of the 934servers avoid doing costly but ultimately useless work on behalf of the
928client, improving overall performance. 935client, improving overall performance.
936
937@item eglot-code-action-indications
938This variable controls the indication of code actions available at
939point. Value is a list of symbols, more than one can be specified:
940
941@itemize @minus
942@item
943@code{eldoc-hint}: ElDoc is used to hint about at-point actions.
944@item
945@code{margin}: A special indicator appears in the margin of the line
946that point is currently on. This indicator is not interactive (you
947cannot click on it with the mouse).
948@item
949@code{nearby}: An interactive special indicator appears near point.
950@item
951@code{mode-line}: An interactive special indicator appears in the mode
952line.
953@end itemize
954
955@code{margin} and @code{nearby} are incompatible. If the list is empty,
956ElDoc will not hint about at-point actions.
957
958@item eglot-code-action-indicator
959This variable is a string determining what the special indicator looks
960like.
929@end vtable 961@end vtable
930 962
931@node Other Variables 963@node Other Variables
@@ -1004,6 +1036,8 @@ about an identifier.
1004signature information. 1036signature information.
1005@item @code{eglot-highlight-eldoc-function}, to highlight nearby 1037@item @code{eglot-highlight-eldoc-function}, to highlight nearby
1006manifestations of an identifier. 1038manifestations of an identifier.
1039@item @code{eglot-code-action-suggestion}, to retrieve relevant code
1040actions at point.
1007@end itemize 1041@end itemize
1008 1042
1009A simple tweak to remove at-point identifier information for 1043A simple tweak to remove at-point identifier information for
diff --git a/etc/EGLOT-NEWS b/etc/EGLOT-NEWS
index 9deac73d9fc..18a4e9e2d9e 100644
--- a/etc/EGLOT-NEWS
+++ b/etc/EGLOT-NEWS
@@ -26,6 +26,14 @@ Tweaking this variable may help some LSP servers avoid doing costly but
26ultimately useless work on behalf of the client, improving overall 26ultimately useless work on behalf of the client, improving overall
27performance. 27performance.
28 28
29** Suggests code actions at point
30
31A commonly requested feature, Eglot will use ElDoc to ask the server for
32code actions available at point, indicating to the user, who may use
33execute them quickly via the usual 'eglot-code-actions' command.
34Customize with 'eglot-code-action-indications' and
35'eglot-code-action-indicator'.
36
29 37
30* Changes in Eglot 1.18 (20/1/2025) 38* Changes in Eglot 1.18 (20/1/2025)
31 39
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index f2fcbc0a4c2..b78c634012f 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -579,6 +579,45 @@ notification is implementation defined, and is only useful for some
579servers." 579servers."
580 :type 'boolean) 580 :type 'boolean)
581 581
582(defface eglot-code-action-indicator-face
583 '((t (:inherit font-lock-escape-face :weight bold)))
584 "Face used for code action suggestions.")
585
586(defcustom eglot-code-action-indications
587 '(eldoc-hint mode-line margin)
588 "How Eglot indicates there's are code actions available at point.
589Value is a list of symbols, more than one can be specified:
590
591- `eldoc-hint': ElDoc is used to hint about at-point actions.
592- `margin': A special indicator appears in the margin.
593- `nearby': A special indicator appears near point.
594- `mode-line': A special indicator appears in the mode-line.
595
596`margin' and `nearby' are incompatible. `margin's indicator is not
597interactive. If the list is empty, Eglot will not hint about code
598actions at point."
599 :type '(set
600 :tag "Tick the ones you're interested in"
601 (const :tag "ElDoc textual hint" eldoc-hint)
602 (const :tag "Right besides point" nearby)
603 (const :tag "In mode line" mode-line)
604 (const :tag "In margin" margin))
605 :package-version '(Eglot . "1.19"))
606
607(defcustom eglot-code-action-indicator
608 (cl-loop for c in '(? ?⚡?✓ ?α ??)
609 when (char-displayable-p c)
610 return (make-string 1 c))
611 "Indicator string for code action suggestions."
612 :type (let ((basic-choices
613 (cl-loop for c in '(? ?⚡?✓ ?α ??)
614 when (char-displayable-p c)
615 collect `(const :tag ,(format "Use `%c'" c)
616 ,(make-string 1 c)))))
617 `(choice ,@basic-choices
618 (string :tag "Specify your own")))
619 :package-version '(Eglot . "1.19"))
620
582(defvar eglot-withhold-process-id nil 621(defvar eglot-withhold-process-id nil
583 "If non-nil, Eglot will not send the Emacs process id to the language server. 622 "If non-nil, Eglot will not send the Emacs process id to the language server.
584This can be useful when using docker to run a language server.") 623This 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
2015 "A hook run by Eglot after it started/stopped managing a buffer. 2054 "A hook run by Eglot after it started/stopped managing a buffer.
2016Use `eglot-managed-p' to determine if current buffer is managed.") 2055Use `eglot-managed-p' to determine if current buffer is managed.")
2017 2056
2057(defvar eglot--highlights nil "Overlays for `eglot-highlight-eldoc-function'.")
2058
2059(defvar-local eglot--suggestion-overlay (make-overlay 0 0)
2060 "Overlay for `eglot-code-action-suggestion'.")
2061
2018(define-minor-mode eglot--managed-mode 2062(define-minor-mode eglot--managed-mode
2019 "Mode for source buffers managed by some Eglot project." 2063 "Mode for source buffers managed by some Eglot project."
2020 :init-value nil :lighter nil :keymap eglot-mode-map :interactive nil 2064 :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.")
2056 #'eglot-imenu)) 2100 #'eglot-imenu))
2057 (unless (eglot--stay-out-of-p 'flymake) (flymake-mode 1)) 2101 (unless (eglot--stay-out-of-p 'flymake) (flymake-mode 1))
2058 (unless (eglot--stay-out-of-p 'eldoc) 2102 (unless (eglot--stay-out-of-p 'eldoc)
2059 (add-hook 'eldoc-documentation-functions #'eglot-hover-eldoc-function 2103 (dolist (f (list #'eglot-signature-eldoc-function
2060 nil t) 2104 #'eglot-hover-eldoc-function
2061 (add-hook 'eldoc-documentation-functions #'eglot-signature-eldoc-function 2105 #'eglot-highlight-eldoc-function
2062 nil t) 2106 #'eglot-code-action-suggestion))
2063 (add-hook 'eldoc-documentation-functions #'eglot-highlight-eldoc-function 2107 (add-hook 'eldoc-documentation-functions f t t))
2064 nil t)
2065 (eldoc-mode 1)) 2108 (eldoc-mode 1))
2066 (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) 2109 (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server))))
2067 (t 2110 (t
2111 (mapc #'delete-overlay eglot--highlights)
2112 (delete-overlay eglot--suggestion-overlay)
2068 (remove-hook 'after-change-functions #'eglot--after-change t) 2113 (remove-hook 'after-change-functions #'eglot--after-change t)
2069 (remove-hook 'before-change-functions #'eglot--before-change t) 2114 (remove-hook 'before-change-functions #'eglot--before-change t)
2070 (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t) 2115 (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.")
2080 (remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t) 2125 (remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t)
2081 (remove-hook 'post-self-insert-hook #'eglot--post-self-insert-hook t) 2126 (remove-hook 'post-self-insert-hook #'eglot--post-self-insert-hook t)
2082 (remove-hook 'pre-command-hook #'eglot--pre-command-hook t) 2127 (remove-hook 'pre-command-hook #'eglot--pre-command-hook t)
2083 (remove-hook 'eldoc-documentation-functions #'eglot-hover-eldoc-function t) 2128 (dolist (f (list #'eglot-hover-eldoc-function
2084 (remove-hook 'eldoc-documentation-functions #'eglot-signature-eldoc-function t) 2129 #'eglot-signature-eldoc-function
2085 (remove-hook 'eldoc-documentation-functions #'eglot-highlight-eldoc-function t) 2130 #'eglot-highlight-eldoc-function
2131 #'eglot-code-action-suggestion))
2132 (remove-hook 'eldoc-documentation-functions f t))
2086 (cl-loop for (var . saved-binding) in eglot--saved-bindings 2133 (cl-loop for (var . saved-binding) in eglot--saved-bindings
2087 do (set (make-local-variable var) saved-binding)) 2134 do (set (make-local-variable var) saved-binding))
2088 (remove-function (local 'imenu-create-index-function) #'eglot-imenu) 2135 (remove-function (local 'imenu-create-index-function) #'eglot-imenu)
@@ -2265,6 +2312,16 @@ Uses THING, FACE, DEFS and PREPEND."
2265 keymap ,map help-echo ,(concat prepend blurb) 2312 keymap ,map help-echo ,(concat prepend blurb)
2266 mouse-face mode-line-highlight)))) 2313 mouse-face mode-line-highlight))))
2267 2314
2315(defconst eglot--main-menu-map
2316 (let ((map (make-sparse-keymap)))
2317 (define-key map [mode-line down-mouse-1] eglot-menu)
2318 map))
2319
2320(defconst eglot--server-menu-map
2321 (let ((map (make-sparse-keymap)))
2322 (define-key map [mode-line down-mouse-1] eglot-server-menu)
2323 map))
2324
2268(defun eglot--mode-line-format () 2325(defun eglot--mode-line-format ()
2269 "Compose Eglot's mode-line." 2326 "Compose Eglot's mode-line."
2270 (let* ((server (eglot-current-server)) 2327 (let* ((server (eglot-current-server))
@@ -2277,9 +2334,7 @@ Uses THING, FACE, DEFS and PREPEND."
2277 'face 'eglot-mode-line 2334 'face 'eglot-mode-line
2278 'mouse-face 'mode-line-highlight 2335 'mouse-face 'mode-line-highlight
2279 'help-echo "Eglot: Emacs LSP client\nmouse-1: Display minor mode menu" 2336 'help-echo "Eglot: Emacs LSP client\nmouse-1: Display minor mode menu"
2280 'keymap (let ((map (make-sparse-keymap))) 2337 'keymap eglot--main-menu-map))
2281 (define-key map [mode-line down-mouse-1] eglot-menu)
2282 map)))
2283 (when nick 2338 (when nick
2284 `(":" 2339 `(":"
2285 ,(propertize 2340 ,(propertize
@@ -2287,9 +2342,7 @@ Uses THING, FACE, DEFS and PREPEND."
2287 'face 'eglot-mode-line 2342 'face 'eglot-mode-line
2288 'mouse-face 'mode-line-highlight 2343 'mouse-face 'mode-line-highlight
2289 'help-echo (format "Project '%s'\nmouse-1: LSP server control menu" nick) 2344 'help-echo (format "Project '%s'\nmouse-1: LSP server control menu" nick)
2290 'keymap (let ((map (make-sparse-keymap))) 2345 'keymap eglot--server-menu-map)
2291 (define-key map [mode-line down-mouse-1] eglot-server-menu)
2292 map))
2293 ,@(when last-error 2346 ,@(when last-error
2294 `("/" ,(eglot--mode-line-props 2347 `("/" ,(eglot--mode-line-props
2295 "error" 'compilation-mode-line-fail 2348 "error" 'compilation-mode-line-fail
@@ -2310,7 +2363,11 @@ still unanswered LSP requests to the server\n")))
2310 'eglot-mode-line 2363 'eglot-mode-line
2311 nil 2364 nil
2312 (format "(%s) %s %s" (nth 1 pr) 2365 (format "(%s) %s %s" (nth 1 pr)
2313 (nth 2 pr) (nth 3 pr)))))))))) 2366 (nth 2 pr) (nth 3 pr)))))
2367 ,@(when (and
2368 (memq 'mode-line eglot-code-action-indications)
2369 (overlay-buffer eglot--suggestion-overlay))
2370 `("/" ,(overlay-get eglot--suggestion-overlay 'eglot--suggestion-tooltip))))))))
2314 2371
2315(add-to-list 'mode-line-misc-info 2372(add-to-list 'mode-line-misc-info
2316 `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) 2373 `(eglot--managed-mode (" [" eglot--mode-line-format "] ")))
@@ -3513,8 +3570,6 @@ for which LSP on-type-formatting should be requested."
3513 :deferred :textDocument/hover)) 3570 :deferred :textDocument/hover))
3514 t)) 3571 t))
3515 3572
3516(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.")
3517
3518(defun eglot-highlight-eldoc-function (_cb &rest _ignored) 3573(defun eglot-highlight-eldoc-function (_cb &rest _ignored)
3519 "A member of `eldoc-documentation-functions', for highlighting symbols'." 3574 "A member of `eldoc-documentation-functions', for highlighting symbols'."
3520 ;; Obviously, we're not using ElDoc for documentation, but merely its 3575 ;; Obviously, we're not using ElDoc for documentation, but merely its
@@ -3760,6 +3815,20 @@ edit proposed by the server."
3760 (t 3815 (t
3761 (list (point) (point)))))) 3816 (list (point) (point))))))
3762 3817
3818(cl-defun eglot--code-action-params (&key (beg (point)) (end beg)
3819 only triggerKind)
3820 (list :textDocument (eglot--TextDocumentIdentifier)
3821 :range (list :start (eglot--pos-to-lsp-position beg)
3822 :end (eglot--pos-to-lsp-position end))
3823 :context
3824 `(:diagnostics
3825 [,@(cl-loop for diag in (flymake-diagnostics beg end)
3826 when (cdr (assoc 'eglot-lsp-diag
3827 (eglot--diag-data diag)))
3828 collect it)]
3829 ,@(when only `(:only [,only]))
3830 ,@(when triggerKind `(:triggerKind ,triggerKind)))))
3831
3763(defun eglot-code-actions (beg &optional end action-kind interactive) 3832(defun eglot-code-actions (beg &optional end action-kind interactive)
3764 "Find LSP code actions of type ACTION-KIND between BEG and END. 3833 "Find LSP code actions of type ACTION-KIND between BEG and END.
3765Interactively, offer to execute them. 3834Interactively, offer to execute them.
@@ -3776,29 +3845,31 @@ at point. With prefix argument, prompt for ACTION-KIND."
3776 t)) 3845 t))
3777 (eglot-server-capable-or-lose :codeActionProvider) 3846 (eglot-server-capable-or-lose :codeActionProvider)
3778 (let* ((server (eglot--current-server-or-lose)) 3847 (let* ((server (eglot--current-server-or-lose))
3848 (shortcut (and interactive
3849 (not (listp last-nonmenu-event)) ;; not run by mouse
3850 (overlayp eglot--suggestion-overlay)
3851 (overlay-buffer eglot--suggestion-overlay)
3852 (= beg (overlay-start eglot--suggestion-overlay))
3853 (= end (overlay-end eglot--suggestion-overlay))))
3779 (actions 3854 (actions
3780 (eglot--request 3855 (if shortcut
3781 server 3856 (overlay-get eglot--suggestion-overlay 'eglot--actions)
3782 :textDocument/codeAction 3857 (eglot--request
3783 (list :textDocument (eglot--TextDocumentIdentifier) 3858 server
3784 :range (list :start (eglot--pos-to-lsp-position beg) 3859 :textDocument/codeAction
3785 :end (eglot--pos-to-lsp-position end)) 3860 (eglot--code-action-params :beg beg :end end :only action-kind))))
3786 :context
3787 `(:diagnostics
3788 [,@(cl-loop for diag in (flymake-diagnostics beg end)
3789 when (cdr (assoc 'eglot-lsp-diag
3790 (eglot--diag-data diag)))
3791 collect it)]
3792 ,@(when action-kind `(:only [,action-kind]))))))
3793 ;; Redo filtering, in case the `:only' didn't go through. 3861 ;; Redo filtering, in case the `:only' didn't go through.
3794 (actions (cl-loop for a across actions 3862 (actions (cl-loop for a across actions
3795 when (or (not action-kind) 3863 when (or (not action-kind)
3796 ;; github#847 3864 ;; github#847
3797 (string-prefix-p action-kind (plist-get a :kind))) 3865 (string-prefix-p action-kind (plist-get a :kind)))
3798 collect a))) 3866 collect a)))
3799 (if interactive 3867 (cond
3800 (eglot--read-execute-code-action actions server action-kind) 3868 ((and shortcut actions (null (cdr actions)))
3801 actions))) 3869 (eglot-execute server (car actions)))
3870 (interactive
3871 (eglot--read-execute-code-action actions server action-kind))
3872 (t actions))))
3802 3873
3803(defalias 'eglot-code-actions-at-mouse (eglot--mouse-call 'eglot-code-actions) 3874(defalias 'eglot-code-actions-at-mouse (eglot--mouse-call 'eglot-code-actions)
3804 "Like `eglot-code-actions', but intended for mouse events.") 3875 "Like `eglot-code-actions', but intended for mouse events.")
@@ -3826,7 +3897,8 @@ at point. With prefix argument, prompt for ACTION-KIND."
3826 default-action) 3897 default-action)
3827 menu-items nil t nil nil default-action) 3898 menu-items nil t nil nil default-action)
3828 menu-items)))))) 3899 menu-items))))))
3829 (eglot-execute server chosen))) 3900 (when chosen
3901 (eglot-execute server chosen))))
3830 3902
3831(defmacro eglot--code-action (name kind) 3903(defmacro eglot--code-action (name kind)
3832 "Define NAME to execute KIND code action." 3904 "Define NAME to execute KIND code action."
@@ -3841,6 +3913,57 @@ at point. With prefix argument, prompt for ACTION-KIND."
3841(eglot--code-action eglot-code-action-rewrite "refactor.rewrite") 3913(eglot--code-action eglot-code-action-rewrite "refactor.rewrite")
3842(eglot--code-action eglot-code-action-quickfix "quickfix") 3914(eglot--code-action eglot-code-action-quickfix "quickfix")
3843 3915
3916(defun eglot-code-action-suggestion (cb &rest _ignored)
3917 "A member of `eldoc-documentation-functions', for suggesting actions."
3918 (when (and (eglot-server-capable :codeActionProvider)
3919 eglot-code-action-indications)
3920 (let ((buf (current-buffer))
3921 (bounds (eglot--code-action-bounds))
3922 (use-text-p (memq 'eldoc-hint eglot-code-action-indications))
3923 tooltip blurb)
3924 (jsonrpc-async-request
3925 (eglot--current-server-or-lose)
3926 :textDocument/codeAction
3927 (eglot--code-action-params :beg (car bounds) :end (cadr bounds)
3928 :triggerKind 2)
3929 :success-fn
3930 (lambda (actions)
3931 (eglot--when-buffer-window buf
3932 (delete-overlay eglot--suggestion-overlay)
3933 (when (cl-plusp (length actions))
3934 (setq blurb
3935 (substitute-command-keys
3936 (eglot--format "\\[eglot-code-actions]: %s"
3937 (plist-get (aref actions 0) :title))))
3938 (if (>= (length actions) 2)
3939 (setq blurb (concat blurb (format " (and %s more actions)"
3940 (1- (length actions))))))
3941 (setq tooltip
3942 (propertize eglot-code-action-indicator
3943 'face 'eglot-code-action-indicator-face
3944 'help-echo blurb
3945 'mouse-face 'highlight
3946 'keymap eglot-diagnostics-map))
3947 (save-excursion
3948 (goto-char (car bounds))
3949 (let ((ov (make-overlay (car bounds) (cadr bounds))))
3950 (overlay-put ov 'eglot--actions actions)
3951 (overlay-put ov 'eglot--suggestion-tooltip tooltip)
3952 (overlay-put
3953 ov
3954 'before-string
3955 (cond ((memq 'nearby eglot-code-action-indications)
3956 tooltip)
3957 ((memq 'margin eglot-code-action-indications)
3958 (propertize "⚡"
3959 'display
3960 `((margin left-margin)
3961 ,tooltip)))))
3962 (setq eglot--suggestion-overlay ov)))))
3963 (when use-text-p (funcall cb blurb)))
3964 :deferred :textDocument/codeAction)
3965 (and use-text-p t))))
3966
3844 3967
3845;;; Dynamic registration 3968;;; Dynamic registration
3846;;; 3969;;;