turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

symbol_test.go · 208 lines · 7.3 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 11h ago1package lsp
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "testing"
8)
9
10// The three shapes a symbol answer arrives in, written as the servers send
11// them rather than as Go values, because the decoding is the thing under test.
12const (
13 nestedSymbols = `[
14 {"name":"Server","kind":23,
15 "range":{"start":{"line":8,"character":0},"end":{"line":20,"character":1}},
16 "selectionRange":{"start":{"line":9,"character":5},"end":{"line":9,"character":11}},
17 "children":[
18 {"name":"ServeHTTP","kind":6,
19 "range":{"start":{"line":12,"character":1},"end":{"line":14,"character":2}},
20 "selectionRange":{"start":{"line":12,"character":18},"end":{"line":12,"character":27}}}
21 ]}
22 ]`
23
24 flatSymbols = `[
25 {"name":"Server","kind":23,"containerName":"main",
26 "location":{"uri":"file:///tmp/p/main.go","range":{"start":{"line":9,"character":5}}}},
27 {"name":"ServeHTTP","kind":6,"containerName":"Server",
28 "location":{"uri":"file:///tmp/p/main.go","range":{"start":{"line":12,"character":18}}}}
29 ]`
30
31 // A 3.17 server answering with a location that has no range at all.
32 rangelessSymbols = `[
33 {"name":"Config","kind":23,"location":{"uri":"file:///tmp/p/config.go"}}
34 ]`
35)
36
37func TestNestedDocumentSymbolsAreFlattenedInFileOrder(t *testing.T) {
38 symbols, err := decodeDocumentSymbols(json.RawMessage(nestedSymbols), "file:///tmp/p/main.go")
39 if err != nil {
40 t.Fatalf("decodeDocumentSymbols() error = %v", err)
41 }
42
43 if len(symbols) != 2 {
44 t.Fatalf("got %d symbols, want the type and its method", len(symbols))
45 }
46 if symbols[0].Name != "Server" || symbols[1].Name != "ServeHTTP" {
47 t.Errorf("got %q then %q, want them in the order they appear in the file", symbols[0].Name, symbols[1].Name)
48 }
49 if symbols[0].Depth != 0 || symbols[1].Depth != 1 {
50 t.Errorf("depths are %d and %d, want 0 then 1 so a caller can indent", symbols[0].Depth, symbols[1].Depth)
51 }
52 if symbols[1].Container != "Server" {
53 t.Errorf("the method's container is %q, want the type it hangs off", symbols[1].Container)
54 }
55}
56
57func TestANestedSymbolIsLocatedByItsNameNotItsWholeDeclaration(t *testing.T) {
58 // range covers the doc comment and the body; selectionRange is the name.
59 // Jumping to a function must put the cursor on the function, not several
60 // lines above it.
61 symbols, err := decodeDocumentSymbols(json.RawMessage(nestedSymbols), "file:///tmp/p/main.go")
62 if err != nil {
63 t.Fatalf("decodeDocumentSymbols() error = %v", err)
64 }
65
66 if got := symbols[0].Location.Range.Start; got.Line != 9 || got.Character != 5 {
67 t.Errorf("Server is at %+v, want the selectionRange at 9:5, not the range at 8:0", got)
68 }
69}
70
71func TestNestedSymbolsInheritTheFileTheyWereAskedAbout(t *testing.T) {
72 // The nested shape carries no URI: every symbol is in the file requested.
73 // Losing it means every jump goes nowhere.
74 symbols, err := decodeDocumentSymbols(json.RawMessage(nestedSymbols), "file:///tmp/p/main.go")
75 if err != nil {
76 t.Fatalf("decodeDocumentSymbols() error = %v", err)
77 }
78
79 for _, symbol := range symbols {
80 if symbol.Location.URI != "file:///tmp/p/main.go" {
81 t.Errorf("%s has URI %q, want the file it was found in", symbol.Name, symbol.Location.URI)
82 }
83 }
84}
85
86func TestFlatDocumentSymbolsAreReadToo(t *testing.T) {
87 // Still legal, and still sent by older servers.
88 symbols, err := decodeDocumentSymbols(json.RawMessage(flatSymbols), "file:///tmp/p/main.go")
89 if err != nil {
90 t.Fatalf("decodeDocumentSymbols() error = %v", err)
91 }
92
93 if len(symbols) != 2 {
94 t.Fatalf("got %d symbols, want 2", len(symbols))
95 }
96 if symbols[1].Container != "Server" {
97 t.Errorf("container = %q, want the containerName the flat shape carries", symbols[1].Container)
98 }
99 if got := symbols[0].Location.Range.Start.Line; got != 9 {
100 t.Errorf("Server is on line %d, want 9", got)
101 }
102}
103
104func TestTheTwoShapesAreToldApartByAFieldOnlyOneHas(t *testing.T) {
105 // "children" is optional, so a file whose symbols happen to have none
106 // would be read as flat — and every symbol would lose its position.
107 // "selectionRange" is the discriminator for that reason.
108 const nestedWithoutChildren = `[
109 {"name":"main","kind":12,
110 "range":{"start":{"line":3,"character":0}},
111 "selectionRange":{"start":{"line":3,"character":5}}}
112 ]`
113
114 symbols, err := decodeDocumentSymbols(json.RawMessage(nestedWithoutChildren), "file:///tmp/p/main.go")
115 if err != nil {
116 t.Fatalf("decodeDocumentSymbols() error = %v", err)
117 }
118 if len(symbols) != 1 {
119 t.Fatalf("got %d symbols, want 1", len(symbols))
120 }
121 if got := symbols[0].Location.Range.Start; got.Line != 3 || got.Character != 5 {
122 t.Errorf("main is at %+v, want 3:5 — the nested shape was read as flat", got)
123 }
124}
125
126func TestAnEmptyOrNullSymbolAnswerIsNotAnError(t *testing.T) {
127 // A file with nothing in it, and a server that has nothing to say, are
128 // both ordinary.
129 for _, raw := range []string{"", "null", "[]"} {
130 symbols, err := decodeDocumentSymbols(json.RawMessage(raw), "file:///tmp/p/main.go")
131 if err != nil {
132 t.Errorf("decodeDocumentSymbols(%q) error = %v", raw, err)
133 }
134 if len(symbols) != 0 {
135 t.Errorf("decodeDocumentSymbols(%q) returned %d symbols", raw, len(symbols))
136 }
137 }
138}
139
140func TestAWorkspaceSymbolWithNoRangeIsKept(t *testing.T) {
141 // Since 3.17 a server may answer "I know which file, ask me later for
142 // where". There is no asking later here, and the top of the right file
143 // beats no answer at all.
144 symbols, err := decodeWorkspaceSymbols(json.RawMessage(rangelessSymbols))
145 if err != nil {
146 t.Fatalf("decodeWorkspaceSymbols() error = %v", err)
147 }
148
149 if len(symbols) != 1 {
150 t.Fatalf("got %d symbols, want the one with no range", len(symbols))
151 }
152 if symbols[0].Location.URI != "file:///tmp/p/config.go" {
153 t.Errorf("URI = %q, want the file the server named", symbols[0].Location.URI)
154 }
155}
156
157func TestSymbolRequestsGoOutUnderTheirOwnNames(t *testing.T) {
158 for _, request := range []struct {
159 method string
160 ask func(*Client) ([]Symbol, error)
161 }{
162 {"textDocument/documentSymbol", func(c *Client) ([]Symbol, error) {
163 return c.DocumentSymbols(context.Background(), "main.go")
164 }},
165 {"workspace/symbol", func(c *Client) ([]Symbol, error) {
166 return c.WorkspaceSymbols(context.Background(), "Server")
167 }},
168 } {
169 t.Run(request.method, func(t *testing.T) {
170 client, server := newFakeServer(t)
171 server.setHandler(func(method string, _ json.RawMessage) (any, *ResponseError) {
172 if method != request.method {
173 return nil, nil
174 }
175 return json.RawMessage(flatSymbols), nil
176 })
177 mustInitialize(t, client)
178
179 symbols, err := request.ask(client)
180 if err != nil {
181 t.Fatalf("error = %v", err)
182 }
183 if len(symbols) != 2 {
184 t.Errorf("got %d symbols, want the two the server answered to %s", len(symbols), request.method)
185 }
186 })
187 }
188}
189
190func TestSymbolRequestsRefuseBeforeTheServerIsReady(t *testing.T) {
191 client, _ := newFakeServer(t)
192
193 if _, err := client.DocumentSymbols(t.Context(), "main.go"); !errors.Is(err, ErrNotReady) {
194 t.Errorf("DocumentSymbols() error = %v, want ErrNotReady", err)
195 }
196 if _, err := client.WorkspaceSymbols(t.Context(), "x"); !errors.Is(err, ErrNotReady) {
197 t.Errorf("WorkspaceSymbols() error = %v, want ErrNotReady", err)
198 }
199}
200
201func TestASymbolKindWithNoTagIsDrawnWithoutOne(t *testing.T) {
202 if got := SymbolMethod.String(); got != "method" {
203 t.Errorf("SymbolMethod.String() = %q, want %q", got, "method")
204 }
205 if got := SymbolKind(999).String(); got != "" {
206 t.Errorf("an unknown kind reads %q, want the empty tag rather than a number", got)
207 }
208}