1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
|
package golang_test
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gdamore/tcell/v2"
"rickub.com/turbo-editors/turbo-core/app"
"rickub.com/turbo-editors/turbo-core/buffer"
"rickub.com/turbo-editors/turbo-core/lsp"
"rickub.com/turbo-editors/turbo-core/syntax"
"rickub.com/turbo-editors/turbo-go/internal/golang"
)
// TestCompletionEndToEndWithRealGopls drives the exact sequence the command
// does at start-up: open the files first, start the language server second,
// then ask for a completion.
//
// That order is the whole point. The editor's earlier version announced its
// open documents to a server that did not exist yet and never mentioned them
// again, so gopls answered every completion about a file it had never heard
// of — which looks, from the outside, exactly like completion not working.
//
// It lives in Turbo Go rather than in turbo-core because gopls is Turbo Go's
// server: the library has no language server of its own to be driven against.
//
// It skips itself when gopls is not installed, and under -short.
func TestCompletionEndToEndWithRealGopls(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")
// The file on disk stops short of the dot. The text the completion is
// about gets *typed* below, so the answer can only come from what the
// editor told the server — which is the whole point of this test. A
// fixture already containing "strings." would be answered from disk, and
// would pass whether or not the editor said anything at all.
source := "package main\n\nimport \"strings\"\n\nfunc main() {\n\t\n}\n"
path := filepath.Join(root, "main.go")
writeFile(t, path, source)
editor := newTestEditor(t)
// 1. Open the file, exactly as main does — before there is any server.
editor.Open(path)
// 2. Start the language server, exactly as main does — afterwards.
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
editor.StartLanguageServer(ctx, root)
t.Cleanup(func() { editor.Language().Stop(context.Background()) })
waitUntilReady(t, editor)
// 3. Let the event loop notice the server is ready, as Run does on every
// turn. This is what announces the file that was already open.
editor.Tick()
// 4. Type "strings." into the buffer, so that only the editor knows it is
// there, then ask for a completion.
view := editor.ActiveView()
view.Buffer().SetCursor(buffer.Position{Line: 5, Col: 1})
typeText(editor, "strings.")
if !editor.Completion().Visible() {
// Typing the dot asks for a completion by itself; ask again explicitly
// so a failure reports the status rather than the popup's absence.
editor.RequestCompletion()
}
if !editor.Completion().Visible() {
t.Fatalf("no completion list opened; the status bar says %q", editor.StatusBar().Message())
}
if !completionOffers(editor, "Contains") {
t.Errorf("the list does not offer strings.Contains; it has %d entries", editor.Completion().Count())
}
}
func TestTheEditorColoursGoSourceItOpens(t *testing.T) {
// The whole path in one test: Register taught the library about Go, the
// profile named the editor, and a .go file opened through the public API
// comes out coloured.
root := t.TempDir()
path := filepath.Join(root, "main.go")
writeFile(t, path, "package main\n")
editor := newTestEditor(t)
editor.Open(path)
if got := editor.ActiveView().Language(); got != golang.Language {
t.Fatalf("the view colours the file as %q, want %q", got, golang.Language)
}
if spans := syntax.Highlight(golang.Language, "package main"); len(spans[0]) == 0 {
t.Error("the registered Go scanner colours nothing")
}
}
func TestTheEditorCallsItselfTurboGo(t *testing.T) {
editor := newTestEditor(t)
if got := editor.Profile().Name; got != golang.Name {
t.Errorf("Profile().Name = %q, want %q", got, golang.Name)
}
if got := editor.Profile().ProjectDir(); got != ".turbo-go" {
t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-go")
}
}
// newTestEditor returns Turbo Go drawing on a simulated terminal, set up the
// way the command sets it up.
func newTestEditor(t *testing.T) *app.App {
t.Helper()
golang.Register()
screen := tcell.NewSimulationScreen("UTF-8")
if err := screen.Init(); err != nil {
t.Fatalf("initialising the simulation screen: %v", err)
}
t.Cleanup(screen.Fini)
screen.SetSize(80, 24)
// Never read the themes or snippets of whoever is running the tests.
p := golang.Profile()
t.Setenv(p.ThemeDirEnvVar(), t.TempDir())
t.Setenv(p.SnippetDirEnvVar(), t.TempDir())
editor := app.New(screen, "turbo-classic", p)
editor.Render()
return editor
}
// typeText sends a run of printable characters through the whole routing chain.
func typeText(editor *app.App, text string) {
for _, r := range text {
editor.Handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
}
}
// completionOffers reports whether the open popup holds an entry starting with
// a label.
func completionOffers(editor *app.App, label string) bool {
for _, item := range editor.Completion().Matches() {
if strings.HasPrefix(item.Label, label) {
return true
}
}
return false
}
// waitUntilReady blocks until the language server has finished starting.
func waitUntilReady(t *testing.T, editor *app.App) {
t.Helper()
deadline := time.After(lsp.InitializeTimeout)
for !editor.Language().Ready() {
select {
case <-deadline:
t.Fatalf("the language server never became ready: %s", editor.Language().Status())
case <-time.After(10 * time.Millisecond):
}
}
}
// writeFile creates a file, making its directory first.
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("creating %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", path, err)
}
}
|