aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2023-02-21 14:14:05 +0000
committerJoão Távora2023-02-22 13:40:39 +0000
commite5c3d80355c19f39169c05599852df413d723d52 (patch)
tree136c19219e19c9c1d8381c4f3d01d35ed965ce9a
parent0d5440ef678259fc69a642061c4310cdbd0fa256 (diff)
downloademacs-scratch/eglot-inlay-hints.tar.gz
emacs-scratch/eglot-inlay-hints.zip
Eglot: implement inlay hints (bug#61412, bug#61066)scratch/eglot-inlay-hints
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 not have to remember these things by heart. 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, by default this inlay feature is turned ON, which is the usual practice of Eglot when facilities are in place to enable a given feature. Although this can be tweaked by the user via 'eglot-ignored-server-capabilities' as usual, it's possible this feature is visually suprising enough to warrant an exception to the usual rule. 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. * doc/misc/eglot.texi (Eglot Features): Mention inlay hints. (Eglot Variables): Mention eglot-lazy-inlay-hints.
-rw-r--r--doc/misc/eglot.texi17
-rw-r--r--lisp/progmodes/eglot.el145
2 files changed, 155 insertions, 7 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
502of code applicable actions at point. 502of code applicable actions at point.
503@end table 503@end table
504 504
505@item M-x eglot-inlay-hints-mode
506This command toggles LSP ``inlay hints'' on and off for the current
507buffer. Inlay hints are small text annotations to specific parts of
508the whole buffer, not unlike diagnostics, but designed to help
509readability instead of indicating problems. For example, a C++ LSP
510server can serve hints about positional parameter names in function
511calls and a variable's automatically deduced type. Inlay hints help
512the user not have to remember these things by heart.
513
505@end itemize 514@end itemize
506 515
507Not all servers support the full set of LSP capabilities, but most of 516Not all servers support the full set of LSP capabilities, but most of
@@ -874,6 +883,14 @@ this map. For example:
874 (define-key eglot-mode-map (kbd "<f6>") 'xref-find-definitions) 883 (define-key eglot-mode-map (kbd "<f6>") 'xref-find-definitions)
875@end lisp 884@end lisp
876 885
886@item eglot-lazy-inlay-hints
887This variable controls the operation and performance of LSP Inlay
888Hints (@pxref{Eglot Features}). If non-@code{nil}, it specifies how
889much time to wait after a window is displayed or scrolled before
890requesting hints for that visible portion of a given buffer. If
891@code{nil}, inlay hints are always requested for the whole buffer,
892even for parts of it not currently visible.
893
877@end vtable 894@end vtable
878 895
879Additional variables, which are relevant for customizing the server 896Additional variables, which are relevant for customizing the server
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 668eea74e2e..18f2ed056c2 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -47,9 +47,10 @@
47;; definition-chasing, Flymake for diagnostics, Eldoc for at-point 47;; definition-chasing, Flymake for diagnostics, Eldoc for at-point
48;; documentation, etc. Eglot's job is generally *not* to provide 48;; documentation, etc. Eglot's job is generally *not* to provide
49;; such a UI itself, though a small number of simple 49;; such a UI itself, though a small number of simple
50;; counter-examples do exist, for example in the `eglot-rename' 50;; counter-examples do exist, e.g. in the `eglot-rename' command or
51;; command. When a new UI is evidently needed, consider adding a 51;; the `eglot-inlay-hints-mode' minor mode. When a new UI is
52;; new package to Emacs, or extending an existing one. 52;; evidently needed, consider adding a new package to Emacs, or
53;; extending an existing one.
53;; 54;;
54;; * Eglot was designed to function with just the UI facilities found 55;; * Eglot was designed to function with just the UI facilities found
55;; in the latest Emacs core, as long as those facilities are also 56;; 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.")
483 (VersionedTextDocumentIdentifier (:uri :version) ()) 484 (VersionedTextDocumentIdentifier (:uri :version) ())
484 (WorkDoneProgress (:kind) (:title :message :percentage :cancellable)) 485 (WorkDoneProgress (:kind) (:title :message :percentage :cancellable))
485 (WorkspaceEdit () (:changes :documentChanges)) 486 (WorkspaceEdit () (:changes :documentChanges))
486 (WorkspaceSymbol (:name :kind) (:containerName :location :data))) 487 (WorkspaceSymbol (:name :kind) (:containerName :location :data))
488 (InlayHint (:position :label) (:kind :textEdits :tooltip :paddingLeft
489 :paddingRight :data)))
487 "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. 490 "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces.
488 491
489INTERFACE-NAME is a symbol designated by the spec as 492INTERFACE-NAME is a symbol designated by the spec as
@@ -803,6 +806,7 @@ treated as in `eglot--dbind'."
803 :formatting `(:dynamicRegistration :json-false) 806 :formatting `(:dynamicRegistration :json-false)
804 :rangeFormatting `(:dynamicRegistration :json-false) 807 :rangeFormatting `(:dynamicRegistration :json-false)
805 :rename `(:dynamicRegistration :json-false) 808 :rename `(:dynamicRegistration :json-false)
809 :inlayHint `(:dynamicRegistration :json-false)
806 :publishDiagnostics (list :relatedInformation :json-false 810 :publishDiagnostics (list :relatedInformation :json-false
807 ;; TODO: We can support :codeDescription after 811 ;; TODO: We can support :codeDescription after
808 ;; adding an appropriate UI to 812 ;; adding an appropriate UI to
@@ -1625,7 +1629,8 @@ under cursor."
1625 (const :tag "Highlight links in document" :documentLinkProvider) 1629 (const :tag "Highlight links in document" :documentLinkProvider)
1626 (const :tag "Decorate color references" :colorProvider) 1630 (const :tag "Decorate color references" :colorProvider)
1627 (const :tag "Fold regions of buffer" :foldingRangeProvider) 1631 (const :tag "Fold regions of buffer" :foldingRangeProvider)
1628 (const :tag "Execute custom commands" :executeCommandProvider))) 1632 (const :tag "Execute custom commands" :executeCommandProvider)
1633 (const :tag "Inlay hints" :inlayHintProvider)))
1629 1634
1630(defun eglot--server-capable (&rest feats) 1635(defun eglot--server-capable (&rest feats)
1631 "Determine if current server is capable of FEATS." 1636 "Determine if current server is capable of FEATS."
@@ -1853,7 +1858,9 @@ If it is activated, also signal textDocument/didOpen."
1853 (when (and buffer-file-name (eglot-current-server)) 1858 (when (and buffer-file-name (eglot-current-server))
1854 (setq eglot--diagnostics nil) 1859 (setq eglot--diagnostics nil)
1855 (eglot--managed-mode) 1860 (eglot--managed-mode)
1856 (eglot--signal-textDocument/didOpen)))) 1861 (eglot--signal-textDocument/didOpen)
1862 (when (eglot--server-capable :inlayHintProvider)
1863 (eglot-inlay-hints-mode 1)))))
1857 1864
1858(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) 1865(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode)
1859(add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) 1866(add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode)
@@ -2279,6 +2286,7 @@ THINGS are either registrations or unregisterations (sic)."
2279 2286
2280(defun eglot--before-change (beg end) 2287(defun eglot--before-change (beg end)
2281 "Hook onto `before-change-functions' with BEG and END." 2288 "Hook onto `before-change-functions' with BEG and END."
2289 (remove-overlays beg end 'eglot--overlay t)
2282 (when (listp eglot--recent-changes) 2290 (when (listp eglot--recent-changes)
2283 ;; Records BEG and END, crucially convert them into LSP 2291 ;; Records BEG and END, crucially convert them into LSP
2284 ;; (line/char) positions before that information is lost (because 2292 ;; (line/char) positions before that information is lost (because
@@ -2291,6 +2299,9 @@ THINGS are either registrations or unregisterations (sic)."
2291 (,end . ,(copy-marker end t))) 2299 (,end . ,(copy-marker end t)))
2292 eglot--recent-changes))) 2300 eglot--recent-changes)))
2293 2301
2302(defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange)
2303 "Internal hook for doing things when the document changes.")
2304
2294(defun eglot--after-change (beg end pre-change-length) 2305(defun eglot--after-change (beg end pre-change-length)
2295 "Hook onto `after-change-functions'. 2306 "Hook onto `after-change-functions'.
2296Records BEG, END and PRE-CHANGE-LENGTH locally." 2307Records BEG, END and PRE-CHANGE-LENGTH locally."
@@ -2331,7 +2342,7 @@ Records BEG, END and PRE-CHANGE-LENGTH locally."
2331 eglot-send-changes-idle-time 2342 eglot-send-changes-idle-time
2332 nil (lambda () (eglot--when-live-buffer buf 2343 nil (lambda () (eglot--when-live-buffer buf
2333 (when eglot--managed-mode 2344 (when eglot--managed-mode
2334 (eglot--signal-textDocument/didChange) 2345 (run-hooks 'eglot--document-changed-hook)
2335 (setq eglot--change-idle-timer nil)))))))) 2346 (setq eglot--change-idle-timer nil))))))))
2336 2347
2337;; HACK! Launching a deferred sync request with outstanding changes is a 2348;; HACK! Launching a deferred sync request with outstanding changes is a
@@ -3459,6 +3470,126 @@ If NOERROR, return predicate, else erroring function."
3459 (pop-to-buffer (current-buffer))))) 3470 (pop-to-buffer (current-buffer)))))
3460 3471
3461 3472
3473;;; Inlay hints
3474(defface eglot-inlay-hint-face '((t (:height 0.8 :inherit shadow)))
3475 "Face used for inlay hint overlays.")
3476
3477(defface eglot-type-hint-face '((t (:inherit eglot-inlay-hint-face)))
3478 "Face used for type inlay hint overlays.")
3479
3480(defface eglot-parameter-hint-face '((t (:inherit eglot-inlay-hint-face)))
3481 "Face used for parameter inlay hint overlays.")
3482
3483(defcustom eglot-lazy-inlay-hints 0.3
3484 "If non-nil, restrict LSP inlay hints to visible portion of buffer.
3485
3486Value is number specifying how many seconds to wait after a
3487window has been (re)scrolled before requesting new inlay hints
3488for the visible region of the window being manipulated.
3489
3490If nil, then inlay hints are requested for the entire buffer.
3491
3492This value is only meaningful if the minor mode
3493`eglot-inlay-hints-mode' is true.
3494"
3495 :type 'number
3496 :version "29.1")
3497
3498(defun eglot--inlay-hints-fully ()
3499 (eglot--widening (eglot--update-hints-1 (point-min) (point-max))))
3500
3501(cl-defun eglot--inlay-hints-lazily (&optional (buffer (current-buffer)))
3502 (eglot--when-live-buffer buffer
3503 (when eglot--managed-mode
3504 (dolist (window (get-buffer-window-list nil nil 'visible))
3505 (eglot--update-hints-1 (window-start window) (window-end window))))))
3506
3507(defun eglot--update-hints-1 (from to)
3508 "Request LSP inlay hints and annotate current buffer from FROM to TO."
3509 (let* ((buf (current-buffer))
3510 (paint-hint
3511 (eglot--lambda ((InlayHint) position kind label paddingLeft paddingRight)
3512 (goto-char (eglot--lsp-position-to-point position))
3513 (let ((ov (make-overlay (point) (point)))
3514 (left-pad (and paddingLeft (not (memq (char-before) '(32 9)))))
3515 (right-pad (and paddingRight (not (memq (char-after) '(32 9)))))
3516 (text (if (stringp label) label (plist-get label :value))))
3517 (overlay-put ov 'before-string
3518 (propertize
3519 (concat (and left-pad " ") text (and right-pad " "))
3520 'face (pcase kind
3521 (1 'eglot-type-hint-face)
3522 (2 'eglot-parameter-hint-face)
3523 (_ 'eglot-inlay-hint-face))))
3524 (overlay-put ov 'eglot--inlay-hint t)
3525 (overlay-put ov 'eglot--overlay t)))))
3526 (jsonrpc-async-request
3527 (eglot--current-server-or-lose)
3528 :textDocument/inlayHint
3529 (list :textDocument (eglot--TextDocumentIdentifier)
3530 :range (list :start (eglot--pos-to-lsp-position from)
3531 :end (eglot--pos-to-lsp-position to)))
3532 :success-fn (lambda (hints)
3533 (eglot--when-live-buffer buf
3534 (eglot--widening
3535 (remove-overlays from to 'eglot--inlay-hint t)
3536 (mapc paint-hint hints))))
3537 :deferred 'eglot--update-hints-1)))
3538
3539(defun eglot--inlay-hints-after-scroll (window display-start)
3540 (cl-macrolet ((wsetq (sym val) `(set-window-parameter window ',sym ,val))
3541 (wgetq (sym) `(window-parameter window ',sym)))
3542 (let ((buf (window-buffer window))
3543 (timer (wgetq eglot--inlay-hints-timer))
3544 (last-display-start (wgetq eglot--last-inlay-hint-display-start)))
3545 (when (and eglot-lazy-inlay-hints
3546 ;; FIXME: If `window' is _not_ the selected window,
3547 ;; then for some unknown reason probably related to
3548 ;; the overlays added later to the buffer, the scroll
3549 ;; function will be called indefinitely. Not sure if
3550 ;; an Emacs bug, but prevent useless duplicate calls
3551 ;; by saving and examining `display-start' fixes it.
3552 (not (eql last-display-start display-start)))
3553 (when timer (cancel-timer timer))
3554 (wsetq eglot--last-inlay-hint-display-start
3555 display-start)
3556 (wsetq eglot--inlay-hints-timer
3557 (run-at-time
3558 eglot-lazy-inlay-hints
3559 nil (lambda ()
3560 (eglot--when-live-buffer buf
3561 (when (eq buf (window-buffer window))
3562 (eglot--update-hints-1 (window-start window)
3563 (window-end window))
3564 (wsetq eglot--inlay-hints-timer nil))))))))))
3565
3566(define-minor-mode eglot-inlay-hints-mode
3567 "Minor mode annotating buffer with LSP inlay hints."
3568 :global nil
3569 (cond (eglot-inlay-hints-mode
3570 (eglot--server-capable-or-lose :inlayHintProvider)
3571 (cond (eglot-lazy-inlay-hints
3572 (add-hook 'eglot--document-changed-hook
3573 #'eglot--inlay-hints-lazily t t)
3574 (add-hook 'window-scroll-functions
3575 #'eglot--inlay-hints-after-scroll nil t)
3576 ;; Maybe there isn't a window yet for current buffer,
3577 ;; so `run-at-time' ensures this runs after redisplay.
3578 (run-at-time 0 nil #'eglot--inlay-hints-lazily))
3579 (t
3580 (add-hook 'eglot--document-changed-hook
3581 #'eglot--inlay-hints-fully nil t)
3582 (eglot--inlay-hints-fully))))
3583 (t
3584 (remove-hook 'eglot--document-changed-hook
3585 #'eglot--inlay-hints-lazily t)
3586 (remove-hook 'eglot--document-changed-hook
3587 #'eglot--inlay-hints-fully t)
3588 (remove-hook 'window-scroll-functions
3589 #'eglot--inlay-hints-after-scroll t)
3590 (remove-overlays nil nil 'eglot--inlay-hint t))))
3591
3592
3462;;; Hacks 3593;;; Hacks
3463;;; 3594;;;
3464;; FIXME: Although desktop.el compatibility is Emacs bug#56407, the 3595;; FIXME: Although desktop.el compatibility is Emacs bug#56407, the