emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

emacs-29 1841299a11d 2/2: Eglot: implement inlay hints (bug#61412, bug#6


From: João Távora
Subject: emacs-29 1841299a11d 2/2: Eglot: implement inlay hints (bug#61412, bug#61066)
Date: Wed, 22 Feb 2023 14:17:49 -0500 (EST)

branch: emacs-29
commit 1841299a11dfcd875bdbdb75d1fc56d996a727f7
Author: João Távora <joaotavora@gmail.com>
Commit: João Távora <joaotavora@gmail.com>

    Eglot: implement inlay hints (bug#61412, bug#61066)
    
    Inlay hints are small text annotations to specific parts of the whole
    buffer, not unlike diagnostics, but designed to help readability
    instead of indicating problems.  For example, a C++ LSP server can
    serve hints about positional parameter names in function calls and a
    variable's automatically deduced type.  Emacs can display these hints
    in many little 0-length overlays with an 'before-string property, thus
    helping the user remember those types and parameter names.
    
    Since inlay hints are potentially a large amount of data to request
    from the LSP server, the implementation strives to be as parsimonious
    as possible with these requests.
    
    So, by default, inlay hints are only requested for the visible
    portions of the buffer across windows showing this buffer.  This is
    done by leveraging the 'window-scroll-functions' variable, making for
    a reasonably complex implementation involving per-window timers.  When
    scrolling a window, it may take a short amount of time for inlay hints
    to "pop in".  The new user variable 'eglot-lazy-inlay-hints' can be
    used to exert some control over this.
    
    Specifically, if the variable's value is set to 'nil', then inlay
    hints are greedily fetched for the whole buffer every time a change
    occurs.  This is a much simpler mode of operation which may avoid
    problems, but is also likely much slower in large buffers.
    
    Also, because the inlay feature is probably visually suprising to
    some, it is turned OFF by default, which is not the usual practice of
    Eglot (at least not when the necessary infrastructure is present).
    This decision may be changed soon.  Here's a good one-liner for
    enabling it by default in every Eglot-managed buffer:
    
       (add-hook 'eglot-managed-mode-hook #'eglot-inlay-hints-mode)
    
    I haven't tested inlay hints extensively across many LSP servers, so I
    would appreciate any testing, both for functional edge cases and
    regarding performance.  There are possibly more optimization
    oportunities in the "lazy" mode of operation, like more aggressively
    deleting buffer overlays that are not in visible parts of the buffer.
    
    Though I ended up writing this one from scratch, I want to thank
    Dimitry Bolopopsky <dimitri@belopopsky.com> and Chinmay Dala
    <dalal.chinmay.0101@gmail.com> for suggestions and early patches.
    
    * lisp/progmodes/eglot.el (eglot--lsp-interface-alist): Define
    InlayHint.
    (eglot-client-capabilities): Announce 'inlayHint' capability.
    (eglot-ignored-server-capabilities): Add :inlayHintProvider.
    (eglot--document-changed-hook): New helper hook.
    (eglot--after-change): Use it.
    (eglot-inlay-hint-face, eglot-type-hint-face)
    (eglot-parameter-hint-face): New faces.
    (eglot--update-hints-1, eglot--inlay-hints-after-scroll)
    (eglot--inlay-hints-fully, eglot--inlay-hints-lazily): New helpers.
    (eglot-lazy-inlay-hints): New user variable.
    (eglot-inlay-hints-mode): New minor mode.
    (eglot--maybe-activate-editing-mode): Try to activate
    eglot-inlay-hints-mode.
    (eglot--before-change): Remove overlays immediately in the
    area being changed.
    (eglot--managed-mode-off): Remove overlays.
    
    * doc/misc/eglot.texi (Eglot Features): Mention inlay hints.
    (Eglot Variables): Mention eglot-lazy-inlay-hints.
---
 doc/misc/eglot.texi     |  17 ++++++
 lisp/progmodes/eglot.el | 145 ++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi
index 56151b5482f..38c6adaf131 100644
--- a/doc/misc/eglot.texi
+++ b/doc/misc/eglot.texi
@@ -502,6 +502,15 @@ project.  The command @kbd{M-x eglot-code-actions} will 
pop up a menu
 of code applicable actions at point.
 @end table
 
+@item M-x eglot-inlay-hints-mode
+This command toggles LSP ``inlay hints'' on and off for the current
+buffer.  Inlay hints are small text annotations to specific parts of
+the whole buffer, not unlike diagnostics, but designed to help
+readability instead of indicating problems.  For example, a C++ LSP
+server can serve hints about positional parameter names in function
+calls and a variable's automatically deduced type.  Inlay hints help
+the user not have to remember these things by heart.
+
 @end itemize
 
 Not all servers support the full set of LSP capabilities, but most of
@@ -874,6 +883,14 @@ this map.  For example:
   (define-key eglot-mode-map (kbd "<f6>") 'xref-find-definitions)
 @end lisp
 
+@item eglot-lazy-inlay-hints
+This variable controls the operation and performance of LSP Inlay
+Hints (@pxref{Eglot Features}).  If non-@code{nil}, it specifies how
+much time to wait after a window is displayed or scrolled before
+requesting hints for that visible portion of a given buffer.  If
+@code{nil}, inlay hints are always requested for the whole buffer,
+even for parts of it not currently visible.
+
 @end vtable
 
 Additional variables, which are relevant for customizing the server
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 8b0caf41ad7..df755dfa43a 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -47,9 +47,10 @@
 ;;   definition-chasing, Flymake for diagnostics, Eldoc for at-point
 ;;   documentation, etc.  Eglot's job is generally *not* to provide
 ;;   such a UI itself, though a small number of simple
-;;   counter-examples do exist, for example in the `eglot-rename'
-;;   command.  When a new UI is evidently needed, consider adding a
-;;   new package to Emacs, or extending an existing one.
+;;   counter-examples do exist, e.g. in the `eglot-rename' command or
+;;   the `eglot-inlay-hints-mode' minor mode.  When a new UI is
+;;   evidently needed, consider adding a new package to Emacs, or
+;;   extending an existing one.
 ;;
 ;; * Eglot was designed to function with just the UI facilities found
 ;;   in the latest Emacs core, as long as those facilities are also
@@ -483,7 +484,9 @@ This can be useful when using docker to run a language 
server.")
       (VersionedTextDocumentIdentifier (:uri :version) ())
       (WorkDoneProgress (:kind) (:title :message :percentage :cancellable))
       (WorkspaceEdit () (:changes :documentChanges))
-      (WorkspaceSymbol (:name :kind) (:containerName :location :data)))
+      (WorkspaceSymbol (:name :kind) (:containerName :location :data))
+      (InlayHint (:position :label) (:kind :textEdits :tooltip :paddingLeft
+                                           :paddingRight :data)))
     "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces.
 
 INTERFACE-NAME is a symbol designated by the spec as
@@ -803,6 +806,7 @@ treated as in `eglot--dbind'."
              :formatting         `(:dynamicRegistration :json-false)
              :rangeFormatting    `(:dynamicRegistration :json-false)
              :rename             `(:dynamicRegistration :json-false)
+             :inlayHint          `(:dynamicRegistration :json-false)
              :publishDiagnostics (list :relatedInformation :json-false
                                        ;; TODO: We can support 
:codeDescription after
                                        ;; adding an appropriate UI to
@@ -1625,7 +1629,8 @@ under cursor."
           (const :tag "Highlight links in document" :documentLinkProvider)
           (const :tag "Decorate color references" :colorProvider)
           (const :tag "Fold regions of buffer" :foldingRangeProvider)
-          (const :tag "Execute custom commands" :executeCommandProvider)))
+          (const :tag "Execute custom commands" :executeCommandProvider)
+          (const :tag "Inlay hints" :inlayHintProvider)))
 
 (defun eglot--server-capable (&rest feats)
   "Determine if current server is capable of FEATS."
@@ -1818,6 +1823,7 @@ Use `eglot-managed-p' to determine if current buffer is 
managed.")
 
 (defun eglot--managed-mode-off ()
   "Turn off `eglot--managed-mode' unconditionally."
+  (remove-overlays nil nil 'eglot--overlay t)
   (eglot--managed-mode -1))
 
 (defun eglot-current-server ()
@@ -2285,6 +2291,7 @@ THINGS are either registrations or unregisterations 
(sic)."
 
 (defun eglot--before-change (beg end)
   "Hook onto `before-change-functions' with BEG and END."
+  (remove-overlays beg end 'eglot--overlay t)
   (when (listp eglot--recent-changes)
     ;; Records BEG and END, crucially convert them into LSP
     ;; (line/char) positions before that information is lost (because
@@ -2297,6 +2304,9 @@ THINGS are either registrations or unregisterations 
(sic)."
             (,end . ,(copy-marker end t)))
           eglot--recent-changes)))
 
+(defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange)
+  "Internal hook for doing things when the document changes.")
+
 (defun eglot--after-change (beg end pre-change-length)
   "Hook onto `after-change-functions'.
 Records BEG, END and PRE-CHANGE-LENGTH locally."
@@ -2337,7 +2347,7 @@ Records BEG, END and PRE-CHANGE-LENGTH locally."
            eglot-send-changes-idle-time
            nil (lambda () (eglot--when-live-buffer buf
                             (when eglot--managed-mode
-                              (eglot--signal-textDocument/didChange)
+                              (run-hooks 'eglot--document-changed-hook)
                               (setq eglot--change-idle-timer nil))))))))
 
 ;; HACK! Launching a deferred sync request with outstanding changes is a
