Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.org
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
* Changelog
** Unreleased 9.0.1
* Add support for LSP 3.17 InlayHint features: ~textEdits~, ~tooltip~, ~data~, ~InlayHintLabelPart~ (per-part ~tooltip~/~location~/~command~), ~inlayHint/resolve~, interactive mouse handler, and ~lsp-inlay-hint-accept~ command (see [[https://github.com/emacs-lsp/lsp-mode/issues/4775][#4775]])
* Fix ~lsp-zig--zls-url~ to use the new zigtools.org url (see: [[https://github.com/emacs-lsp/lsp-mode/issues/4445][#4445]])
* Fix ~lsp--npm-dependency-path~ to return correct executable path and improve error message
* Add support for [[https://github.com/PlasmaFAIR/fortitude][Fortitude]] (Fortran)
Expand Down
157 changes: 135 additions & 22 deletions lsp-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,12 @@ directory")
("textDocument/documentColor" :capability :colorProvider)
("textDocument/documentLink" :capability :documentLinkProvider)
("textDocument/inlayHint" :capability :inlayHintProvider)
("inlayHint/resolve"
:check-command (lambda (wk)
(with-lsp-workspace wk
(when-let* ((cap (lsp--capability :inlayHintProvider)))
(and (not (booleanp cap))
(lsp:inlay-hint-options-resolve-provider? cap))))))
("textDocument/documentHighlight" :capability :documentHighlightProvider)
("textDocument/documentSymbol" :capability :documentSymbolProvider)
("textDocument/foldingRange" :capability :foldingRangeProvider)
Expand Down Expand Up @@ -2841,6 +2847,7 @@ BINDINGS is a list of (key def desc cond)."
;; actions
"aa" lsp-execute-code-action "code actions" (lsp-feature? "textDocument/codeAction")
"ah" lsp-document-highlight "highlight symbol" (lsp-feature? "textDocument/documentHighlight")
"ai" lsp-inlay-hint-accept "accept inlay hint" (lsp-feature? "textDocument/inlayHint")
"al" lsp-avy-lens "lens" (and (bound-and-true-p lsp-lens-mode) (featurep 'avy))

;; peeks
Expand Down Expand Up @@ -3902,7 +3909,9 @@ disappearing, unset all the variables related to it."
(relatedDocumentSupport . :json-false)))
(linkedEditingRange . ((dynamicRegistration . t)))
(inlineCompletion . ())
,@(when lsp-inlay-hint-enable '((inlayHint . ((dynamicRegistration . :json-false)))))))
,@(when lsp-inlay-hint-enable
'((inlayHint . ((dynamicRegistration . :json-false)
(resolveSupport . ((properties . ["textEdits" "tooltip"])))))))))
(window . ((workDoneProgress . t)
(showDocument . ((support . t))))))
custom-capabilities))
Expand Down Expand Up @@ -10095,7 +10104,7 @@ defaults to `progress-bar."

(defface lsp-inlay-hint-face
'((t :inherit font-lock-comment-face))
"The face to use for the JavaScript inlays."
"The face to use for inlay hints."
:group 'lsp-mode
:package-version '(lsp-mode . "9.0.0"))

Expand Down Expand Up @@ -10142,25 +10151,120 @@ modifying window sizes."
((eql kind lsp/inlay-hint-kind-parameter-hint) 'lsp-inlay-hint-parameter-face)
(t 'lsp-inlay-hint-face)))

(defun lsp--inlay-hint-tooltip-text (tooltip)
"Extract plain text from TOOLTIP (string or MarkupContent)."
(cond
((stringp tooltip) tooltip)
((lsp-markup-content? tooltip) (lsp:markup-content-value tooltip))
(t nil)))

(defun lsp--update-inlay-hints-scroll-function (window start)
(lsp-update-inlay-hints start (window-end window t)))

(defun lsp--update-inlay-hints ()
(lsp-update-inlay-hints (window-start) (window-end nil t)))

(defun lsp--label-from-inlay-hints-response (label)
"Returns a string label built from an array of
InlayHintLabelParts or the argument itself if it's already a
string."
(defun lsp--inlay-hint-label-part-string (part kind)
"Render a single InlayHintLabelPart PART with text properties for KIND."
(-let (((&InlayHintLabelPart :value :tooltip? :location? :command?) part))
(let ((str value)
(face (lsp--face-for-inlay kind))
(interactive (or location? command?)))
(apply #'propertize str
'font-lock-face face
(append
(when tooltip?
(list 'help-echo (lsp--inlay-hint-tooltip-text tooltip?)))
(when interactive
(list 'mouse-face 'highlight
'pointer 'hand))
(when location?
(list 'lsp-inlay-hint-location location?))
(when command?
(list 'lsp-inlay-hint-command command?)))))))

(defun lsp--label-from-inlay-hints-response (label kind)
"Build a propertized string from LABEL for inlay hint of KIND.
LABEL is either a string or a vector of InlayHintLabelPart."
(cl-typecase label
(string label)
(string (propertize (lsp--format-inlay label kind)
'font-lock-face (lsp--face-for-inlay kind)))
(vector
(string-join (mapcar (lambda (part)
(-let (((&InlayHintLabelPart :value) part))
value))
(lsp--inlay-hint-label-part-string part kind))
label)))))

(defun lsp--inlay-hint-resolve (hint)
"Resolve HINT by sending inlayHint/resolve if the server supports it.
Returns the resolved hint, or the original if resolve is not supported."
(if (lsp-feature? "inlayHint/resolve")
(condition-case err
(lsp-request "inlayHint/resolve" hint)
(error
(lsp--warn "Failed to resolve inlay hint: %s" (error-message-string err))
hint))
hint))

(defun lsp-inlay-hint-accept ()
"Accept the inlay hint at point, applying its text edits.
If the hint has no textEdits, tries to resolve them via inlayHint/resolve."
(interactive)
(if-let* ((overlays (overlays-in (point) (1+ (point))))
(overlay (cl-find-if (lambda (ov)
(and (overlay-get ov 'lsp-inlay-hint)
(= (overlay-start ov) (point))))
overlays))
(hint (overlay-get overlay 'lsp-inlay-hint-data)))
(let* ((resolved (if (lsp:inlay-hint-text-edits? hint)
hint
(lsp--inlay-hint-resolve hint)))
(edits (lsp:inlay-hint-text-edits? resolved)))
(if edits
(progn
(lsp--apply-text-edits edits 'inlay-hint-accept)
(lsp--update-inlay-hints))
(lsp--info "No text edits available for this inlay hint.")))
(lsp--info "No inlay hint at point.")))

(defun lsp--inlay-hint-mouse-handler (event)
"Handle mouse click EVENT on an inlay hint overlay.
Click precedence: label part location > label part command > hint accept."
(interactive "e")
(let* ((pos-data (posn-string (event-start event)))
(string (car pos-data))
(char-pos (cdr pos-data)))
(cond
;; Label part with location: navigate
((and string char-pos
(get-text-property char-pos 'lsp-inlay-hint-location string))
(-let* ((location (get-text-property char-pos 'lsp-inlay-hint-location string))
((&Location :uri :range (&Range :start)) location)
(path (lsp--uri-to-path uri)))
(xref-push-marker-stack)
(find-file path)
(goto-char (lsp--position-to-point start))))
;; Label part with command: execute
((and string char-pos
(get-text-property char-pos 'lsp-inlay-hint-command string))
(lsp--execute-command
(get-text-property char-pos 'lsp-inlay-hint-command string)))
;; Default: accept the hint (apply textEdits)
(t
(let* ((click-posn (event-start event))
(window (posn-window click-posn))
(pos (posn-point click-posn)))
(with-selected-window window
(goto-char pos)
(lsp-inlay-hint-accept)))))))

(defvar lsp--inlay-hint-mouse-map
(let ((map (make-sparse-keymap)))
(define-key map [mouse-1] #'lsp--inlay-hint-mouse-handler)
map)
"Keymap for inlay hint overlays.")

(defun lsp-update-inlay-hints (start end)
"Update inlay hints between START and END buffer positions."
(lsp-request-async
"textDocument/inlayHint"
(lsp-make-inlay-hints-params
Expand All @@ -10172,20 +10276,29 @@ string."
(lambda (res)
(lsp--remove-overlays 'lsp-inlay-hint)
(dolist (hint res)
(-let* (((&InlayHint :label :position :kind? :padding-left? :padding-right?) hint)
(-let* (((&InlayHint :label :position :kind? :padding-left? :padding-right?
:tooltip?) hint)
(kind (or kind? lsp/inlay-hint-kind-type-hint))
(label (lsp--label-from-inlay-hints-response label))
(pos (lsp--position-to-point position))
(overlay (make-overlay pos pos nil 'front-advance 'end-advance)))
(when (stringp label)
(overlay-put overlay 'lsp-inlay-hint t)
(overlay-put overlay 'before-string
(format "%s%s%s"
(if padding-left? " " "")
(propertize (lsp--format-inlay label kind)
'font-lock-face (lsp--face-for-inlay kind))
(if padding-right? " " "")))))))
:mode 'tick))
(label-str (lsp--label-from-inlay-hints-response label kind))
(pos (lsp--position-to-point position)))
(when label-str
(let ((overlay (make-overlay pos pos nil 'front-advance 'end-advance)))
(overlay-put overlay 'lsp-inlay-hint t)
(overlay-put overlay 'lsp-inlay-hint-data hint)
(overlay-put overlay 'before-string
(propertize
(format "%s%s%s"
(if padding-left? " " "")
(let ((s label-str))
;; Add hint-level tooltip as help-echo if no per-part tooltip
(when (and tooltip? (stringp label))
(setq s (propertize s 'help-echo
(lsp--inlay-hint-tooltip-text tooltip?))))
s)
(if padding-right? " " ""))
'keymap lsp--inlay-hint-mouse-map)))))))
:mode 'tick
:cancel-token :inlay-hints))

(define-minor-mode lsp-inlay-hints-mode
"Mode for displaying inlay hints."
Expand Down
3 changes: 2 additions & 1 deletion lsp-protocol.el
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,8 @@ See `-let' for a description of the destructuring mechanism."
;; 3.17
(RelativePattern (:baseUri :pattern) nil)
(LabelDetails nil (:detail :description))
(InlayHint (:label :position) (:kind :paddingLeft :paddingRight))
(InlayHintOptions nil (:resolveProvider))
(InlayHint (:label :position) (:kind :paddingLeft :paddingRight :textEdits :tooltip :data))
(InlayHintLabelPart (:value) (:tooltip :location :command))
(InlayHintsParams (:textDocument) (:range))
;; 3.18
Expand Down
85 changes: 85 additions & 0 deletions test/lsp-mock-server-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,91 @@ line 3 words here and here
(should (equal (line-number-at-pos) (1+ hint-line)))
(should (equal (current-column) hint-col))))))))

