Eglot: simplify inlay hints implementation with jit-lock

This implementation is much simpler than the one based on
windows-scroll-functions.  It's also supposedly safer, as long as
jit-lock guarantees refontification of affected regions.

It's not _trivially_ simple though, as simply adding
'eglot--update-hints-1' to jit-lock-functions, while possible, is
going to request inlay hints from the LSP server for many small
regions of the buffer, depending on what jit-lock thinks is best.  So
we keep coalescing these into a larger region until the time is
suitable for a more bandwidth-efficient request.

To do this, we use a jit-lock implementation detail,
jit-lock-context-unfontify-pos, which is a proxy for knowing that the
jit-lock-context-timer has run.  Not sure how brittle it is, but it
seems to work reasonably.

We also get rid of the previous "get hints for entire buffer"
implementation.

* doc/misc/eglot.texi (Eglot Variables): Remove mention
to deleted eglot-lazy-inlay-hints.

* lisp/progmodes/eglot.el (eglot-lazy-inlay-hints)
(eglot--inlay-hints-after-scroll)
(eglot--inlay-hints-fully)
(eglot--inlay-hints-lazily): Remove.
(eglot--update-hints): Add function.
(eglot-inlay-hints-mode): Simplify.
This commit is contained in:
João Távora 2023-02-23 23:51:09 +00:00
parent 91e24c5b5a
commit b0cbd5590b
2 changed files with 33 additions and 85 deletions

View file

@ -883,14 +883,6 @@ 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

View file

@ -3489,32 +3489,39 @@ If NOERROR, return predicate, else erroring function."
(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 the buffer.
(defvar-local eglot--outstanding-inlay-hints-region (cons nil nil)
"Jit-lock-calculated (FROM . TO) region with potentially outdated hints")
Value is a number specifying how many seconds to wait after a
window has been (re)scrolled before requesting new inlay hints
for the now-visible portion of the buffer shown in the window.
(defvar-local eglot--outstanding-inlay-regions-timer nil
"Helper timer for `eglot--update-hints'")
If nil, then inlay hints are requested for the entire buffer.
This could be slow.
This value is only meaningful if the minor mode
`eglot-inlay-hints-mode' is turned on in a buffer."
: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 (from to)
"Jit-lock function for Eglot inlay hints."
(cl-symbol-macrolet ((region eglot--outstanding-inlay-hints-region)
(timer eglot--outstanding-inlay-regions-timer))
(setcar region (min (or (car region) (point-max)) from))
(setcdr region (max (or (cdr region) (point-min)) to))
;; HACK: We're relying on knowledge of jit-lock internals here. The
;; condition comparing `jit-lock-context-unfontify-pos' to
;; `point-max' is a heuristic for telling whether this call to
;; `jit-lock-functions' happens after `jit-lock-context-timer' has
;; just run. Only after this delay should we start the smoothing
;; timer that will eventually call `eglot--update-hints-1' with the
;; coalesced region. I wish we didn't need the timer, but sometimes
;; a lot of "non-contextual" calls come in all at once and do verify
;; the condition. Notice it is a 0 second timer though, so we're
;; not introducing any more delay over jit-lock's timers.
(when (= jit-lock-context-unfontify-pos (point-max))
(if timer (cancel-timer timer))
(setq timer (run-at-time
0 nil
(lambda ()
(eglot--update-hints-1 (max (car region) (point-min))
(min (cdr region) (point-max)))
(setq region (cons nil nil) timer nil)))))))
(defun eglot--update-hints-1 (from to)
"Request LSP inlay hints and annotate current buffer from FROM to TO."
"Do most work for `eglot--update-hints', including LSP request."
(let* ((buf (current-buffer))
(paint-hint
(eglot--lambda ((InlayHint) position kind label paddingLeft paddingRight)
@ -3545,67 +3552,16 @@ This value is only meaningful if the minor mode
(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))))))))))
(defun eglot--inlay-hints-after-window-config-change ()
(eglot--update-hints-1 (window-start) (window-end)))
(define-minor-mode eglot-inlay-hints-mode
"Minor mode for annotating buffers with LSP server's inlay hints."
:global nil
(cond (eglot-inlay-hints-mode
(cond
((not (eglot--server-capable :inlayHintProvider))
(if (eglot--server-capable :inlayHintProvider)
(jit-lock-register #'eglot--update-hints 'contextual)
(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)
(add-hook 'window-configuration-change-hook
#'eglot--inlay-hints-after-window-config-change 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))))
"No :inlayHintProvider support. Inlay hints will not work.")))
(t
(remove-hook 'window-configuration-change-hook
#'eglot--inlay-hints-after-window-config-change)
(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)
(jit-lock-unregister #'eglot--update-hints)
(remove-overlays nil nil 'eglot--inlay-hint t))))