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.
@@ -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, &params); 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, &params); 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.17.4",
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-arm": "0.17.4",
36
- "@ttsc/linux-arm64": "0.17.4",
37
- "@ttsc/darwin-x64": "0.17.4",
38
- "@ttsc/darwin-arm64": "0.17.4",
39
- "@ttsc/linux-x64": "0.17.4",
40
- "@ttsc/win32-x64": "0.17.4",
41
- "@ttsc/win32-arm64": "0.17.4"
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": [