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:
parent
91e24c5b5a
commit
b0cbd5590b
2 changed files with 33 additions and 85 deletions
|
@ -883,14 +883,6 @@ this map. For example:
|
||||||
(define-key eglot-mode-map (kbd "<f6>") 'xref-find-definitions)
|
(define-key eglot-mode-map (kbd "<f6>") 'xref-find-definitions)
|
||||||
@end lisp
|
@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
|
@end vtable
|
||||||
|
|
||||||
Additional variables, which are relevant for customizing the server
|
Additional variables, which are relevant for customizing the server
|
||||||
|
|
|
@ -3489,32 +3489,39 @@ If NOERROR, return predicate, else erroring function."
|
||||||
(defface eglot-parameter-hint-face '((t (:inherit eglot-inlay-hint-face)))
|
(defface eglot-parameter-hint-face '((t (:inherit eglot-inlay-hint-face)))
|
||||||
"Face used for parameter inlay hint overlays.")
|
"Face used for parameter inlay hint overlays.")
|
||||||
|
|
||||||
(defcustom eglot-lazy-inlay-hints 0.3
|
(defvar-local eglot--outstanding-inlay-hints-region (cons nil nil)
|
||||||
"If non-nil, restrict LSP inlay hints to visible portion of the buffer.
|
"Jit-lock-calculated (FROM . TO) region with potentially outdated hints")
|
||||||
|
|
||||||
Value is a number specifying how many seconds to wait after a
|
(defvar-local eglot--outstanding-inlay-regions-timer nil
|
||||||
window has been (re)scrolled before requesting new inlay hints
|
"Helper timer for `eglot--update-hints'")
|
||||||
for the now-visible portion of the buffer shown in the window.
|
|
||||||
|
|
||||||
If nil, then inlay hints are requested for the entire buffer.
|
(defun eglot--update-hints (from to)
|
||||||
This could be slow.
|
"Jit-lock function for Eglot inlay hints."
|
||||||
|
(cl-symbol-macrolet ((region eglot--outstanding-inlay-hints-region)
|
||||||
This value is only meaningful if the minor mode
|
(timer eglot--outstanding-inlay-regions-timer))
|
||||||
`eglot-inlay-hints-mode' is turned on in a buffer."
|
(setcar region (min (or (car region) (point-max)) from))
|
||||||
:type 'number
|
(setcdr region (max (or (cdr region) (point-min)) to))
|
||||||
:version "29.1")
|
;; HACK: We're relying on knowledge of jit-lock internals here. The
|
||||||
|
;; condition comparing `jit-lock-context-unfontify-pos' to
|
||||||
(defun eglot--inlay-hints-fully ()
|
;; `point-max' is a heuristic for telling whether this call to
|
||||||
(eglot--widening (eglot--update-hints-1 (point-min) (point-max))))
|
;; `jit-lock-functions' happens after `jit-lock-context-timer' has
|
||||||
|
;; just run. Only after this delay should we start the smoothing
|
||||||
(cl-defun eglot--inlay-hints-lazily (&optional (buffer (current-buffer)))
|
;; timer that will eventually call `eglot--update-hints-1' with the
|
||||||
(eglot--when-live-buffer buffer
|
;; coalesced region. I wish we didn't need the timer, but sometimes
|
||||||
(when eglot--managed-mode
|
;; a lot of "non-contextual" calls come in all at once and do verify
|
||||||
(dolist (window (get-buffer-window-list nil nil 'visible))
|
;; the condition. Notice it is a 0 second timer though, so we're
|
||||||
(eglot--update-hints-1 (window-start window) (window-end window))))))
|
;; 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)
|
(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))
|
(let* ((buf (current-buffer))
|
||||||
(paint-hint
|
(paint-hint
|
||||||
(eglot--lambda ((InlayHint) position kind label paddingLeft paddingRight)
|
(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))))
|
(mapc paint-hint hints))))
|
||||||
:deferred 'eglot--update-hints-1)))
|
: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
|
(define-minor-mode eglot-inlay-hints-mode
|
||||||
"Minor mode for annotating buffers with LSP server's inlay hints."
|
"Minor mode for annotating buffers with LSP server's inlay hints."
|
||||||
:global nil
|
:global nil
|
||||||
(cond (eglot-inlay-hints-mode
|
(cond (eglot-inlay-hints-mode
|
||||||
(cond
|
(if (eglot--server-capable :inlayHintProvider)
|
||||||
((not (eglot--server-capable :inlayHintProvider))
|
(jit-lock-register #'eglot--update-hints 'contextual)
|
||||||
(eglot--warn
|
(eglot--warn
|
||||||
"No :inlayHintProvider support. Inlay hints will not work."))
|
"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
|
(t
|
||||||
(add-hook 'eglot--document-changed-hook
|
(jit-lock-unregister #'eglot--update-hints)
|
||||||
#'eglot--inlay-hints-fully nil t)
|
|
||||||
(eglot--inlay-hints-fully))))
|
|
||||||
(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)
|
|
||||||
(remove-overlays nil nil 'eglot--inlay-hint t))))
|
(remove-overlays nil nil 'eglot--inlay-hint t))))
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue