Eglot: fix eglot--sig-info with non-UTF-32 positionEncoding

Github-reference: https://github.com/joaotavora/eglot/discussions/1588

When the server negotiates positionEncoding utf-8 or utf-16,
ParameterInformation.label vector offsets are byte/code-unit counts
into the signature label, not character counts.  Using them raw caused
wrong highlights and crashes on Unicode-rich signatures.

* lisp/progmodes/eglot.el (eglot--sig-info): Mostly rewrite.
(eglot-move-to-utf-8-linepos-function): Tweak docstring.
(eglot-move-to-utf-8-linepos, eglot-move-to-utf-16-linepos): Return
position moved to.
This commit is contained in:
João Távora 2026-05-09 00:58:36 +01:00
parent 543d8a7a9d
commit 56f27dd9f0

View file

@ -2152,19 +2152,18 @@ LBP defaults to `eglot--bol'."
(funcall eglot-current-linepos-function))))) (funcall eglot-current-linepos-function)))))
(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos (defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
"Function to move to a position within a line reported by the LSP server. "Move point to LSP-reported position within a line.
Per the LSP spec, character offsets in LSP Position objects count Per the LSP spec, character offsets in LSP Position objects count UTF-16
UTF-16 code units, not actual code points. So when LSP says code units, not actual code points. So when LSP says position 3 of a
position 3 of a line containing just \"aXbc\", where X is a funny line containing just \"aXbc\", where X is a funny looking character in
looking character in the UTF-16 \"supplementary plane\", it the UTF-16 \"supplementary plane\", it actually means `b', not `c'. The
actually means `b', not `c'. The default value default value `eglot-move-to-utf-16-linepos' accounts for this.
`eglot-move-to-utf-16-linepos' accounts for this.
This variable can also be set to `eglot-move-to-utf-8-linepos' or This variable can also be set to `eglot-move-to-utf-8-linepos' or
`eglot-move-to-utf-32-linepos' for servers not closely following `eglot-move-to-utf-32-linepos' for servers not closely following the
the spec. Also, since LSP 3.17 server and client may agree on an spec. Also, since LSP 3.17 server and client may agree on an encoding
encoding and Eglot will set this variable automatically.") and Eglot will set this variable automatically.")
(defun eglot-move-to-utf-8-linepos (n) (defun eglot-move-to-utf-8-linepos (n)
"Move to line's Nth byte as computed by LSP's UTF-8 criterion." "Move to line's Nth byte as computed by LSP's UTF-8 criterion."
@ -2175,7 +2174,8 @@ encoding and Eglot will set this variable automatically.")
(while (and (< (position-bytes (point)) goal-byte) (< (point) eol)) (while (and (< (position-bytes (point)) goal-byte) (< (point) eol))
;; raw bytes take 2 bytes in the buffer ;; raw bytes take 2 bytes in the buffer
(when (>= (char-after) #x3fff80) (setq goal-byte (1+ goal-byte))) (when (>= (char-after) #x3fff80) (setq goal-byte (1+ goal-byte)))
(forward-char 1)))) (forward-char 1))
(point)))
(defun eglot-move-to-utf-16-linepos (n) (defun eglot-move-to-utf-16-linepos (n)
"Move to line's Nth code unit as computed by LSP's UTF-16 criterion." "Move to line's Nth code unit as computed by LSP's UTF-16 criterion."
@ -2186,7 +2186,8 @@ encoding and Eglot will set this variable automatically.")
(while (and (< (point) goal-char) (< (point) eol)) (while (and (< (point) goal-char) (< (point) eol))
;; code points in the "supplementary place" use two code units ;; code points in the "supplementary place" use two code units
(when (<= #x010000 (char-after) #x10ffff) (setq goal-char (1- goal-char))) (when (<= #x010000 (char-after) #x10ffff) (setq goal-char (1- goal-char)))
(forward-char 1)))) (forward-char 1))
(point)))
(defun eglot-move-to-utf-32-linepos (n) (defun eglot-move-to-utf-32-linepos (n)
"Move to line's Nth codepoint as computed by LSP's UTF-32 criterion." "Move to line's Nth codepoint as computed by LSP's UTF-32 criterion."
@ -4108,66 +4109,67 @@ for which LSP on-type-formatting should be requested."
(mapconcat #'eglot--format-markup (mapconcat #'eglot--format-markup
(if (vectorp contents) contents (list contents)) "\n")) (if (vectorp contents) contents (list contents)) "\n"))
(defun eglot--sig-info (sig &optional sig-active briefp) (cl-defun eglot--sig-info (sig &optional sig-active briefp
&aux (move-fn eglot-move-to-linepos-function)
first-parlabel
fpardoc)
(eglot--dbind ((SignatureInformation) (eglot--dbind ((SignatureInformation)
((:label siglabel)) ((:label siglabel))
((:documentation sigdoc)) parameters activeParameter) ((:documentation sigdoc)) parameters activeParameter)
sig sig
(with-temp-buffer (with-temp-buffer
(insert siglabel) (save-excursion
;; Add documentation, indented so we can distinguish multiple signatures ;; Insert main siglabel line
(when-let* ((doc (and (not briefp) sigdoc (eglot--format-markup sigdoc)))) (insert siglabel)
(goto-char (point-max)) ;; Add function documentation to end on a new line, indented so
(insert "\n" (replace-regexp-in-string "^" " " doc))) ;; we can distinguish multiple signatures
;; Try to highlight function name only (when-let* ((doc (and (not briefp) sigdoc (eglot--format-markup sigdoc))))
(let (first-parlabel) (goto-char (point-max))
(cond ((and (cl-plusp (length parameters)) (insert "\n" (replace-regexp-in-string "^" " " doc))))
(vectorp (setq first-parlabel ;; Back to point-min: try to highlight function name only
(plist-get (aref parameters 0) :label)))) (cond ((and (cl-plusp (length parameters))
(save-excursion (vectorp (setq first-parlabel
(goto-char (elt first-parlabel 0)) (plist-get (aref parameters 0) :label))))
(skip-syntax-backward "^w") (funcall move-fn (elt first-parlabel 0))
(add-face-text-property (point-min) (point) (skip-syntax-backward "^w")
'font-lock-function-name-face))) (add-face-text-property (point-min) (point)
((save-excursion 'font-lock-function-name-face))
(goto-char (point-min)) ((looking-at "\\([^(]*\\)([^)]*)")
(looking-at "\\([^(]*\\)([^)]*)")) (add-face-text-property (match-beginning 1) (match-end 1)
(add-face-text-property (match-beginning 1) (match-end 1) 'font-lock-function-name-face)))
'font-lock-function-name-face))))
;; Now to the parameters ;; Now to the parameters
(cl-loop (cl-loop
with active-param = (or activeParameter sig-active) with active-param = (or activeParameter sig-active)
with case-fold-search = nil
for i from 0 for parameter across parameters do for i from 0 for parameter across parameters do
(eglot--dbind ((ParameterInformation) (eglot--dbind ((ParameterInformation)
((:label parlabel)) ((:label parlabel))
((:documentation pardoc))) ((:documentation pardoc)))
parameter parameter
;; ...perhaps highlight it in the formals list (cl-flet ((parlabel-bounds ()
(when (eq i active-param) (cond ((stringp parlabel)
(save-excursion (and (search-forward parlabel (line-end-position) t)
(goto-char (point-min)) (match-data)))
(pcase-let (t (mapcar move-fn parlabel)))))
((`(,beg ,end) ;; ...perhaps highlight it in the formals list
(if (stringp parlabel) (when-let* ((b (and (eq i active-param)
(let ((case-fold-search nil)) (parlabel-bounds))))
(and (search-forward parlabel (line-end-position) t) (add-face-text-property
(list (match-beginning 0) (match-end 0)))) (car b) (cadr b)
(list (1+ (aref parlabel 0)) (1+ (aref parlabel 1)))))) 'eldoc-highlight-function-argument))
(if (and beg end) ;; ...and/or maybe add its doc on a line by its own.
(add-face-text-property
beg end
'eldoc-highlight-function-argument)))))
;; ...and/or maybe add its doc on a line by its own.
(let (fpardoc)
(when (and pardoc (not briefp) (when (and pardoc (not briefp)
(not (string-empty-p (not (string-empty-p
(setq fpardoc (eglot--format-markup pardoc))))) (setq fpardoc (eglot--format-markup pardoc)))))
(insert "\n " (unless (stringp parlabel)
(propertize (setq parlabel (apply #'buffer-substring (parlabel-bounds))))
(if (stringp parlabel) parlabel (save-excursion
(substring siglabel (aref parlabel 0) (aref parlabel 1))) (goto-char (point-max))
'face (and (eq i active-param) 'eldoc-highlight-function-argument)) (insert "\n "
": " fpardoc))))) (propertize
parlabel
'face (and (eq i active-param) 'eldoc-highlight-function-argument))
": " fpardoc))))))
(buffer-string)))) (buffer-string))))
(defun eglot-signature-eldoc-function (cb &rest _ignored) (defun eglot-signature-eldoc-function (cb &rest _ignored)