ttsc 0.17.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cmd/ttscserver/main.go
CHANGED
|
@@ -25,6 +25,7 @@ import (
|
|
|
25
25
|
"syscall"
|
|
26
26
|
"time"
|
|
27
27
|
|
|
28
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graphsymbols"
|
|
28
29
|
"github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
|
|
29
30
|
)
|
|
30
31
|
|
|
@@ -131,6 +132,12 @@ func runLSP(args []string) int {
|
|
|
131
132
|
return 2
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// The SymbolProvider answers textDocument/documentSymbol and
|
|
136
|
+
// textDocument/references locally from ttsc's compiler-backed code graph;
|
|
137
|
+
// tsgo's native LSP does not implement them. It loads the program lazily on
|
|
138
|
+
// the first such request, so wiring it here adds no startup cost.
|
|
139
|
+
symbolProvider := graphsymbols.NewProvider(cwd, strings.TrimSpace(*tsconfigFlag))
|
|
140
|
+
|
|
134
141
|
err = runLSPServer(ctx, lspserver.LSPServerOptions{
|
|
135
142
|
In: stdin,
|
|
136
143
|
Out: stdout,
|
|
@@ -138,6 +145,7 @@ func runLSP(args []string) int {
|
|
|
138
145
|
Cwd: cwd,
|
|
139
146
|
TsgoBinary: tsgoBinary,
|
|
140
147
|
Source: source,
|
|
148
|
+
SymbolProvider: symbolProvider,
|
|
141
149
|
SuppressExecuteCommandProvider: *suppressExecuteCommandProviderFlag,
|
|
142
150
|
SuppressedExecuteCommandIDs: splitCSV(*suppressExecuteCommandIDsFlag),
|
|
143
151
|
ExecuteCommandIDPrefix: strings.TrimSpace(*executeCommandIDPrefixFlag),
|
package/driver/lsp.go
CHANGED
|
@@ -49,6 +49,20 @@ type LSPTextEdit = lspserver.LSPTextEdit
|
|
|
49
49
|
// LSPDocumentVersion is the driver-level alias for lspserver.LSPDocumentVersion.
|
|
50
50
|
type LSPDocumentVersion = lspserver.LSPDocumentVersion
|
|
51
51
|
|
|
52
|
+
// LSPSymbolKind is the driver-level alias for lspserver.LSPSymbolKind.
|
|
53
|
+
type LSPSymbolKind = lspserver.LSPSymbolKind
|
|
54
|
+
|
|
55
|
+
// LSPDocumentSymbol is the driver-level alias for lspserver.LSPDocumentSymbol.
|
|
56
|
+
type LSPDocumentSymbol = lspserver.LSPDocumentSymbol
|
|
57
|
+
|
|
58
|
+
// LSPLocation is the driver-level alias for lspserver.LSPLocation.
|
|
59
|
+
type LSPLocation = lspserver.LSPLocation
|
|
60
|
+
|
|
61
|
+
// SymbolProvider is the driver-level alias for lspserver.SymbolProvider.
|
|
62
|
+
// It is the seam that answers textDocument/documentSymbol and
|
|
63
|
+
// textDocument/references locally from ttsc's compiler-backed code graph.
|
|
64
|
+
type SymbolProvider = lspserver.SymbolProvider
|
|
65
|
+
|
|
52
66
|
// PluginSource is the driver-level alias for lspserver.PluginSource.
|
|
53
67
|
// It is the public seam downstream pipelines implement to contribute
|
|
54
68
|
// diagnostics, code actions, and workspace commands to the LSP proxy.
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
// Package graphsymbols answers the two graph-oriented LSP methods
|
|
2
|
+
// (textDocument/documentSymbol and textDocument/references) from ttsc's
|
|
3
|
+
// compiler-backed code graph. It is the lspserver.SymbolProvider implementation
|
|
4
|
+
// ttscserver wires in so a raw-LSP graph consumer such as @samchon/graph gets
|
|
5
|
+
// compiler-complete declarations (graph nodes) and usages (graph edges) instead
|
|
6
|
+
// of the empty answers tsgo's native LSP returns for these methods.
|
|
7
|
+
//
|
|
8
|
+
// It lives in its own package rather than in internal/lspserver because it
|
|
9
|
+
// imports internal/graph and driver, and driver already imports lspserver;
|
|
10
|
+
// putting the compiler-facing logic here keeps lspserver free of that
|
|
11
|
+
// dependency cycle. Only cmd/ttscserver imports this package.
|
|
12
|
+
package graphsymbols
|
|
13
|
+
|
|
14
|
+
import (
|
|
15
|
+
"fmt"
|
|
16
|
+
"net/url"
|
|
17
|
+
"os"
|
|
18
|
+
"path/filepath"
|
|
19
|
+
"sort"
|
|
20
|
+
"strings"
|
|
21
|
+
"sync"
|
|
22
|
+
"unicode/utf16"
|
|
23
|
+
"unicode/utf8"
|
|
24
|
+
|
|
25
|
+
shimtspath "github.com/microsoft/typescript-go/shim/tspath"
|
|
26
|
+
|
|
27
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
28
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/graph"
|
|
29
|
+
"github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Provider computes documentSymbol/references from a code graph built for one
|
|
33
|
+
// project (tsconfig). The graph is loaded lazily on the first request and
|
|
34
|
+
// cached for the process lifetime: the graph-indexing use case walks a project
|
|
35
|
+
// once, so a single compiler load keeps indexing fast without re-checking on
|
|
36
|
+
// every request. It is safe for concurrent use.
|
|
37
|
+
type Provider struct {
|
|
38
|
+
cwd string
|
|
39
|
+
tsconfig string
|
|
40
|
+
|
|
41
|
+
mu sync.Mutex
|
|
42
|
+
loaded bool
|
|
43
|
+
graph *graph.Graph
|
|
44
|
+
sources map[string]string
|
|
45
|
+
loadErr error
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// NewProvider returns a Provider that builds the graph for the project rooted at
|
|
49
|
+
// cwd using tsconfig (defaulting to "tsconfig.json").
|
|
50
|
+
func NewProvider(cwd, tsconfig string) *Provider {
|
|
51
|
+
tsconfig = strings.TrimSpace(tsconfig)
|
|
52
|
+
if tsconfig == "" {
|
|
53
|
+
tsconfig = "tsconfig.json"
|
|
54
|
+
}
|
|
55
|
+
return &Provider{cwd: cwd, tsconfig: tsconfig}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// load builds (once) and returns the cached graph and per-file source texts,
|
|
59
|
+
// mirroring how cmd/ttscgraph/dump.go resolves the project and drives the graph
|
|
60
|
+
// package.
|
|
61
|
+
func (pr *Provider) load() (*graph.Graph, map[string]string, error) {
|
|
62
|
+
pr.mu.Lock()
|
|
63
|
+
defer pr.mu.Unlock()
|
|
64
|
+
if pr.loaded {
|
|
65
|
+
return pr.graph, pr.sources, pr.loadErr
|
|
66
|
+
}
|
|
67
|
+
pr.loaded = true
|
|
68
|
+
|
|
69
|
+
cwd := pr.cwd
|
|
70
|
+
if abs, err := filepath.Abs(cwd); err == nil {
|
|
71
|
+
cwd = abs
|
|
72
|
+
}
|
|
73
|
+
cwd = shimtspath.ResolvePath(cwd)
|
|
74
|
+
|
|
75
|
+
prog, _, err := driver.LoadProgram(cwd, pr.tsconfig, driver.LoadProgramOptions{})
|
|
76
|
+
if err != nil {
|
|
77
|
+
pr.loadErr = err
|
|
78
|
+
return nil, nil, err
|
|
79
|
+
}
|
|
80
|
+
if prog == nil {
|
|
81
|
+
pr.loadErr = fmt.Errorf("graphsymbols: could not load %s/%s", cwd, pr.tsconfig)
|
|
82
|
+
return nil, nil, pr.loadErr
|
|
83
|
+
}
|
|
84
|
+
defer func() { _ = prog.Close() }()
|
|
85
|
+
|
|
86
|
+
pr.graph = graph.Build(prog)
|
|
87
|
+
pr.sources = graph.SourceTexts(prog)
|
|
88
|
+
return pr.graph, pr.sources, nil
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// DocumentSymbols returns the declarations in uri as a hierarchy: top-level
|
|
92
|
+
// declarations at the root, class/interface members nested under their owner.
|
|
93
|
+
func (pr *Provider) DocumentSymbols(uri string) ([]lspserver.LSPDocumentSymbol, error) {
|
|
94
|
+
g, sources, err := pr.load()
|
|
95
|
+
if err != nil {
|
|
96
|
+
return nil, err
|
|
97
|
+
}
|
|
98
|
+
file, ok := fileFromURI(uri, sources)
|
|
99
|
+
if !ok {
|
|
100
|
+
return []lspserver.LSPDocumentSymbol{}, nil
|
|
101
|
+
}
|
|
102
|
+
text := sources[file]
|
|
103
|
+
|
|
104
|
+
var fileNodes []*graph.Node
|
|
105
|
+
for _, n := range g.Nodes {
|
|
106
|
+
if n.External {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if pathsEqual(n.File, file) {
|
|
110
|
+
fileNodes = append(fileNodes, n)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return buildDocumentSymbols(fileNodes, text), nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// References returns the usage locations of the symbol at pos in uri. When
|
|
117
|
+
// includeDeclaration is set the symbol's own declaration is added to the result.
|
|
118
|
+
func (pr *Provider) References(uri string, pos lspserver.LSPPosition, includeDeclaration bool) ([]lspserver.LSPLocation, error) {
|
|
119
|
+
g, sources, err := pr.load()
|
|
120
|
+
if err != nil {
|
|
121
|
+
return nil, err
|
|
122
|
+
}
|
|
123
|
+
file, ok := fileFromURI(uri, sources)
|
|
124
|
+
if !ok {
|
|
125
|
+
return []lspserver.LSPLocation{}, nil
|
|
126
|
+
}
|
|
127
|
+
text := sources[file]
|
|
128
|
+
offset, ok := lspPositionToOffset(text, pos)
|
|
129
|
+
if !ok {
|
|
130
|
+
return []lspserver.LSPLocation{}, nil
|
|
131
|
+
}
|
|
132
|
+
target := targetNodeAt(g, file, offset)
|
|
133
|
+
if target == nil {
|
|
134
|
+
return []lspserver.LSPLocation{}, nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
locations := []lspserver.LSPLocation{}
|
|
138
|
+
seen := map[string]struct{}{}
|
|
139
|
+
add := func(f string, start, end int) {
|
|
140
|
+
t, ok := sources[f]
|
|
141
|
+
if !ok {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if end < start {
|
|
145
|
+
end = start
|
|
146
|
+
}
|
|
147
|
+
loc := lspserver.LSPLocation{
|
|
148
|
+
URI: uriFromFile(f),
|
|
149
|
+
Range: lspserver.LSPRange{
|
|
150
|
+
Start: offsetToPosition(t, start),
|
|
151
|
+
End: offsetToPosition(t, end),
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
key := fmt.Sprintf("%s:%d:%d", loc.URI, start, end)
|
|
155
|
+
if _, dup := seen[key]; dup {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
seen[key] = struct{}{}
|
|
159
|
+
locations = append(locations, loc)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Edges are directed From a using declaration To the referenced symbol, so an
|
|
163
|
+
// edge whose To is the target node is one usage site; its span lives in the
|
|
164
|
+
// From node's file (graph's edge-evidence contract).
|
|
165
|
+
for _, e := range g.Edges {
|
|
166
|
+
if e.To != target.ID {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
f := nodeFile(e.From)
|
|
170
|
+
if f == "" {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
add(f, e.Pos, e.End)
|
|
174
|
+
}
|
|
175
|
+
if includeDeclaration {
|
|
176
|
+
declText := sources[target.File]
|
|
177
|
+
add(target.File, firstCodeOffset(declText, target.Pos), target.End)
|
|
178
|
+
}
|
|
179
|
+
return locations, nil
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// buildDocumentSymbols turns a file's graph nodes into a DocumentSymbol forest.
|
|
183
|
+
// A node whose owner (its qualified name minus its simple name) is another node
|
|
184
|
+
// in the file nests under that owner; everything else is a root.
|
|
185
|
+
func buildDocumentSymbols(nodes []*graph.Node, text string) []lspserver.LSPDocumentSymbol {
|
|
186
|
+
names := make(map[string]bool, len(nodes))
|
|
187
|
+
for _, n := range nodes {
|
|
188
|
+
names[n.Name] = true
|
|
189
|
+
}
|
|
190
|
+
childrenOf := map[string][]*graph.Node{}
|
|
191
|
+
var roots []*graph.Node
|
|
192
|
+
for _, n := range nodes {
|
|
193
|
+
owner := ownerName(n)
|
|
194
|
+
if owner != "" && names[owner] {
|
|
195
|
+
childrenOf[owner] = append(childrenOf[owner], n)
|
|
196
|
+
} else {
|
|
197
|
+
roots = append(roots, n)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
sortNodes(roots)
|
|
201
|
+
out := make([]lspserver.LSPDocumentSymbol, 0, len(roots))
|
|
202
|
+
for _, r := range roots {
|
|
203
|
+
out = append(out, buildSymbol(r, childrenOf, text))
|
|
204
|
+
}
|
|
205
|
+
return out
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// buildSymbol materializes one node (and, recursively, the nodes owned by it)
|
|
209
|
+
// as an LSPDocumentSymbol. The owner relation is a strict name-prefix, so the
|
|
210
|
+
// recursion terminates (no cycles).
|
|
211
|
+
func buildSymbol(n *graph.Node, childrenOf map[string][]*graph.Node, text string) lspserver.LSPDocumentSymbol {
|
|
212
|
+
sym := nodeToSymbol(n, text)
|
|
213
|
+
kids := childrenOf[n.Name]
|
|
214
|
+
sortNodes(kids)
|
|
215
|
+
for _, k := range kids {
|
|
216
|
+
sym.Children = append(sym.Children, buildSymbol(k, childrenOf, text))
|
|
217
|
+
}
|
|
218
|
+
return sym
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func nodeToSymbol(n *graph.Node, text string) lspserver.LSPDocumentSymbol {
|
|
222
|
+
start := firstCodeOffset(text, n.Pos)
|
|
223
|
+
end := n.End
|
|
224
|
+
if end < start {
|
|
225
|
+
end = start
|
|
226
|
+
}
|
|
227
|
+
rng := lspserver.LSPRange{
|
|
228
|
+
Start: offsetToPosition(text, start),
|
|
229
|
+
End: offsetToPosition(text, end),
|
|
230
|
+
}
|
|
231
|
+
name := n.Simple
|
|
232
|
+
if name == "" {
|
|
233
|
+
name = n.Name
|
|
234
|
+
}
|
|
235
|
+
return lspserver.LSPDocumentSymbol{
|
|
236
|
+
Name: name,
|
|
237
|
+
Kind: symbolKind(n.Kind),
|
|
238
|
+
Range: rng,
|
|
239
|
+
// The graph does not record the identifier's own span separately, so the
|
|
240
|
+
// selection range reuses the declaration range. LSP only requires it to be
|
|
241
|
+
// contained in Range, which this trivially satisfies.
|
|
242
|
+
SelectionRange: rng,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ownerName is the qualified name of a node's owner (a class/interface/namespace),
|
|
247
|
+
// or "" for a top-level declaration. It strips the trailing ".<simple>" using the
|
|
248
|
+
// node's recorded simple name so a quoted member whose name contains a dot splits
|
|
249
|
+
// exactly.
|
|
250
|
+
func ownerName(n *graph.Node) string {
|
|
251
|
+
if n.Simple == "" || n.Name == n.Simple {
|
|
252
|
+
return ""
|
|
253
|
+
}
|
|
254
|
+
suffix := "." + n.Simple
|
|
255
|
+
if strings.HasSuffix(n.Name, suffix) {
|
|
256
|
+
return n.Name[:len(n.Name)-len(suffix)]
|
|
257
|
+
}
|
|
258
|
+
return ""
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// symbolKind maps a graph node kind onto the closest LSP SymbolKind. LSP has no
|
|
262
|
+
// dedicated kind for a type alias; Struct keeps it in the named-type bucket
|
|
263
|
+
// without reporting it as a runtime class.
|
|
264
|
+
func symbolKind(k graph.NodeKind) lspserver.LSPSymbolKind {
|
|
265
|
+
switch k {
|
|
266
|
+
case graph.NodeFunction:
|
|
267
|
+
return lspserver.LSPSymbolKindFunction
|
|
268
|
+
case graph.NodeClass:
|
|
269
|
+
return lspserver.LSPSymbolKindClass
|
|
270
|
+
case graph.NodeInterface:
|
|
271
|
+
return lspserver.LSPSymbolKindInterface
|
|
272
|
+
case graph.NodeEnum:
|
|
273
|
+
return lspserver.LSPSymbolKindEnum
|
|
274
|
+
case graph.NodeMethod:
|
|
275
|
+
return lspserver.LSPSymbolKindMethod
|
|
276
|
+
case graph.NodeTypeAlias:
|
|
277
|
+
return lspserver.LSPSymbolKindStruct
|
|
278
|
+
case graph.NodeVariable:
|
|
279
|
+
return lspserver.LSPSymbolKindVariable
|
|
280
|
+
default:
|
|
281
|
+
return lspserver.LSPSymbolKindVariable
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// targetNodeAt resolves the symbol the cursor addresses: the smallest
|
|
286
|
+
// declaration whose span contains offset (so a method wins over its enclosing
|
|
287
|
+
// class), or, when the cursor sits on a usage rather than a declaration, the
|
|
288
|
+
// node an edge whose source expression covers the offset points to.
|
|
289
|
+
func targetNodeAt(g *graph.Graph, file string, offset int) *graph.Node {
|
|
290
|
+
var best *graph.Node
|
|
291
|
+
for _, n := range g.Nodes {
|
|
292
|
+
if n.External || !pathsEqual(n.File, file) {
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
if offset >= n.Pos && offset < n.End {
|
|
296
|
+
if best == nil || (n.End-n.Pos) < (best.End-best.Pos) {
|
|
297
|
+
best = n
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if best != nil {
|
|
302
|
+
return best
|
|
303
|
+
}
|
|
304
|
+
for _, e := range g.Edges {
|
|
305
|
+
if !pathsEqual(nodeFile(e.From), file) {
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
if offset >= e.Pos && offset < e.End {
|
|
309
|
+
if n, ok := g.Nodes[e.To]; ok {
|
|
310
|
+
return n
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return nil
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
func sortNodes(nodes []*graph.Node) {
|
|
318
|
+
sort.Slice(nodes, func(i, j int) bool {
|
|
319
|
+
if nodes[i].Pos != nodes[j].Pos {
|
|
320
|
+
return nodes[i].Pos < nodes[j].Pos
|
|
321
|
+
}
|
|
322
|
+
return nodes[i].Name < nodes[j].Name
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// nodeFile recovers the source file path embedded in a node id
|
|
327
|
+
// ("path#name:kind"); "" for an id without a path.
|
|
328
|
+
func nodeFile(id string) string {
|
|
329
|
+
if hash := strings.Index(id, "#"); hash >= 0 {
|
|
330
|
+
return id[:hash]
|
|
331
|
+
}
|
|
332
|
+
return ""
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// fileFromURI maps a file:// uri onto the source map key for the same file,
|
|
336
|
+
// returning that key so text lookups by it succeed. Paths are compared
|
|
337
|
+
// case-insensitively so a Windows drive-letter case mismatch between the
|
|
338
|
+
// editor's uri and tsgo's normalized path still resolves.
|
|
339
|
+
func fileFromURI(uri string, sources map[string]string) (string, bool) {
|
|
340
|
+
path, ok := filePathFromURI(uri)
|
|
341
|
+
if !ok {
|
|
342
|
+
return "", false
|
|
343
|
+
}
|
|
344
|
+
resolved := shimtspath.ResolvePath(path)
|
|
345
|
+
for key := range sources {
|
|
346
|
+
if pathsEqual(key, resolved) || pathsEqual(key, path) {
|
|
347
|
+
return key, true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return "", false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
func pathsEqual(a, b string) bool {
|
|
354
|
+
return strings.EqualFold(filepath.ToSlash(a), filepath.ToSlash(b))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// filePathFromURI decodes a file:// uri to an absolute OS path. It mirrors the
|
|
358
|
+
// proxy's own unexported converter so this package stays self-contained.
|
|
359
|
+
func filePathFromURI(raw string) (string, bool) {
|
|
360
|
+
parsed, err := url.Parse(raw)
|
|
361
|
+
if err != nil || parsed.Scheme != "file" {
|
|
362
|
+
return "", false
|
|
363
|
+
}
|
|
364
|
+
path := parsed.Path
|
|
365
|
+
if parsed.Host != "" {
|
|
366
|
+
path = "//" + parsed.Host + path
|
|
367
|
+
}
|
|
368
|
+
if path == "" {
|
|
369
|
+
return "", false
|
|
370
|
+
}
|
|
371
|
+
if os.PathSeparator == '\\' && strings.HasPrefix(path, "/") && len(path) >= 3 && path[2] == ':' {
|
|
372
|
+
path = path[1:]
|
|
373
|
+
}
|
|
374
|
+
abs, err := filepath.Abs(path)
|
|
375
|
+
if err != nil {
|
|
376
|
+
return "", false
|
|
377
|
+
}
|
|
378
|
+
return abs, true
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// uriFromFile encodes an OS path as a file:// uri.
|
|
382
|
+
func uriFromFile(path string) string {
|
|
383
|
+
slashed := filepath.ToSlash(path)
|
|
384
|
+
if !strings.HasPrefix(slashed, "/") {
|
|
385
|
+
slashed = "/" + slashed
|
|
386
|
+
}
|
|
387
|
+
return (&url.URL{Scheme: "file", Path: slashed}).String()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// firstCodeOffset advances past leading trivia (whitespace, // and /* */
|
|
391
|
+
// comments) so a declaration span starts at the keyword rather than its doc
|
|
392
|
+
// comment or indentation. It mirrors the graph dump's own trivia skip.
|
|
393
|
+
func firstCodeOffset(text string, pos int) int {
|
|
394
|
+
if pos < 0 {
|
|
395
|
+
return 0
|
|
396
|
+
}
|
|
397
|
+
i := pos
|
|
398
|
+
for i < len(text) {
|
|
399
|
+
switch {
|
|
400
|
+
case text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n':
|
|
401
|
+
i++
|
|
402
|
+
case text[i] == '/' && i+1 < len(text) && text[i+1] == '/':
|
|
403
|
+
i += 2
|
|
404
|
+
for i < len(text) && text[i] != '\n' {
|
|
405
|
+
i++
|
|
406
|
+
}
|
|
407
|
+
case text[i] == '/' && i+1 < len(text) && text[i+1] == '*':
|
|
408
|
+
i += 2
|
|
409
|
+
for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
|
|
410
|
+
i++
|
|
411
|
+
}
|
|
412
|
+
if i+1 < len(text) {
|
|
413
|
+
i += 2
|
|
414
|
+
} else {
|
|
415
|
+
i = len(text)
|
|
416
|
+
}
|
|
417
|
+
default:
|
|
418
|
+
return i
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return i
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// offsetToPosition converts a byte offset into an LSP Position (0-based line,
|
|
425
|
+
// UTF-16 code-unit column).
|
|
426
|
+
func offsetToPosition(text string, offset int) lspserver.LSPPosition {
|
|
427
|
+
if offset < 0 {
|
|
428
|
+
offset = 0
|
|
429
|
+
}
|
|
430
|
+
if offset > len(text) {
|
|
431
|
+
offset = len(text)
|
|
432
|
+
}
|
|
433
|
+
line := 0
|
|
434
|
+
lineStart := 0
|
|
435
|
+
for i := 0; i < offset; {
|
|
436
|
+
r, size := utf8.DecodeRuneInString(text[i:])
|
|
437
|
+
if size == 0 {
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
if r == '\n' {
|
|
441
|
+
line++
|
|
442
|
+
lineStart = i + size
|
|
443
|
+
}
|
|
444
|
+
i += size
|
|
445
|
+
}
|
|
446
|
+
character := 0
|
|
447
|
+
for i := lineStart; i < offset; {
|
|
448
|
+
r, size := utf8.DecodeRuneInString(text[i:])
|
|
449
|
+
if size == 0 {
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
if n := utf16.RuneLen(r); n > 0 {
|
|
453
|
+
character += n
|
|
454
|
+
} else {
|
|
455
|
+
character++
|
|
456
|
+
}
|
|
457
|
+
i += size
|
|
458
|
+
}
|
|
459
|
+
return lspserver.LSPPosition{Line: line, Character: character}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// lspPositionToOffset converts an LSP Position (0-based line, UTF-16 column)
|
|
463
|
+
// into a byte offset. It returns (offset, false) when the position points past
|
|
464
|
+
// the end of the text so the caller can treat it as "no symbol here".
|
|
465
|
+
func lspPositionToOffset(text string, pos lspserver.LSPPosition) (int, bool) {
|
|
466
|
+
if pos.Line < 0 || pos.Character < 0 {
|
|
467
|
+
return 0, false
|
|
468
|
+
}
|
|
469
|
+
i := 0
|
|
470
|
+
line := 0
|
|
471
|
+
for line < pos.Line {
|
|
472
|
+
if i >= len(text) {
|
|
473
|
+
return len(text), false
|
|
474
|
+
}
|
|
475
|
+
r, size := utf8.DecodeRuneInString(text[i:])
|
|
476
|
+
if size == 0 {
|
|
477
|
+
return len(text), false
|
|
478
|
+
}
|
|
479
|
+
if r == '\r' {
|
|
480
|
+
i += size
|
|
481
|
+
if i < len(text) && text[i] == '\n' {
|
|
482
|
+
i++
|
|
483
|
+
}
|
|
484
|
+
line++
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
if r == '\n' {
|
|
488
|
+
i += size
|
|
489
|
+
line++
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
i += size
|
|
493
|
+
}
|
|
494
|
+
units := 0
|
|
495
|
+
for units < pos.Character {
|
|
496
|
+
if i >= len(text) {
|
|
497
|
+
return len(text), false
|
|
498
|
+
}
|
|
499
|
+
r, size := utf8.DecodeRuneInString(text[i:])
|
|
500
|
+
if size == 0 {
|
|
501
|
+
return len(text), false
|
|
502
|
+
}
|
|
503
|
+
if r == '\n' || r == '\r' {
|
|
504
|
+
return i, false
|
|
505
|
+
}
|
|
506
|
+
n := utf16.RuneLen(r)
|
|
507
|
+
if n <= 0 {
|
|
508
|
+
n = 1
|
|
509
|
+
}
|
|
510
|
+
units += n
|
|
511
|
+
i += size
|
|
512
|
+
}
|
|
513
|
+
return i, true
|
|
514
|
+
}
|
|
@@ -34,6 +34,8 @@ const (
|
|
|
34
34
|
methodExecuteCommand = "workspace/executeCommand"
|
|
35
35
|
methodCancelRequest = "$/cancelRequest"
|
|
36
36
|
methodFormatting = "textDocument/formatting"
|
|
37
|
+
methodDocumentSymbol = "textDocument/documentSymbol"
|
|
38
|
+
methodReferences = "textDocument/references"
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
// formatDocumentCommand is the ttsc-owned workspace command that the lint
|
|
@@ -61,6 +63,11 @@ type ProxyOptions struct {
|
|
|
61
63
|
// run multiple proxy instances in one global command registry use this to
|
|
62
64
|
// avoid collisions; incoming prefixed ids are mapped back before dispatch.
|
|
63
65
|
ExecuteCommandIDPrefix string
|
|
66
|
+
// SymbolProvider answers textDocument/documentSymbol and
|
|
67
|
+
// textDocument/references locally from ttsc's compiler-backed code graph.
|
|
68
|
+
// When nil the proxy forwards those methods to upstream tsgo (its default
|
|
69
|
+
// behavior), which does not implement them.
|
|
70
|
+
SymbolProvider SymbolProvider
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// Proxy bridges the editor and an upstream tsgo LSP process, intercepting
|
|
@@ -75,6 +82,7 @@ type Proxy struct {
|
|
|
75
82
|
suppressExecuteCommandProvider bool
|
|
76
83
|
suppressedExecuteCommandIDs map[string]struct{}
|
|
77
84
|
executeCommandIDPrefix string
|
|
85
|
+
symbolProvider SymbolProvider
|
|
78
86
|
|
|
79
87
|
writeMu sync.Mutex // serializes WriteFrame calls to editorOut
|
|
80
88
|
upstreamWriteMu sync.Mutex // serializes writes to upstreamIn
|
|
@@ -143,6 +151,7 @@ func NewProxy(opts ProxyOptions) *Proxy {
|
|
|
143
151
|
suppressExecuteCommandProvider: opts.SuppressExecuteCommandProvider,
|
|
144
152
|
suppressedExecuteCommandIDs: commandIDSet(opts.SuppressedExecuteCommandIDs),
|
|
145
153
|
executeCommandIDPrefix: opts.ExecuteCommandIDPrefix,
|
|
154
|
+
symbolProvider: opts.SymbolProvider,
|
|
146
155
|
asyncErrCh: make(chan error, 1),
|
|
147
156
|
pendingActions: make(map[string]pendingCodeActionRequest),
|
|
148
157
|
pendingAugmentingActions: make(map[string]struct{}),
|
|
@@ -285,6 +294,14 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
|
|
|
285
294
|
if env.IsRequest() {
|
|
286
295
|
return p.handleCodeActionRequest(env)
|
|
287
296
|
}
|
|
297
|
+
case methodDocumentSymbol:
|
|
298
|
+
if env.IsRequest() {
|
|
299
|
+
return p.handleDocumentSymbolRequest(env)
|
|
300
|
+
}
|
|
301
|
+
case methodReferences:
|
|
302
|
+
if env.IsRequest() {
|
|
303
|
+
return p.handleReferencesRequest(env)
|
|
304
|
+
}
|
|
288
305
|
case methodCancelRequest:
|
|
289
306
|
// $/cancelRequest names an in-flight id the editor has given up on.
|
|
290
307
|
// The proxy drops any pending codeAction entry for that id so the
|
|
@@ -1499,6 +1516,21 @@ func (p *Proxy) augmentInitializeResult(env Envelope) ([]byte, bool) {
|
|
|
1499
1516
|
changed = true
|
|
1500
1517
|
}
|
|
1501
1518
|
}
|
|
1519
|
+
// Advertise documentSymbol/references when a local SymbolProvider is wired so
|
|
1520
|
+
// graph consumers know to call them. The proxy intercepts both methods
|
|
1521
|
+
// (handleEditorEnvelope) whether or not upstream tsgo advertised them, so
|
|
1522
|
+
// forcing the capability on is safe: tsgo's own (incomplete) handlers are
|
|
1523
|
+
// never reached for these methods.
|
|
1524
|
+
if p.symbolProvider != nil {
|
|
1525
|
+
if existing, ok := caps["documentSymbolProvider"].(bool); !ok || !existing {
|
|
1526
|
+
caps["documentSymbolProvider"] = true
|
|
1527
|
+
changed = true
|
|
1528
|
+
}
|
|
1529
|
+
if existing, ok := caps["referencesProvider"].(bool); !ok || !existing {
|
|
1530
|
+
caps["referencesProvider"] = true
|
|
1531
|
+
changed = true
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1502
1534
|
if !changed {
|
|
1503
1535
|
return nil, false
|
|
1504
1536
|
}
|
|
@@ -62,6 +62,10 @@ type LSPServerOptions struct {
|
|
|
62
62
|
// Source contributes ttsc plugin diagnostics / code actions /
|
|
63
63
|
// executeCommand handling. Nil falls back to NullPluginSource{}.
|
|
64
64
|
Source PluginSource
|
|
65
|
+
// SymbolProvider answers textDocument/documentSymbol and
|
|
66
|
+
// textDocument/references locally from ttsc's compiler-backed code graph.
|
|
67
|
+
// Nil leaves those methods forwarded to upstream tsgo.
|
|
68
|
+
SymbolProvider SymbolProvider
|
|
65
69
|
// SuppressExecuteCommandProvider keeps ttsc command ids out of the
|
|
66
70
|
// initialize response for clients that route wrapper commands themselves.
|
|
67
71
|
SuppressExecuteCommandProvider bool
|
|
@@ -180,6 +184,7 @@ func RunLSPServer(ctx context.Context, opts LSPServerOptions) error {
|
|
|
180
184
|
SuppressExecuteCommandProvider: opts.SuppressExecuteCommandProvider,
|
|
181
185
|
SuppressedExecuteCommandIDs: opts.SuppressedExecuteCommandIDs,
|
|
182
186
|
ExecuteCommandIDPrefix: opts.ExecuteCommandIDPrefix,
|
|
187
|
+
SymbolProvider: opts.SymbolProvider,
|
|
183
188
|
})
|
|
184
189
|
|
|
185
190
|
serverCtx, cancel := context.WithCancel(ctx)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
// lsp_symbols.go adds the two graph-oriented language methods the proxy answers
|
|
4
|
+
// locally instead of forwarding to the wrapped tsgo LSP, which does not
|
|
5
|
+
// implement them: textDocument/documentSymbol and textDocument/references. The
|
|
6
|
+
// facts come from a SymbolProvider computed off ttsc's compiler-backed code
|
|
7
|
+
// graph (see internal/graphsymbols), so a raw-LSP graph consumer such as
|
|
8
|
+
// @samchon/graph gets compiler-complete declarations (nodes) and usages (edges).
|
|
9
|
+
|
|
10
|
+
import "encoding/json"
|
|
11
|
+
|
|
12
|
+
// LSPSymbolKind mirrors the LSP SymbolKind enum. Only the members the graph's
|
|
13
|
+
// node kinds map onto are named here; the numeric values are the wire contract.
|
|
14
|
+
type LSPSymbolKind int
|
|
15
|
+
|
|
16
|
+
const (
|
|
17
|
+
LSPSymbolKindClass LSPSymbolKind = 5
|
|
18
|
+
LSPSymbolKindMethod LSPSymbolKind = 6
|
|
19
|
+
LSPSymbolKindEnum LSPSymbolKind = 10
|
|
20
|
+
LSPSymbolKindInterface LSPSymbolKind = 11
|
|
21
|
+
LSPSymbolKindFunction LSPSymbolKind = 12
|
|
22
|
+
LSPSymbolKindVariable LSPSymbolKind = 13
|
|
23
|
+
LSPSymbolKindStruct LSPSymbolKind = 23
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// LSPDocumentSymbol is the hierarchical shape returned by
|
|
27
|
+
// textDocument/documentSymbol. Range spans the whole declaration and
|
|
28
|
+
// SelectionRange the name (LSP requires SelectionRange to be contained in
|
|
29
|
+
// Range). Children nest members (a class's methods) under their owner.
|
|
30
|
+
type LSPDocumentSymbol struct {
|
|
31
|
+
Name string `json:"name"`
|
|
32
|
+
Kind LSPSymbolKind `json:"kind"`
|
|
33
|
+
Range LSPRange `json:"range"`
|
|
34
|
+
SelectionRange LSPRange `json:"selectionRange"`
|
|
35
|
+
Children []LSPDocumentSymbol `json:"children,omitempty"`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// LSPLocation is the wire shape of an LSP Location: a range inside a document,
|
|
39
|
+
// returned by textDocument/references for each usage site.
|
|
40
|
+
type LSPLocation struct {
|
|
41
|
+
URI string `json:"uri"`
|
|
42
|
+
Range LSPRange `json:"range"`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// SymbolProvider computes documentSymbol and references locally from ttsc's
|
|
46
|
+
// compiler-backed code graph so the proxy can answer these two methods rather
|
|
47
|
+
// than forwarding them to tsgo's native LSP, which does not implement them.
|
|
48
|
+
//
|
|
49
|
+
// Implementations may load a compiler Program lazily and must be safe to call
|
|
50
|
+
// from multiple goroutines: the proxy invokes them off its pump goroutine so a
|
|
51
|
+
// slow program load never blocks other traffic.
|
|
52
|
+
type SymbolProvider interface {
|
|
53
|
+
// DocumentSymbols returns the declarations in the document identified by uri
|
|
54
|
+
// as a hierarchy of LSPDocumentSymbol. A document with no declarations
|
|
55
|
+
// yields an empty (non-nil) slice.
|
|
56
|
+
DocumentSymbols(uri string) ([]LSPDocumentSymbol, error)
|
|
57
|
+
|
|
58
|
+
// References returns the usage locations of the symbol at pos in the document
|
|
59
|
+
// identified by uri. includeDeclaration adds the symbol's own declaration to
|
|
60
|
+
// the result. A position that resolves to no symbol yields an empty slice.
|
|
61
|
+
References(uri string, pos LSPPosition, includeDeclaration bool) ([]LSPLocation, error)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// handleDocumentSymbolRequest answers textDocument/documentSymbol from the local
|
|
65
|
+
// SymbolProvider. When no provider is wired it returns false so the request
|
|
66
|
+
// flows to upstream tsgo, preserving the proxy's default forward behavior. The
|
|
67
|
+
// computation runs on its own goroutine (a program load can be slow) so the
|
|
68
|
+
// editor->upstream pump keeps servicing other traffic.
|
|
69
|
+
func (p *Proxy) handleDocumentSymbolRequest(env Envelope) (bool, error) {
|
|
70
|
+
if p.symbolProvider == nil {
|
|
71
|
+
return false, nil
|
|
72
|
+
}
|
|
73
|
+
var params struct {
|
|
74
|
+
TextDocument struct {
|
|
75
|
+
URI string `json:"uri"`
|
|
76
|
+
} `json:"textDocument"`
|
|
77
|
+
}
|
|
78
|
+
if err := json.Unmarshal(env.Params, ¶ms); err != nil || params.TextDocument.URI == "" {
|
|
79
|
+
return false, nil
|
|
80
|
+
}
|
|
81
|
+
go p.completeDocumentSymbolRequest(env, params.TextDocument.URI)
|
|
82
|
+
return true, nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func (p *Proxy) completeDocumentSymbolRequest(env Envelope, uri string) {
|
|
86
|
+
symbols, err := p.symbolProvider.DocumentSymbols(uri)
|
|
87
|
+
if err != nil || symbols == nil {
|
|
88
|
+
// A provider failure must not surface as an LSP error to a graph
|
|
89
|
+
// consumer mid-index; reply with an empty result so the client moves on.
|
|
90
|
+
symbols = []LSPDocumentSymbol{}
|
|
91
|
+
}
|
|
92
|
+
p.reportAsyncError(p.writeResult(env.ID, symbols))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// handleReferencesRequest answers textDocument/references from the local
|
|
96
|
+
// SymbolProvider, mirroring handleDocumentSymbolRequest's forward-when-unwired
|
|
97
|
+
// and off-pump-goroutine behavior.
|
|
98
|
+
func (p *Proxy) handleReferencesRequest(env Envelope) (bool, error) {
|
|
99
|
+
if p.symbolProvider == nil {
|
|
100
|
+
return false, nil
|
|
101
|
+
}
|
|
102
|
+
var params struct {
|
|
103
|
+
TextDocument struct {
|
|
104
|
+
URI string `json:"uri"`
|
|
105
|
+
} `json:"textDocument"`
|
|
106
|
+
Position LSPPosition `json:"position"`
|
|
107
|
+
Context struct {
|
|
108
|
+
IncludeDeclaration bool `json:"includeDeclaration"`
|
|
109
|
+
} `json:"context"`
|
|
110
|
+
}
|
|
111
|
+
if err := json.Unmarshal(env.Params, ¶ms); err != nil || params.TextDocument.URI == "" {
|
|
112
|
+
return false, nil
|
|
113
|
+
}
|
|
114
|
+
go p.completeReferencesRequest(env, params.TextDocument.URI, params.Position, params.Context.IncludeDeclaration)
|
|
115
|
+
return true, nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func (p *Proxy) completeReferencesRequest(env Envelope, uri string, pos LSPPosition, includeDeclaration bool) {
|
|
119
|
+
locations, err := p.symbolProvider.References(uri, pos, includeDeclaration)
|
|
120
|
+
if err != nil || locations == nil {
|
|
121
|
+
locations = []LSPLocation{}
|
|
122
|
+
}
|
|
123
|
+
p.reportAsyncError(p.writeResult(env.ID, locations))
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ttsc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@ttsc/linux-
|
|
36
|
-
"@ttsc/linux-
|
|
37
|
-
"@ttsc/
|
|
38
|
-
"@ttsc/
|
|
39
|
-
"@ttsc/
|
|
40
|
-
"@ttsc/
|
|
41
|
-
"@ttsc/
|
|
35
|
+
"@ttsc/linux-x64": "0.18.0",
|
|
36
|
+
"@ttsc/linux-arm": "0.18.0",
|
|
37
|
+
"@ttsc/linux-arm64": "0.18.0",
|
|
38
|
+
"@ttsc/win32-x64": "0.18.0",
|
|
39
|
+
"@ttsc/win32-arm64": "0.18.0",
|
|
40
|
+
"@ttsc/darwin-arm64": "0.18.0",
|
|
41
|
+
"@ttsc/darwin-x64": "0.18.0"
|
|
42
42
|
},
|
|
43
43
|
"sideEffects": false,
|
|
44
44
|
"files": [
|