Skip to content

Commit 7299a33

Browse files
committed
Sync eglot.el and eglot-tests.el from upstream
1 parent ad7e201 commit 7299a33

2 files changed

Lines changed: 1202 additions & 388 deletions

File tree

eglot-tests.el

Lines changed: 175 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; eglot-tests.el --- Tests for eglot.el -*- lexical-binding: t; -*-
22

3-
;; Copyright (C) 2018-2025 Free Software Foundation, Inc.
3+
;; Copyright (C) 2018-2026 Free Software Foundation, Inc.
44

55
;; Author: João Távora <joaotavora@gmail.com>
66
;; Keywords: tests
@@ -185,7 +185,9 @@ directory hierarchy."
185185
(funcall fn)))
186186
(cancel-timer timer)
187187
(when (eq retval timed-out)
188-
(error "%s" (concat "Timed out " message))))))
188+
(if (getenv "EMACS_EMBA_CI")
189+
(ert-skip (concat "Timed out " message))
190+
(error "%s" (concat "Timed out " message)))))))
189191

190192
(defun eglot--find-file-noselect (file &optional noerror)
191193
(unless (or noerror
@@ -236,39 +238,47 @@ directory hierarchy."
236238
,@body)
237239
(remove-hook 'jsonrpc-event-hook #',log-event-hook-sym))))))
238240

