diff options
| -rw-r--r-- | doc/misc/eglot.texi | 17 | ||||
| -rw-r--r-- | lisp/progmodes/eglot.el | 145 |
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 | |||
| 502 | of code applicable actions at point. | 502 | of code applicable actions at point. |
| 503 | @end table | 503 | @end table |
| 504 | 504 | ||
| 505 | @item M-x eglot-inlay-hints-mode | ||
| 506 | This command toggles LSP ``inlay hints'' on and off for the current | ||
| 507 | buffer. Inlay hints are small text annotations to specific parts of | ||
| 508 | the whole buffer, not unlike diagnostics, but designed to help | ||
| 509 | readability instead of indicating problems. For example, a C++ LSP | ||
| 510 | server can serve hints about positional parameter names in function | ||
| 511 | calls and a variable's automatically deduced type. Inlay hints help | ||
| 512 | the user not have to remember these things by heart. | ||
| 513 | |||
| 505 | @end itemize | 514 | @end itemize |
| 506 | 515 | ||
| 507 | Not all servers support the full set of LSP capabilities, but most of | 516 | Not 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 | ||
| 887 | This variable controls the operation and performance of LSP Inlay | ||
| 888 | Hints (@pxref{Eglot Features}). If non-@code{nil}, it specifies how | ||
| 889 | much time to wait after a window is displayed or scrolled before | ||
| 890 | requesting hints for that visible portion of a given buffer. If | ||
| 891 | @code{nil}, inlay hints are always requested for the whole buffer, | ||
| 892 | even for parts of it not currently visible. | ||
| 893 | |||
| 877 | @end vtable | 894 | @end vtable |
| 878 | 895 | ||
| 879 | Additional variables, which are relevant for customizing the server | 896 | Additional 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 | ||
| 489 | INTERFACE-NAME is a symbol designated by the spec as | 492 | INTERFACE-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'. |
| 2296 | Records BEG, END and PRE-CHANGE-LENGTH locally." | 2307 | Records 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 | |||
| 3486 | Value is number specifying how many seconds to wait after a | ||
| 3487 | window has been (re)scrolled before requesting new inlay hints | ||
| 3488 | for the visible region of the window being manipulated. | ||
| 3489 | |||
| 3490 | If nil, then inlay hints are requested for the entire buffer. | ||
| 3491 | |||
| 3492 | This 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 |