1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-05-31 01:32:00 -07:00

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