239-
(cl-defmacro eglot--wait-for ((events-sym &optional (timeout 1) message) args &body body)
241+
(cl-defmacro eglot--wait-for ((events-sym &optional (timeout 1) message)
242+
args &body body)
240243
(declare (indent 2) (debug (sexp sexp sexp &rest form)))
241-
`(eglot--with-timeout '(,timeout ,(or message
242-
(format "waiting for:\n%s" (pp-to-string body))))
244+
`(eglot--with-timeout '(,timeout
245+
,(or message
246+
(format "waiting for:\n%s" (pp-to-string body))))
243247
(eglot--test-message "waiting for `%s'" (with-output-to-string
244248
(mapc #'princ ',body)))
245-
(let ((events
246-
(cl-loop thereis (cl-loop for json in ,events-sym
247-
for method = (plist-get json :method)
248-
when (keywordp method)
249-
do (plist-put json :method
250-
(substring
251-
(symbol-name method)
252-
1))
253-
when (funcall
254-
(jsonrpc-lambda ,args ,@body) json)
255-
return (cons json before)
256-
collect json into before)
257-
for i from 0
258-
when (zerop (mod i 5))
259-
;; do (eglot--test-message "still struggling to find in %s"
260-
;; ,events-sym)
261-
do
262-
;; `read-event' is essential to have the file
263-
;; watchers come through.
264-
(cond ((fboundp 'flush-standard-output)
265-
(read-event nil nil 0.1) (princ ".")
266-
(flush-standard-output))
267-
(t
268-
(read-event "." nil 0.1)))
269-
(accept-process-output nil 0.1))))
270-
(setq ,events-sym (cdr events))
271-
(cl-destructuring-bind (&key method id &allow-other-keys) (car events)
249+
(let ((probe
250+
(cl-loop
251+
thereis
252+
(cl-loop for (json . tail) on ,events-sym
253+
for method = (plist-get json :method)
254+
when (keywordp method)
255+
do (plist-put
256+
json :method (substring (symbol-name method) 1))
257+
when (funcall (jsonrpc-lambda ,args ,@body) json)
258+
return json
259+
do
260+
(unless
261+
;; $/progress is *truly* uninteresting and spammy
262+
(and (string-match "$/progress" (format "%s" method)))
263+
(eglot--test-message
264+
"skip uninteresting event %s[%s]"
265+
(plist-get json :method)
266+
(plist-get json :id)))
267+
finally (setq ,events-sym tail))
268+
for i from 0
269+
when (zerop (mod i 5))
270+
;; do (eglot--test-message "still struggling to find in %s"
271+
;; ,events-sym)
272+
do
273+
;; `read-event' is essential to have the file
274+
;; watchers come through.
275+
(cond ((fboundp 'flush-standard-output)
276+
(read-event nil nil 0.1) (princ ".")
277+
(flush-standard-output))
278+
(t
279+
(read-event "." nil 0.1)))
280+
(accept-process-output nil 0.1))))
281+
(cl-destructuring-bind (&key method id &allow-other-keys) probe
272282
(eglot--test-message "detected: %s"
273283
(or method (and id (format "id=%s" id))))))))
274284

@@ -284,10 +294,13 @@ directory hierarchy."
284294
(define-derived-mode typescript-mode prog-mode "TypeScript")
285295
(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-mode)))
286296

287-
(defun eglot--tests-connect (&optional timeout)
297+
(cl-defun eglot--tests-connect (&key timeout server)
288298
(let* ((timeout (or timeout 10))
289299
(eglot-sync-connect t)
290-
(eglot-connect-timeout timeout))
300+
(eglot-connect-timeout timeout)
301+
(eglot-server-programs
302+
(if server `((,major-mode . ,(string-split server)))
303+
eglot-server-programs)))
291304
(apply #'eglot--connect (eglot--guess-contact))))
292305

293306
(defun eglot--simulate-key-event (char)
@@ -315,7 +328,7 @@ directory hierarchy."
315328
(with-current-buffer
316329
(eglot--find-file-noselect "project/src/main/java/foo/Main.java")
317330
(eglot--sniffing (:server-notifications s-notifs)
318-
(should (eglot--tests-connect 20))
331+
(should (eglot--tests-connect :timeout 20))
319332
(eglot--wait-for (s-notifs 10)
320333
(&key _id method &allow-other-keys)
321334
(string= method "language/status"))))))
@@ -429,15 +442,69 @@ directory hierarchy."
429442
(with-current-buffer
430443
(eglot--find-file-noselect "diag-project/main.c")
431444
(eglot--sniffing (:server-notifications s-notifs)
432-
(eglot--tests-connect)
445+
(eglot--tests-connect :server "clangd")
446+
(flymake-start)
433447
(eglot--wait-for (s-notifs 10)
434-
(&key _id method &allow-other-keys)
448+
(&key method &allow-other-keys)
435449
(string= method "textDocument/publishDiagnostics"))
450+
(goto-char (point-min))
451+
(flymake-goto-next-error 1 '() t)
452+
(should (eq 'flymake-error (face-at-point)))))))
453+
454+
(ert-deftest eglot-test-basic-pull-diagnostics ()
455+
"Test basic diagnostics."
456+
(skip-unless (executable-find "ty"))
457+
(eglot--with-fixture
458+
`(("diag-project" .
459+
(("main.py" . "def main:\npuss"))))
460+
(with-current-buffer
461+
(eglot--find-file-noselect "diag-project/main.py")
462+
(eglot--sniffing (:server-replies s-replies)
463+
(eglot--tests-connect :server "ty server")
436464
(flymake-start)
465+
(eglot--wait-for (s-replies 5)
466+
(&key _id method &allow-other-keys)
467+
(string= method "textDocument/diagnostic"))
437468
(goto-char (point-min))
438469
(flymake-goto-next-error 1 '() t)
439470
(should (eq 'flymake-error (face-at-point)))))))
440471

472+
(ert-deftest eglot-test-basic-stream-diagnostics ()
473+
"Test basic diagnostics."
474+
(skip-unless (executable-find "rass"))
475+
(skip-unless (executable-find "ruff"))
476+
(skip-unless (executable-find "ty"))
477+
(eglot--with-fixture
478+
`(("diag-project" .
479+
(("main.py" . "from lib import greet\ndef main():\n greet()")
480+
("lib.py" . "def geet():\n print('hello')"))))
481+
(set-buffer (eglot--find-file-noselect "diag-project/main.py"))
482+
(eglot--sniffing (:server-notifications s-notifs)
483+
(eglot--tests-connect :server "rass -- ty server -- ruff server")
484+
(flymake-start)
485+
(cl-loop repeat 2 ;; 2 stream notifs for 2 rass servers
486+
do (eglot--wait-for (s-notifs 5)
487+
(&key method &allow-other-keys)
488+
(string= method "$/streamDiagnostics")))
489+
(goto-char (point-min))
490+
(flymake-goto-next-error 1 '() t)
491+
(should (eq 'flymake-error (face-at-point))))
492+
493+
;; Now fix it
494+
(set-buffer (eglot--find-file-noselect "lib.py"))
495+
(search-forward "geet")
496+
(replace-match "greet")
497+
(eglot--sniffing (:server-notifications s-notifs)
498+
(eglot--signal-textDocument/didChange)
499+
(set-buffer (eglot--find-file-noselect "main.py"))
500+
(flymake-start)
501+
(cl-loop repeat 2
502+
do (eglot--wait-for (s-notifs 5)
503+
(&key method &allow-other-keys)
504+
(string= method "$/streamDiagnostics")))
505+
(goto-char (point-min))
506+
(should-error (flymake-goto-next-error 1 '() t)))))
507+
441508
(ert-deftest eglot-test-basic-symlink ()
442509
"Test basic symlink support."
443510
(skip-unless (executable-find "clangd"))
@@ -708,6 +775,7 @@ directory hierarchy."
708775
;; This originally appeared in github#1339
709776
(skip-unless (executable-find "rust-analyzer"))
710777
(skip-unless (executable-find "cargo"))
778+
(skip-when (getenv "EMACS_EMBA_CI"))
711779
(eglot--with-fixture
712780
'(("cmpl-project" .
713781
(("main.rs" .
@@ -1002,26 +1070,31 @@ int main() {
10021070
(eglot--with-fixture
10031071
'(("project" .
10041072
(("foo.c" . "const char write_data[] = u8\"🚂🚃🚄🚅🚆🚈🚇🚈🚉🚊🚋🚌🚎🚝🚞🚟🚠🚡🛤🛲\";"))))
1005-
(let ((eglot-server-programs
1073+
(let (expected-column
1074+
(eglot-server-programs
10061075
'((c-mode . ("clangd")))))
10071076
(with-current-buffer
10081077
(eglot--find-file-noselect "project/foo.c")
1009-
(setq-local eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos)
1010-
(setq-local eglot-current-linepos-function #'eglot-utf-16-linepos)
10111078
(eglot--sniffing (:client-notifications c-notifs)
10121079
(eglot--tests-connect)
10131080
(end-of-line)
1081+
1082+
;; will be 71 if utf-16 was negotiated, 51 if utf-32,
1083+
;; something else if utf-8
1084+
(setq expected-column (funcall eglot-current-linepos-function))
1085+
(eglot--test-message
1086+
"Looks like we negotiated %S as the offset encoding"
1087+
(list eglot-move-to-linepos-function eglot-current-linepos-function))
10141088
(insert "p ")
10151089
(eglot--signal-textDocument/didChange)
10161090
(eglot--wait-for (c-notifs 2) (&key params &allow-other-keys)
1017-
(message "PARAMS=%S" params)
1018-
(should (equal 71 (eglot-tests--get
1091+
(should (equal expected-column
1092+
(eglot-tests--get
10191093
params
10201094
'(:contentChanges 0
10211095
:range :start :character)))))
10221096
(beginning-of-line)
1023-
(should (eq eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos))
1024-
(funcall eglot-move-to-linepos-function 71)
1097+
(funcall eglot-move-to-linepos-function expected-column)
10251098
(should (looking-at "p")))))))
10261099

10271100
(ert-deftest eglot-test-lsp-abiding-column ()
@@ -1061,7 +1134,7 @@ int main() {
10611134
(let ((eglot-sync-connect t)
10621135
(eglot-server-programs
10631136
`((c-mode . ("sh" "-c" "sleep 1 && clangd")))))
1064-
(should (eglot--tests-connect 3))))))
1137+
(should (eglot--tests-connect :timeout 3))))))
10651138

10661139
(ert-deftest eglot-test-slow-sync-connection-intime ()
10671140
"Connect synchronously with `eglot-sync-connect' set to 2."
@@ -1073,7 +1146,7 @@ int main() {
10731146
(let ((eglot-sync-connect 2)
10741147
(eglot-server-programs
10751148
`((c-mode . ("sh" "-c" "sleep 1 && clangd")))))
1076-
(should (eglot--tests-connect 3))))))
1149+
(should (eglot--tests-connect :timeout 3))))))
10771150

10781151
(ert-deftest eglot-test-slow-async-connection ()
10791152
"Connect asynchronously with `eglot-sync-connect' set to 2."
@@ -1466,6 +1539,10 @@ GUESSED-MAJOR-MODES-SYM are bound to the useful return values of
14661539
(should (string-suffix-p "c%3A/Users/Foo/bar.lisp"
14671540
(eglot-path-to-uri "c:/Users/Foo/bar.lisp"))))
14681541

1542+
(ert-deftest eglot-test-path-to-uri-escape ()
1543+
(should (equal "file:///path/with%20%25%20funny%20%3F%20characters"
1544+
(eglot-path-to-uri "/path/with % funny ? characters"))))
1545+
14691546
(ert-deftest eglot-test-same-server-multi-mode ()
14701547
"Check single LSP instance manages multiple modes in same project."
14711548
(skip-unless (executable-find "clangd"))
@@ -1491,6 +1568,58 @@ GUESSED-MAJOR-MODES-SYM are bound to the useful return values of
14911568
(eglot--find-file-noselect "project/foolib.c")
14921569
(should (eq (eglot-current-server) server))))))
14931570

1571+
(defun eglot--semtok-faces () "Get semtok faces before point"
1572+
(get-text-property (1- (point)) 'eglot--semtok-faces))
1573+
1574+
(defun eglot--semtok-wait (pos) "Wait for semtok faces to appear after POS"
1575+
(eglot--with-timeout
1576+
'(3 "Timeout waiting for semantic tokens")
1577+
(while (not (save-excursion
1578+
(goto-char pos)
1579+
(text-property-search-forward 'eglot--semtok-faces)))
1580+
(accept-process-output nil 0.1)
1581+
(font-lock-ensure))))
1582+
1583+
(ert-deftest eglot-test-semtok-basic ()
1584+
"Test basic semantic tokens fontification."
1585+
(skip-unless (executable-find "clangd"))
1586+
(eglot--with-fixture
1587+
`(("project" . (("main.c" . "int main() { int x = 42; return x; }"))))
1588+
(with-current-buffer
1589+
(eglot--find-file-noselect "project/main.c")
1590+
(eglot--tests-connect)
1591+
(should (eglot-server-capable :semanticTokensProvider))
1592+
(should eglot-semantic-tokens-mode)
1593+
;; Trigger initial fontification, then wait for semantic tokens
1594+
(font-lock-ensure)
1595+
(eglot--semtok-wait (point-min))
1596+
(goto-char (point-min))
1597+
(search-forward "main")
1598+
(should (memq 'eglot-semantic-function (eglot--semtok-faces)))
1599+
(search-forward "int x")
1600+
(should (memq 'eglot-semantic-variable (eglot--semtok-faces))))))
1601+
1602+
(ert-deftest eglot-test-semtok-refontify ()
1603+
"Test semantic tokens refontification after edits."
1604+
(skip-unless (executable-find "clangd"))
1605+
(eglot--with-fixture
1606+
`(("project" . (("code.c" . "int foo() { return 0; }"))))
1607+
(with-current-buffer
1608+
(eglot--find-file-noselect "project/code.c")
1609+
(eglot--tests-connect)
1610+
(should eglot-semantic-tokens-mode)
1611+
(font-lock-ensure)
1612+
(eglot--semtok-wait (point-min))
1613+
(goto-char (point-max))
1614+
(save-excursion (insert "\nint bar() { int y = 10; return y; }"))
1615+
(font-lock-ensure)
1616+
(eglot--signal-textDocument/didChange) ; a bit unrealistic
1617+
(eglot--semtok-wait (point))
1618+
(search-forward "bar")
1619+
(should (memq 'eglot-semantic-function (eglot--semtok-faces)))
1620+
(search-forward "int y")
1621+
(should (memq 'eglot-semantic-variable (eglot--semtok-faces))))))
1622+
14941623
(provide 'eglot-tests)
14951624

14961625
;; Local Variables:

0 commit comments

Comments
 (0)