| 🛟 Updated. 28d5985 k33g 11h ago | 1 | package lsp |
| 2 | |
| 3 | import ( |
| 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. |
| 12 | const ( |
| 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 | |
| 37 | func 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 | |
| 57 | func 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 | |
| 71 | func 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 | |
| 86 | func 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 | |
| 104 | func 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 | |
| 126 | func 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 | |
| 140 | func 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 | |
| 157 | func 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 | |
| 190 | func 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 | |
| 201 | func 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 | } |