]> git.eshelyaron.com Git - emacs.git/commitdiff
Eglot: suggest code actions at point
authorJoão Távora <joaotavora@gmail.com>
Sun, 26 Jan 2025 23:26:51 +0000 (23:26 +0000)
committerEshel Yaron <me@eshelyaron.com>
Thu, 30 Jan 2025 18:11:26 +0000 (19:11 +0100)
* 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.

(cherry picked from commit d6a502fc7a69dfa11aa100da5966a6962a82f613)

doc/misc/eglot.texi
etc/EGLOT-NEWS
lisp/progmodes/eglot.el

index 47649455ceceb9426f27dd6a7d2f6442490f957c..93675210c59d68575e507563907b1c87a1e5532d 100644 (file)
@@ -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
index 9deac73d9fc2296d688ced337a44064b801a98f7..18a4e9e2d9e985e1329ad11a8031ebe88dced788 100644 (file)
@@ -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'.
+
 \f
 * Changes in Eglot 1.18 (20/1/2025)
 
index b87a45e9912bd78568d7cf4d2d60f4b25ee3b34c..0125abe729e4f03bf82640cc058696fd2e676c50 100644 (file)
@@ -577,6 +577,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.")
@@ -2014,6 +2053,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.")
       (add-hook 'flymake-diagnostic-functions #'eglot-flymake-backend nil t)
       (if flymake-mode (flymake-start) (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)
@@ -2079,10 +2124,12 @@ 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 'flymake-diagnostic-functions #'eglot-flymake-backend 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)
@@ -2255,6 +2302,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))
@@ -2267,9 +2324,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
@@ -2277,9 +2332,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
@@ -2300,7 +2353,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 "] ")))
@@ -3552,8 +3609,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
@@ -3833,6 +3888,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.
@@ -3849,29 +3918,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.")
@@ -3899,7 +3970,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."
@@ -3914,6 +3986,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))))
+
 \f
 ;;; Dynamic registration
 ;;;