11; ;; eglot-tests.el --- Tests for eglot.el -*- lexical-binding : t ; -*-
22
3- ; ; Copyright (C) 2018-2023 Free Software Foundation, Inc.
3+ ; ; Copyright (C) 2018-2025 Free Software Foundation, Inc.
44
55; ; Author: João Távora <joaotavora@gmail.com>
66; ; Keywords: tests
@@ -136,9 +136,11 @@ directory hierarchy."
136136 (jsonrpc-events-buffer server)))))
137137 (cond (noninteractive
138138 (dolist (buffer buffers)
139- (eglot--test-message " contents of `%s' :" (buffer-name buffer))
140- (princ (with-current-buffer buffer (buffer-string ))
141- 'external-debugging-output )))
139+ (eglot--test-message " contents of `%s' %S:" (buffer-name buffer) buffer)
140+ (if (buffer-live-p buffer)
141+ (princ (with-current-buffer buffer (buffer-string ))
142+ 'external-debugging-output )
143+ (princ " Killed\n " #'external-debugging-output ))))
142144 (t
143145 (eglot--test-message " Preserved for inspection: %s"
144146 (mapconcat #'buffer-name buffers " , " ))))))))
@@ -230,7 +232,7 @@ directory hierarchy."
230232 `(push message , client-replies )))))))))
231233 (unwind-protect
232234 (progn
233- (add-hook 'jsonrpc-event-hook #' , log-event-hook-sym )
235+ (add-hook 'jsonrpc-event-hook #' , log-event-hook-sym t )
234236 ,@body )
235237 (remove-hook 'jsonrpc-event-hook #' , log-event-hook-sym ))))))
236238
@@ -436,6 +438,56 @@ directory hierarchy."
436438 (flymake-goto-next-error 1 '() t )
437439 (should (eq 'flymake-error (face-at-point )))))))
438440
441+ (ert-deftest eglot-test-basic-symlink ()
442+ " Test basic symlink support."
443+ (skip-unless (executable-find " clangd" ))
444+ ; ; MS-Windows either fails symlink creation or pops up UAC prompts.
445+ (skip-when (eq system-type 'windows-nt ))
446+ (eglot--with-fixture
447+ `((" symlink-project" .
448+ ((" main.cpp" . " #include\" foo.h\"\n int main() { return foo(); }" )
449+ (" foo.h" . " int foo();" ))))
450+ (with-current-buffer
451+ (find-file-noselect " symlink-project/main.cpp" )
452+ (make-symbolic-link " main.cpp" " mainlink.cpp" )
453+ (eglot--tests-connect)
454+ (eglot--sniffing (:client-notifications c-notifs)
455+ (let ((eglot-autoshutdown nil )) (kill-buffer (current-buffer )))
456+ (eglot--wait-for (c-notifs 10 )
457+ (&key method &allow-other-keys)
458+ (and (string= method " textDocument/didClose" )))))
459+ (eglot--sniffing (:client-notifications c-notifs)
460+ (with-current-buffer
461+ (find-file-noselect " symlink-project/main.cpp" )
462+ (should (eglot-current-server)))
463+ (eglot--wait-for (c-notifs 10 )
464+ (&rest whole &key params method &allow-other-keys)
465+ (and (string= method " textDocument/didOpen" )
466+ (string-match " main.cpp$"
467+ (plist-get (plist-get params :textDocument )
468+ :uri )))))
469+ ; ; This last segment is deactivated, because it's likely not needed.
470+ ; ; The only way the server would answer with '3' references is if we
471+ ; ; had erroneously sent a 'didOpen' for anything other than
472+ ; ; `main.cpp' , but if we got this far is because we've just asserted
473+ ; ; that we didn't.
474+ (when nil
475+ (with-current-buffer
476+ (find-file-noselect " symlink-project/foo.h" )
477+ ; ; Give clangd some time to settle its analysis so it can
478+ ; ; accurately respond to `textDocument/references'
479+ (sleep-for 3 )
480+ (search-forward " foo" )
481+ (eglot--sniffing (:server-replies s-replies)
482+ (call-interactively 'xref-find-references )
483+ (eglot--wait-for (s-replies 10 )
484+ (&key method result &allow-other-keys)
485+ ; ; Expect xref buffer to not contain duplicate references to
486+ ; ; main.cpp and mainlink.cpp. If it did, 'result's length
487+ ; ; would be 3.
488+ (and (string= method " textDocument/references" )
489+ (= (length result) 2 ))))))))
490+
439491(ert-deftest eglot-test-diagnostic-tags-unnecessary-code ()
440492 " Test rendering of diagnostics tagged \" unnecessary\" ."
441493 (skip-unless (executable-find " clangd" ))
@@ -537,6 +589,19 @@ directory hierarchy."
537589 (eglot--wait-for (s-notifs 20 ) (&key method &allow-other-keys)
538590 (string= method " textDocument/publishDiagnostics" ))))
539591
592+ (defun eglot--wait-for-rust-analyzer ()
593+ (eglot--sniffing (:server-notifications s-notifs)
594+ (should (eglot--tests-connect))
595+ (eglot--wait-for (s-notifs 20 ) (&key method params &allow-other-keys)
596+ (and
597+ (string= method " $/progress" )
598+ (equal (plist-get params :token ) " rustAnalyzer/Roots Scanned" )
599+ (equal (plist-get (plist-get params :value ) :kind ) " end" )))
600+ ; ; Annoyingly, waiting for that special progress report is still not
601+ ; ; enough to make sure the server is ready to provide completions,
602+ ; ; so here's two extra seconds.
603+ (sit-for 2 )))
604+
540605(ert-deftest eglot-test-basic-completions ()
541606 " Test basic autocompletion in a clangd LSP."
542607 (skip-unless (executable-find " clangd" ))
@@ -569,19 +634,125 @@ directory hierarchy."
569634 (forward-line -1 )
570635 (should (looking-at " Complete, but not unique" )))))))
571636
572- (ert-deftest eglot-test-basic-xref ()
573- " Test basic xref functionality in a clangd LSP."
637+ (ert-deftest eglot-test-stop-completion-on-nonprefix ()
638+ " Test completion also resulting in 'Complete, but not unique'."
639+ (skip-unless (executable-find " clangd" ))
640+ (eglot--with-fixture
641+ `((" project" . ((" coiso.c" .
642+ ,(concat " int foot; int footer; int fo_obar;"
643+ " int main() {foo" )))))
644+ (with-current-buffer
645+ (eglot--find-file-noselect " project/coiso.c" )
646+ (eglot--wait-for-clangd)
647+ (goto-char (point-max ))
648+ (completion-at-point )
649+ (should (looking-back " foo" )))))
650+
651+ (defun eglot--kill-completions-buffer ()
652+ (when (buffer-live-p (get-buffer " *Completions*" ))
653+ (kill-buffer " *Completions*" )))
654+
655+ (ert-deftest eglot-test-try-completion-nomatch ()
656+ " Test completion table with non-matching input, returning nil."
657+ (skip-unless (executable-find " clangd" ))
658+ (eglot--with-fixture
659+ `((" project" . ((" coiso.c" .
660+ ,(concat " int main() {abc" )))))
661+ (with-current-buffer
662+ (eglot--find-file-noselect " project/coiso.c" )
663+ (eglot--wait-for-clangd)
664+ (eglot--kill-completions-buffer)
665+ (goto-char (point-max ))
666+ (completion-at-point )
667+ (should (looking-back " abc" ))
668+ (should-not (get-buffer " *Completions*" )))))
669+
670+ (ert-deftest eglot-test-try-completion-inside-symbol ()
671+ " Test completion table inside symbol, with only prefix matching."
672+ (skip-unless (executable-find " clangd" ))
673+ (eglot--with-fixture
674+ `((" project" . ((" coiso.c" .
675+ ,(concat
676+ " int foobar;"
677+ " int foobarbaz;"
678+ " int main() {foo123" )))))
679+ (with-current-buffer
680+ (eglot--find-file-noselect " project/coiso.c" )
681+ (eglot--wait-for-clangd)
682+ (goto-char (- (point-max ) 3 ))
683+ (eglot--kill-completions-buffer)
684+ (completion-at-point )
685+ (should (looking-back " foo" ))
686+ (should (looking-at " 123" ))
687+ (should (get-buffer " *Completions*" )))))
688+
689+ (ert-deftest eglot-test-try-completion-inside-symbol-2 ()
690+ " Test completion table inside symbol, with only prefix matching."
574691 (skip-unless (executable-find " clangd" ))
575692 (eglot--with-fixture
576693 `((" project" . ((" coiso.c" .
577- ,(concat " int foo=42; int fooey;"
578- " int main() {foo=82;}" )))))
694+ ,(concat
695+ " int foobar;"
696+ " int main() {foo123" )))))
579697 (with-current-buffer
580698 (eglot--find-file-noselect " project/coiso.c" )
699+ (eglot--wait-for-clangd)
700+ (goto-char (- (point-max ) 3 ))
701+ (completion-at-point )
702+ (should (looking-back " foobar" ))
703+ (should (looking-at " 123" )))))
704+
705+ (ert-deftest eglot-test-rust-completion-exit-function ()
706+ " Ensure rust-analyzer exit function creates the expected contents."
707+ :tags '(:expensive-test )
708+ ; ; This originally appeared in github#1339
709+ (skip-unless (executable-find " rust-analyzer" ))
710+ (skip-unless (executable-find " cargo" ))
711+ (eglot--with-fixture
712+ '((" cmpl-project" .
713+ ((" main.rs" .
714+ " fn test() -> i32 { let v: usize = 1; v.count_on1234.1234567890;" ))))
715+ (with-current-buffer
716+ (eglot--find-file-noselect " cmpl-project/main.rs" )
717+ (should (zerop (shell-command " cargo init" )))
718+ (search-forward " v.count_on" )
719+ (eglot--wait-for-rust-analyzer)
720+ (completion-at-point )
721+ (should
722+ (equal
723+ (if (bound-and-true-p yas-minor-mode)
724+ " fn test() -> i32 { let v: usize = 1; v.count_ones().1234567890;"
725+ " fn test() -> i32 { let v: usize = 1; v.count_ones.1234567890;" )
726+ (buffer-string ))))))
727+
728+ (ert-deftest eglot-test-zig-insert-replace-completion ()
729+ " Test zls's use of 'InsertReplaceEdit'."
730+ (skip-unless (functionp 'zig-ts-mode ))
731+ (eglot--with-fixture
732+ `((" project" .
733+ ((" main.zig" .
734+ ,(concat " const Foo = struct {correct_name: u32,\n };\n "
735+ " fn example(foo: Foo) u32 {return foo.correc_name; }" )))))
736+ (with-current-buffer
737+ (eglot--find-file-noselect " project/main.zig" )
581738 (should (eglot--tests-connect))
582- (search-forward " {foo" )
583- (call-interactively 'xref-find-definitions )
584- (should (looking-at " foo=42" )))))
739+ (search-forward " foo.correc" )
740+ (completion-at-point )
741+ (should (looking-back " correct_name" )))))
742+
743+ (ert-deftest eglot-test-basic-xref ()
744+ " Test basic xref functionality in a clangd LSP."
745+ (skip-unless (executable-find " clangd" ))
746+ (eglot--with-fixture
747+ `((" project" . ((" coiso.c" .
748+ ,(concat " int foo=42; int fooey;"
749+ " int main() {foo=82;}" )))))
750+ (with-current-buffer
751+ (eglot--find-file-noselect " project/coiso.c" )
752+ (should (eglot--tests-connect))
753+ (search-forward " {foo" )
754+ (call-interactively 'xref-find-definitions )
755+ (should (looking-at " foo=42" )))))
585756
586757(defvar eglot--test-c-buffer
587758 " \
@@ -710,6 +881,7 @@ int main() {
710881
711882(ert-deftest eglot-test-javascript-basic ()
712883 " Test basic autocompletion in a JavaScript LSP."
884+ :tags '(:expensive-test )
713885 (skip-unless (and (executable-find " typescript-language-server" )
714886 (executable-find " tsserver" )))
715887 (eglot--with-fixture
@@ -724,14 +896,14 @@ int main() {
724896 :client-notifications
725897 c-notifs)
726898 (should (eglot--tests-connect))
727- (eglot--wait-for (s-notifs 2 ) (&key method &allow-other-keys)
899+ (eglot--wait-for (s-notifs 10 ) (&key method &allow-other-keys)
728900 (string= method " textDocument/publishDiagnostics" ))
729901 (should (not (eq 'flymake-error (face-at-point ))))
730902 (insert " {" )
731903 (eglot--signal-textDocument/didChange)
732904 (eglot--wait-for (c-notifs 1 ) (&key method &allow-other-keys)
733905 (string= method " textDocument/didChange" ))
734- (eglot--wait-for (s-notifs 2 ) (&key params method &allow-other-keys)
906+ (eglot--wait-for (s-notifs 10 ) (&key params method &allow-other-keys)
735907 (and (string= method " textDocument/publishDiagnostics" )
736908 (cl-destructuring-bind (&key _uri diagnostics) params
737909 (cl-find-if (jsonrpc-lambda (&key severity &allow-other-keys)
@@ -821,6 +993,12 @@ int main() {
821993 (should (looking-back " \" foo.bar\" : \" " ))
822994 (should (looking-at " fb\" $" ))))))
823995
996+ (defun eglot-tests--get (object path )
997+ (dolist (op path)
998+ (setq object (if (natnump op) (aref object op)
999+ (plist-get object op))))
1000+ object)
1001+
8241002(defun eglot-tests--lsp-abiding-column-1 ()
8251003 (eglot--with-fixture
8261004 '((" project" .
@@ -837,7 +1015,11 @@ int main() {
8371015 (insert " p " )
8381016 (eglot--signal-textDocument/didChange)
8391017 (eglot--wait-for (c-notifs 2 ) (&key params &allow-other-keys)
840- (should (equal 71 (cadddr (cadadr (aref (cadddr params) 0 ))))))
1018+ (message " PARAMS=%S " params)
1019+ (should (equal 71 (eglot-tests--get
1020+ params
1021+ '(:contentChanges 0
1022+ :range :start :character )))))
8411023 (beginning-of-line )
8421024 (should (eq eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos ))
8431025 (funcall eglot-move-to-linepos-function 71 )
@@ -1175,8 +1357,8 @@ GUESSED-MAJOR-MODES-SYM are bound to the useful return values of
11751357 (let ((eglot-server-programs '(((baz-mode (foo-mode :language-id " bar" ))
11761358 . (" prog-executable" )))))
11771359 (eglot--guessing-contact (_ nil _ _ modes guessed-langs)
1178- (should (equal guessed-langs '(" bar " " baz " )))
1179- (should (equal modes '(foo -mode baz -mode)))))))
1360+ (should (equal guessed-langs '(" baz " " bar " )))
1361+ (should (equal modes '(baz -mode foo -mode)))))))
11801362
11811363(defun eglot--glob-match (glob str )
11821364 (funcall (eglot--glob-compile glob t t ) str))
@@ -1224,13 +1406,19 @@ GUESSED-MAJOR-MODES-SYM are bound to the useful return values of
12241406 ; ; (should (eglot--glob-match "{foo,bar}/**" "foo"))
12251407 ; ; (should (eglot--glob-match "{foo,bar}/**" "bar"))
12261408
1227- ; ; VSCode also supports nested blobs. Do we care?
1409+ ; ; VSCode also supports nested blobs. Do we care? Apparently yes:
1410+ ; ; github#1403
12281411 ; ;
1229- ; ; (should (eglot--glob-match "{**/*.d.ts,**/*.js}" "/testing/foo.js"))
1230- ; ; (should (eglot--glob-match "{**/*.d.ts,**/*.js}" "testing/foo.d.ts"))
1231- ; ; (should (eglot--glob-match "{**/*.d.ts,**/*.js,foo.[0-9]}" "foo.5"))
1232- ; ; (should (eglot--glob-match "prefix/{**/*.d.ts,**/*.js,foo.[0-9]}" "prefix/foo.8"))
1233- )
1412+ (should (eglot--glob-match " {**/*.d.ts,**/*.js}" " /testing/foo.js" ))
1413+ (should (eglot--glob-match " {**/*.d.ts,**/*.js}" " testing/foo.d.ts" ))
1414+ (should (eglot--glob-match " {**/*.d.ts,**/*.js,foo.[0-9]}" " foo.5" ))
1415+ (should-not (eglot--glob-match " {**/*.d.ts,**/*.js,foo.[0-4]}" " foo.5" ))
1416+ (should (eglot--glob-match " prefix/{**/*.d.ts,**/*.js,foo.[0-9]}"
1417+ " prefix/foo.8" ))
1418+ (should (eglot--glob-match " prefix/{**/*.js,**/foo.[0-9]}.suffix"
1419+ " prefix/a/b/c/d/foo.5.suffix" ))
1420+ (should (eglot--glob-match " prefix/{**/*.js,**/foo.[0-9]}.suffix"
1421+ " prefix/a/b/c/d/foo.js.suffix" )))
12341422
12351423(defvar tramp-histfile-override )
12361424(defun eglot--call-with-tramp-test (fn )
0 commit comments