@@ -3465,6 +3475,129 @@ If NOERROR, return predicate, else erroring function."
       (pop-to-buffer (current-buffer)))))
 
 
+;;; Inlay hints
+(defface eglot-inlay-hint-face '((t (:height 0.8 :inherit shadow)))
+  "Face used for inlay hint overlays.")
+
+(defface eglot-type-hint-face '((t (:inherit eglot-inlay-hint-face)))
+  "Face used for type inlay hint overlays.")
+
+(defface eglot-parameter-hint-face '((t (:inherit eglot-inlay-hint-face)))
+  "Face used for parameter inlay hint overlays.")
+
+(defcustom eglot-lazy-inlay-hints 0.3
+  "If non-nil, restrict LSP inlay hints to visible portion of buffer.
+
+Value is number specifying how many seconds to wait after a
+window has been (re)scrolled before requesting new inlay hints
+for the visible region of the window being manipulated.
+
+If nil, then inlay hints are requested for the entire buffer.
+
+This value is only meaningful if the minor mode
+`eglot-inlay-hints-mode' is true.
+"
+  :type 'number
+  :version "29.1")
+
+(defun eglot--inlay-hints-fully ()
+  (eglot--widening (eglot--update-hints-1 (point-min) (point-max))))
+
+(cl-defun eglot--inlay-hints-lazily (&optional (buffer (current-buffer)))
+  (eglot--when-live-buffer buffer
+    (when eglot--managed-mode
+      (dolist (window (get-buffer-window-list nil nil 'visible))
+        (eglot--update-hints-1 (window-start window) (window-end window))))))
+
+(defun eglot--update-hints-1 (from to)
+  "Request LSP inlay hints and annotate current buffer from FROM to TO."
+  (let* ((buf (current-buffer))
+         (paint-hint
+          (eglot--lambda ((InlayHint) position kind label paddingLeft 
paddingRight)
+            (goto-char (eglot--lsp-position-to-point position))
+            (let ((ov (make-overlay (point) (point)))
+                  (left-pad (and paddingLeft (not (memq (char-before) '(32 
9)))))
+                  (right-pad (and paddingRight (not (memq (char-after) '(32 
9)))))
+                  (text (if (stringp label) label (plist-get label :value))))
+              (overlay-put ov 'before-string
+                           (propertize
+                            (concat (and left-pad " ") text (and right-pad " 
"))
+                            'face (pcase kind
+                                    (1 'eglot-type-hint-face)
+                                    (2 'eglot-parameter-hint-face)
+                                    (_ 'eglot-inlay-hint-face))))
+              (overlay-put ov 'eglot--inlay-hint t)
+              (overlay-put ov 'eglot--overlay t)))))
+    (jsonrpc-async-request
+     (eglot--current-server-or-lose)
+     :textDocument/inlayHint
+     (list :textDocument (eglot--TextDocumentIdentifier)
+           :range (list :start (eglot--pos-to-lsp-position from)
+                        :end (eglot--pos-to-lsp-position to)))
+     :success-fn (lambda (hints)
+                   (eglot--when-live-buffer buf
+                     (eglot--widening
+                      (remove-overlays from to 'eglot--inlay-hint t)
+                      (mapc paint-hint hints))))
+     :deferred 'eglot--update-hints-1)))
+
+(defun eglot--inlay-hints-after-scroll (window display-start)
+  (cl-macrolet ((wsetq (sym val) `(set-window-parameter window ',sym ,val))
+                (wgetq (sym) `(window-parameter window ',sym)))
+    (let ((buf (window-buffer window))
+          (timer (wgetq eglot--inlay-hints-timer))
+          (last-display-start (wgetq eglot--last-inlay-hint-display-start)))
+      (when (and eglot-lazy-inlay-hints
+                 ;; FIXME: If `window' is _not_ the selected window,
+                 ;; then for some unknown reason probably related to
+                 ;; the overlays added later to the buffer, the scroll
+                 ;; function will be called indefinitely.  Not sure if
+                 ;; an Emacs bug, but prevent useless duplicate calls
+                 ;; by saving and examining `display-start' fixes it.
+                 (not (eql last-display-start display-start)))
+        (when timer (cancel-timer timer))
+        (wsetq eglot--last-inlay-hint-display-start
+               display-start)
+        (wsetq eglot--inlay-hints-timer
+               (run-at-time
+                eglot-lazy-inlay-hints
+                nil (lambda ()
+                      (eglot--when-live-buffer buf
+                        (when (eq buf (window-buffer window))
+                          (eglot--update-hints-1 (window-start window)
+                                                 (window-end window))
+                          (wsetq eglot--inlay-hints-timer nil))))))))))
+
+(define-minor-mode eglot-inlay-hints-mode
+  "Minor mode annotating buffer with LSP inlay hints."
+  :global nil
+  (cond (eglot-inlay-hints-mode
+         (cond
+          ((not (eglot--server-capable :inlayHintProvider))
+           (eglot--warn
+            "No :inlayHintProvider support. Inlay hints will not work."))
+          (eglot-lazy-inlay-hints
+           (add-hook 'eglot--document-changed-hook
+                     #'eglot--inlay-hints-lazily t t)
+           (add-hook 'window-scroll-functions
+                     #'eglot--inlay-hints-after-scroll nil t)
+           ;; Maybe there isn't a window yet for current buffer,
+           ;; so `run-at-time' ensures this runs after redisplay.
+           (run-at-time 0 nil #'eglot--inlay-hints-lazily))
+          (t
+           (add-hook 'eglot--document-changed-hook
+                     #'eglot--inlay-hints-fully nil t)
+           (eglot--inlay-hints-fully))))
+        (t
+         (remove-hook 'eglot--document-changed-hook
+                      #'eglot--inlay-hints-lazily t)
+         (remove-hook 'eglot--document-changed-hook
+                      #'eglot--inlay-hints-fully t)
+         (remove-hook 'window-scroll-functions
+                      #'eglot--inlay-hints-after-scroll t)
+         (remove-overlays nil nil 'eglot--inlay-hint t))))
+
+
 ;;; Hacks
 ;;;
 ;; FIXME: Although desktop.el compatibility is Emacs bug#56407, the



reply via email to

[Prev in Thread] Current Thread [Next in Thread]