turbo-editors/turbo-corepublic Fork 0
v0.9.0
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.go · 269 lines · 9.4 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 12h ago1package lsp
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8)
9
10// Symbol is a named thing in the code, flattened to the one shape the editor
11// draws: a name, what sort of thing it is, where it lives, and — for a symbol
12// found in a project rather than in the file in front of you — what contains
13// it.
14//
15// symbols, err := client.DocumentSymbols(ctx, "main.go")
16// for _, s := range symbols {
17// fmt.Println(s.Kind, s.Name, s.Location.URI)
18// }
19//
20// The protocol has three shapes for this and the editor wants one. Which
21// shape arrives depends on the server and on the request, not on anything the
22// caller can choose, so the flattening happens here rather than in every
23// caller.
24type Symbol struct {
25 // Name is the symbol itself: "ServeHTTP", "Config".
26 Name string
27 // Kind says what sort of thing it is, in the same numbering completions
28 // use for the overlapping values.
29 Kind SymbolKind
30 // Container is what the symbol is inside — a type for a method, a package
31 // for a type — when the server says. Empty otherwise.
32 Container string
33 // Depth is how deeply nested the symbol was in the answer, so a caller can
34 // indent a file's outline. Zero for a flat answer.
35 Depth int
36 // Location is where to go to reach it.
37 Location Location
38}
39
40// SymbolKind says what sort of thing a symbol is. The values are the
41// protocol's own, and overlap with CompletionItemKind only by coincidence —
42// they are separate numberings in the specification, so they are separate
43// types here.
44type SymbolKind int
45
46// The kinds a language server actually returns for source code.
47const (
48 SymbolFile SymbolKind = 1
49 SymbolModule SymbolKind = 2
50 SymbolNamespace SymbolKind = 3
51 SymbolPackage SymbolKind = 4
52 SymbolClass SymbolKind = 5
53 SymbolMethod SymbolKind = 6
54 SymbolProperty SymbolKind = 7
55 SymbolField SymbolKind = 8
56 SymbolConstructor SymbolKind = 9
57 SymbolEnum SymbolKind = 10
58 SymbolInterface SymbolKind = 11
59 SymbolFunction SymbolKind = 12
60 SymbolVariable SymbolKind = 13
61 SymbolConstant SymbolKind = 14
62 SymbolStruct SymbolKind = 23
63 SymbolEnumMember SymbolKind = 22
64 SymbolTypeParameter SymbolKind = 26
65)
66
67// symbolKindNames are the short tags shown beside a symbol, in the same spirit
68// as the ones beside a completion: enough to tell a method from a field, no
69// more.
70var symbolKindNames = map[SymbolKind]string{
71 SymbolModule: "module",
72 SymbolNamespace: "namespace",
73 SymbolPackage: "package",
74 SymbolClass: "class",
75 SymbolMethod: "method",
76 SymbolProperty: "property",
77 SymbolField: "field",
78 SymbolConstructor: "new",
79 SymbolEnum: "enum",
80 SymbolInterface: "interface",
81 SymbolFunction: "func",
82 SymbolVariable: "var",
83 SymbolConstant: "const",
84 SymbolStruct: "struct",
85 SymbolEnumMember: "case",
86 SymbolTypeParameter: "type param",
87}
88
89// String returns the short tag for a kind, or "" for one with no name worth
90// showing. A kind with no tag is drawn without one rather than as a number.
91//
92// lsp.SymbolMethod.String() // "method"
93func (k SymbolKind) String() string { return symbolKindNames[k] }
94
95// DocumentSymbols returns the symbols declared in one file, in the order the
96// server gives them — which is the order they appear in the file.
97//
98// symbols, err := client.DocumentSymbols(ctx, "main.go")
99//
100// Nested symbols are flattened, and each carries the Depth it was found at, so
101// a caller can show a method under its type without walking a tree.
102func (c *Client) DocumentSymbols(ctx context.Context, path string) ([]Symbol, error) {
103 if !c.Ready() {
104 return nil, ErrNotReady
105 }
106 ctx, cancel := context.WithTimeout(ctx, RequestTimeout)
107 defer cancel()
108
109 params := map[string]any{"textDocument": TextDocumentIdentifier{URI: PathToURI(path)}}
110 var raw json.RawMessage
111 if err := c.conn.Call(ctx, "textDocument/documentSymbol", params, &raw); err != nil {
112 return nil, err
113 }
114 return decodeDocumentSymbols(raw, PathToURI(path))
115}
116
117// WorkspaceSymbols searches the whole project for symbols matching a query.
118//
119// symbols, err := client.WorkspaceSymbols(ctx, "ServeHTTP")
120//
121// What "matching" means belongs to the server: gopls does a fuzzy match,
122// others a prefix. An empty query is the server's business too — some answer
123// with everything, some with nothing — so it is passed through rather than
124// refused here.
125func (c *Client) WorkspaceSymbols(ctx context.Context, query string) ([]Symbol, error) {
126 if !c.Ready() {
127 return nil, ErrNotReady
128 }
129 ctx, cancel := context.WithTimeout(ctx, RequestTimeout)
130 defer cancel()
131
132 var raw json.RawMessage
133 if err := c.conn.Call(ctx, "workspace/symbol", map[string]any{"query": query}, &raw); err != nil {
134 return nil, err
135 }
136 return decodeWorkspaceSymbols(raw)
137}
138
139// documentSymbolNode is the nested shape: a symbol with its children, no URI,
140// and two ranges — the whole declaration and just the name.
141type documentSymbolNode struct {
142 Name string `json:"name"`
143 Kind SymbolKind `json:"kind"`
144 Detail string `json:"detail"`
145 Range Range `json:"range"`
146 SelectionRange Range `json:"selectionRange"`
147 Children []documentSymbolNode `json:"children"`
148}
149
150// symbolInformation is the flat shape: a symbol with a full location and,
151// sometimes, the name of what contains it.
152type symbolInformation struct {
153 Name string `json:"name"`
154 Kind SymbolKind `json:"kind"`
155 ContainerName string `json:"containerName"`
156 Location Location `json:"location"`
157}
158
159// decodeDocumentSymbols reads either shape a documentSymbol answer may take.
160//
161// Which one arrives depends on the server: the nested DocumentSymbol[] is
162// preferred by the specification and is what gopls and rust-analyzer send, but
163// the flat SymbolInformation[] is still legal and older servers send it. They
164// are told apart by a field only one of them has, because both are arrays of
165// objects with a name and a kind.
166//
167// The nested shape carries no URI — every symbol is in the file that was
168// asked about — so the caller's own URI is put back in.
169func decodeDocumentSymbols(raw json.RawMessage, uri string) ([]Symbol, error) {
170 trimmed := bytes.TrimSpace(raw)
171 if len(trimmed) == 0 || string(trimmed) == "null" {
172 return nil, nil
173 }
174
175 if nested, ok := looksNested(trimmed); ok {
176 var nodes []documentSymbolNode
177 if err := json.Unmarshal(nested, &nodes); err != nil {
178 return nil, fmt.Errorf("lsp: unreadable document symbols: %w", err)
179 }
180 return flattenSymbols(nodes, uri, 0, nil), nil
181 }
182
183 var flat []symbolInformation
184 if err := json.Unmarshal(trimmed, &flat); err != nil {
185 return nil, fmt.Errorf("lsp: unreadable document symbols: %w", err)
186 }
187 return fromSymbolInformation(flat), nil
188}
189
190// looksNested reports whether an array of symbols is the nested shape, by
191// looking for the one field only that shape has.
192//
193// "selectionRange" is the discriminator rather than "children" because
194// children is optional and a file whose symbols happen to have none would
195// otherwise be read as flat — and then every symbol would lose its position.
196func looksNested(raw json.RawMessage) (json.RawMessage, bool) {
197 var probe []map[string]json.RawMessage
198 if err := json.Unmarshal(raw, &probe); err != nil || len(probe) == 0 {
199 return raw, false
200 }
201 _, nested := probe[0]["selectionRange"]
202 return raw, nested
203}
204
205// flattenSymbols walks the nested shape depth-first, which is the order the
206// symbols appear in the file.
207//
208// The position used is the **selection** range — the name — not the whole
209// declaration: jumping to a function should put the cursor on its name, not on
210// the doc comment above it.
211func flattenSymbols(nodes []documentSymbolNode, uri string, depth int, container []string) []Symbol {
212 var out []Symbol
213 for _, node := range nodes {
214 where := node.SelectionRange
215 if where == (Range{}) {
216 where = node.Range
217 }
218 out = append(out, Symbol{
219 Name: node.Name,
220 Kind: node.Kind,
221 Container: joinContainer(container),
222 Depth: depth,
223 Location: Location{URI: uri, Range: where},
224 })
225 out = append(out, flattenSymbols(node.Children, uri, depth+1, append(container, node.Name))...)
226 }
227 return out
228}
229
230// joinContainer names what a nested symbol sits inside, innermost last.
231func joinContainer(names []string) string {
232 if len(names) == 0 {
233 return ""
234 }
235 return names[len(names)-1]
236}
237
238// decodeWorkspaceSymbols reads a workspace/symbol answer.
239//
240// Since 3.17 a server may answer with a WorkspaceSymbol whose location is
241// `{"uri": …}` and no range at all, meaning "I know which file, ask me later
242// for where". There is no "ask me later" here, so such a symbol is kept with a
243// zero range: the top of the right file is a better answer than nothing.
244func decodeWorkspaceSymbols(raw json.RawMessage) ([]Symbol, error) {
245 trimmed := bytes.TrimSpace(raw)
246 if len(trimmed) == 0 || string(trimmed) == "null" {
247 return nil, nil
248 }
249
250 var flat []symbolInformation
251 if err := json.Unmarshal(trimmed, &flat); err != nil {
252 return nil, fmt.Errorf("lsp: unreadable workspace symbols: %w", err)
253 }
254 return fromSymbolInformation(flat), nil
255}
256
257// fromSymbolInformation turns the flat shape into the editor's own.
258func fromSymbolInformation(flat []symbolInformation) []Symbol {
259 out := make([]Symbol, 0, len(flat))
260 for _, symbol := range flat {
261 out = append(out, Symbol{
262 Name: symbol.Name,
263 Kind: symbol.Kind,
264 Container: symbol.ContainerName,
265 Location: symbol.Location,
266 })
267 }
268 return out
269}