package golang_test import ( "context" "errors" "path/filepath" "strings" "testing" "rickub.com/turbo-editors/turbo-core/lsp" "rickub.com/turbo-editors/turbo-go/internal/golang" ) // TestAgainstRealGopls drives turbo-core's LSP client against an actual // language server: process, pipes, handshake, completion and shutdown. // // It lives here rather than in turbo-core because gopls is Turbo Go's server; // the library has no language server of its own to test against. // // The original comment follows. // // It drives the whole client against an actual language // server: process, pipes, handshake, completion and shutdown. // // It is skipped when gopls is not installed and under -short, so a checkout // with no language server still has a green suite — which is exactly the // situation the editor itself is built to cope with. func TestAgainstRealGopls(t *testing.T) { if testing.Short() { t.Skip("-short: not starting a language server") } if _, err := lsp.FindServer(golang.Profile().Server); errors.Is(err, lsp.ErrServerNotFound) { t.Skipf("%s is not installed; %s", golang.ServerCommand, golang.InstallHint) } root := t.TempDir() writeFile(t, filepath.Join(root, "go.mod"), "module example.test\n\ngo 1.24\n") source := "package main\n\nimport \"strings\"\n\nfunc main() {\n\tstrings.\n}\n" path := filepath.Join(root, "main.go") writeFile(t, path, source) server, err := lsp.StartServer(t.Context(), golang.Profile().Server, root, golang.Name) if err != nil { t.Fatalf("StartServer() error = %v", err) } client := server.Client() t.Cleanup(func() { // Not t.Context(): that one is already cancelled by the time cleanups // run, and shutting a server down needs a context that is still alive. if err := server.Stop(context.Background()); err != nil { t.Errorf("Stop() error = %v", err) } }) if !client.Ready() { t.Fatal("the client is not ready after StartServer returned") } if err := client.DidOpen(path, source); err != nil { t.Fatalf("DidOpen() error = %v", err) } // Line 5, just after "strings." — the column counts the leading tab as one // rune, as the editor does everywhere. items, err := client.Complete(t.Context(), path, 5, 9, "\tstrings.") if err != nil { t.Fatalf("Complete() error = %v", err) } if len(items) == 0 { t.Fatal("gopls offered no completions after \"strings.\"") } if !containsLabel(items, "Contains") { t.Errorf("the completions do not include strings.Contains: %v", labelsOf(items)) } } func containsLabel(items []lsp.CompletionItem, want string) bool { for _, item := range items { if strings.HasPrefix(item.Label, want) { return true } } return false } func labelsOf(items []lsp.CompletionItem) []string { labels := make([]string, 0, min(len(items), 10)) for _, item := range items[:min(len(items), 10)] { labels = append(labels, item.Label) } return labels }