From b0cbd5590b238fa9001e3f07b7035704ef976722 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 23 Feb 2023 23:51:09 +0000 Subject: [PATCH] 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. --- doc/misc/eglot.texi | 8 --- lisp/progmodes/eglot.el | 114 ++++++++++++---------------------------- 2 files changed, 35 insertions(+), 87 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 38c6adaf131..eed9744b9f0 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -883,14 +883,6 @@ this map. For example: (define-key eglot-mode-map (kbd "") '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 eea8be6d1aa..9d722047b47 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -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. - -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. - -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)))))) +(defvar-local eglot--outstanding-inlay-hints-region (cons nil nil) + "Jit-lock-calculated (FROM . TO) region with potentially outdated hints") + +(defvar-local eglot--outstanding-inlay-regions-timer nil + "Helper timer for `eglot--update-hints'") + +(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)))) -- 2.39.5