(ert-deftest lsp-mock-server-inlay-hint-stores-data ()
"Inlay hint overlays store the full hint data for later resolve/accept."
(let ((lsp-inlay-hint-enable t)
(hint-line 2)
(hint-col 10))
(lsp-mock-run-with-mock-server
(lsp-mock-with-temp-window
(current-buffer)
(lambda ()
(lsp-test-schedule-response
"textDocument/inlayHint"
(vconcat (list `(:kind 1
:position (:line ,hint-line :character ,hint-col)
:paddingLeft ()
:label "my_type"
:textEdits [(:range (:start (:line ,hint-line :character ,hint-col)
:end (:line ,hint-line :character ,hint-col))
:newText ": my_type")]))))
(run-hooks 'lsp-on-idle-hook)
(lsp-test-sync-wait (progn (should (lsp-workspaces))
(lsp-test-all-overlays 'lsp-inlay-hint)))
(let* ((hints (lsp-test-all-overlays 'lsp-inlay-hint))
(hint-overlay (car hints))
(hint-data (overlay-get hint-overlay 'lsp-inlay-hint-data)))
(should (eq (length hints) 1))
(should hint-data)
(should (lsp:inlay-hint-text-edits? hint-data))))))))

(ert-deftest lsp-mock-server-inlay-hint-label-parts ()
"Inlay hints with label parts render per-part text properties."
(let ((lsp-inlay-hint-enable t)
(hint-line 2)
(hint-col 10))
(lsp-mock-run-with-mock-server
(lsp-mock-with-temp-window
(current-buffer)
(lambda ()
(lsp-test-schedule-response
"textDocument/inlayHint"
(vconcat (list `(:kind 1
:position (:line ,hint-line :character ,hint-col)
:paddingLeft ()
:label [(:value "Vec" :tooltip "A vector type")
(:value "<" )
(:value "String")
(:value ">")]))))
(run-hooks 'lsp-on-idle-hook)
(lsp-test-sync-wait (progn (should (lsp-workspaces))
(lsp-test-all-overlays 'lsp-inlay-hint)))
(let* ((hints (lsp-test-all-overlays 'lsp-inlay-hint))
(hint-overlay (car hints))
(before-str (overlay-get hint-overlay 'before-string)))
(should (eq (length hints) 1))
;; The before-string should contain the concatenated parts
(should (string-match-p "Vec" before-str))
(should (string-match-p "String" before-str))
;; First part "Vec" should have help-echo from its tooltip
(should (get-text-property 0 'help-echo before-str))))))))

(ert-deftest lsp-mock-server-inlay-hint-has-keymap ()
"Inlay hint overlays have a mouse keymap for click interaction."
(let ((lsp-inlay-hint-enable t)
(hint-line 2)
(hint-col 10))
(lsp-mock-run-with-mock-server
(lsp-mock-with-temp-window
(current-buffer)
(lambda ()
(lsp-test-schedule-response
"textDocument/inlayHint"
(vconcat (list `(:kind 2
:position (:line ,hint-line :character ,hint-col)
:paddingLeft ()
:label "param"))))
(run-hooks 'lsp-on-idle-hook)
(lsp-test-sync-wait (progn (should (lsp-workspaces))
(lsp-test-all-overlays 'lsp-inlay-hint)))
(let* ((hints (lsp-test-all-overlays 'lsp-inlay-hint))
(hint-overlay (car hints))
(before-str (overlay-get hint-overlay 'before-string))
(keymap (get-text-property 0 'keymap before-str)))
(should (eq (length hints) 1))
(should keymap)
(should (keymapp keymap))))))))

(ert-deftest lsp-mock-server-provides-code-lens ()
"lsp-mode accepts code lenses from the server and displays them."
(let ((line 2))
Expand Down
2 changes: 1 addition & 1 deletion test/mock-lsp-server.el
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
:codeActionProvider t
:declarationProvider t
:definitionProvider t
:inlayHintProvider t
:inlayHintProvider (:resolveProvider t)
:codeLensProvider (:resolveProvider ()))
"Capabilities of the server.")

Expand Down
Loading