ttsc 0.18.4 → 0.19.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.
Files changed (137) hide show
  1. package/README.md +5 -6
  2. package/cmd/graphbench/main.go +1 -1
  3. package/cmd/platform/main.go +1 -48
  4. package/cmd/ttsc/build_branches_test.go +5 -1
  5. package/cmd/ttsc/main.go +0 -50
  6. package/cmd/ttsc/main_alias_branches_test.go +6 -12
  7. package/cmd/ttscgraph/dump.go +13 -3
  8. package/cmd/ttscserver/main.go +5 -3
  9. package/driver/lsp.go +13 -1
  10. package/driver/plugins.go +35 -0
  11. package/driver/program.go +7 -8
  12. package/driver/rewrite.go +381 -21
  13. package/go.mod +1 -0
  14. package/internal/graph/build.go +297 -3
  15. package/internal/graph/build_records_a_node_per_top_level_declaration_test.go +16 -2
  16. package/internal/graph/diagnostics.go +5 -4
  17. package/internal/graph/dump.go +60 -4
  18. package/internal/graph/dump_is_a_function_of_the_source_test.go +71 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +16 -4
  20. package/internal/graph/edges.go +122 -11
  21. package/internal/graph/exports.go +74 -17
  22. package/internal/graph/graph.go +33 -1
  23. package/internal/graph/node_identity_is_position_invariant_test.go +4 -2
  24. package/internal/graph/resolve.go +24 -0
  25. package/internal/graphsymbols/provider.go +50 -14
  26. package/internal/lspserver/lsp_native_plugin_source.go +77 -23
  27. package/internal/lspserver/lsp_plugin.go +19 -2
  28. package/internal/lspserver/lsp_proxy.go +244 -52
  29. package/internal/lspserver/lsp_server.go +35 -27
  30. package/internal/lspserver/lsp_symbols.go +25 -16
  31. package/lib/TtscCompiler.d.ts +10 -0
  32. package/lib/TtscCompiler.js +24 -6
  33. package/lib/TtscCompiler.js.map +1 -1
  34. package/lib/TtscService.js +9 -7
  35. package/lib/TtscService.js.map +1 -1
  36. package/lib/compiler/internal/compileProjectInMemory.js +1 -0
  37. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  38. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +3 -0
  39. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +14 -0
  40. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -0
  41. package/lib/compiler/internal/project/readProjectConfig.js +53 -4
  42. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +9 -2
  44. package/lib/compiler/internal/project/resolveProjectConfig.js +45 -16
  45. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  46. package/lib/compiler/internal/residentTransformProcess.d.ts +16 -5
  47. package/lib/compiler/internal/residentTransformProcess.js +68 -20
  48. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  49. package/lib/compiler/internal/runBuild.d.ts +11 -0
  50. package/lib/compiler/internal/runBuild.js +44 -8
  51. package/lib/compiler/internal/runBuild.js.map +1 -1
  52. package/lib/compiler/internal/runSingleFileEmit.js +7 -4
  53. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  54. package/lib/compiler/internal/sharedHostHelpers.d.ts +16 -0
  55. package/lib/compiler/internal/sharedHostHelpers.js +23 -0
  56. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  57. package/lib/compiler/internal/startResidentTransform.js +18 -1
  58. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  59. package/lib/compiler/internal/transformProjectInMemory.js +34 -8
  60. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  61. package/lib/flags/parser.d.ts +17 -0
  62. package/lib/flags/parser.js +11 -1
  63. package/lib/flags/parser.js.map +1 -1
  64. package/lib/flags/schema.js +7 -0
  65. package/lib/flags/schema.js.map +1 -1
  66. package/lib/launcher/internal/prepareExecution.js +21 -2
  67. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  68. package/lib/launcher/internal/runTtsc.js +13 -53
  69. package/lib/launcher/internal/runTtsc.js.map +1 -1
  70. package/lib/launcher/internal/runTtscserver.js +11 -0
  71. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  72. package/lib/launcher/internal/runTtsx.js +27 -11
  73. package/lib/launcher/internal/runTtsx.js.map +1 -1
  74. package/lib/launcher/internal/runtimeHooks.d.ts +81 -0
  75. package/lib/launcher/internal/runtimeHooks.js +632 -43
  76. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  77. package/lib/launcher/internal/servedSourceMap.d.ts +25 -0
  78. package/lib/launcher/internal/servedSourceMap.js +164 -0
  79. package/lib/launcher/internal/servedSourceMap.js.map +1 -0
  80. package/lib/plugin/internal/buildSourcePlugin.d.ts +146 -2
  81. package/lib/plugin/internal/buildSourcePlugin.js +651 -113
  82. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  83. package/lib/plugin/internal/loadProjectPlugins.d.ts +7 -0
  84. package/lib/plugin/internal/loadProjectPlugins.js +33 -10
  85. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  86. package/lib/structures/ITtscCompilerContext.d.ts +16 -2
  87. package/lib/structures/ITtscPluginCapabilities.d.ts +8 -0
  88. package/lib/structures/ITtscPluginFactoryContext.d.ts +14 -0
  89. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +3 -0
  90. package/lib/structures/internal/ITtscProjectIdentity.d.ts +17 -0
  91. package/lib/structures/internal/ITtscProjectIdentity.js +3 -0
  92. package/lib/structures/internal/ITtscProjectIdentity.js.map +1 -0
  93. package/lib/structures/internal/TtscCommonOptions.d.ts +8 -0
  94. package/package.json +11 -8
  95. package/shim/ast/lint.go +10 -0
  96. package/shim/ast/shim.go +44 -0
  97. package/shim/checker/enums_gen.go +19 -0
  98. package/shim/checker/shim.go +130 -0
  99. package/shim/diagnosticwriter/lint.go +30 -3
  100. package/shim/scanner/regexp.go +27 -0
  101. package/src/TtscCompiler.ts +25 -6
  102. package/src/TtscService.ts +15 -7
  103. package/src/compiler/internal/compileProjectInMemory.ts +1 -0
  104. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +15 -0
  105. package/src/compiler/internal/project/readProjectConfig.ts +61 -5
  106. package/src/compiler/internal/project/resolveProjectConfig.ts +51 -21
  107. package/src/compiler/internal/residentTransformProcess.ts +95 -21
  108. package/src/compiler/internal/runBuild.ts +76 -9
  109. package/src/compiler/internal/runSingleFileEmit.ts +7 -4
  110. package/src/compiler/internal/sharedHostHelpers.ts +24 -0
  111. package/src/compiler/internal/startResidentTransform.ts +21 -1
  112. package/src/compiler/internal/transformProjectInMemory.ts +49 -6
  113. package/src/flags/parser.ts +28 -1
  114. package/src/flags/schema.ts +8 -0
  115. package/src/launcher/internal/prepareExecution.ts +22 -2
  116. package/src/launcher/internal/runTtsc.ts +13 -61
  117. package/src/launcher/internal/runTtscserver.ts +14 -0
  118. package/src/launcher/internal/runTtsx.ts +28 -13
  119. package/src/launcher/internal/runtimeHooks.ts +771 -53
  120. package/src/launcher/internal/servedSourceMap.ts +183 -0
  121. package/src/plugin/internal/buildSourcePlugin.ts +859 -122
  122. package/src/plugin/internal/loadProjectPlugins.ts +36 -6
  123. package/src/structures/ITtscCompilerContext.ts +17 -2
  124. package/src/structures/ITtscPluginCapabilities.ts +9 -0
  125. package/src/structures/ITtscPluginFactoryContext.ts +15 -0
  126. package/src/structures/internal/ITtscParsedProjectConfig.ts +3 -0
  127. package/src/structures/internal/ITtscProjectIdentity.ts +17 -0
  128. package/src/structures/internal/TtscCommonOptions.ts +8 -0
  129. package/tools/shim_audit/baseline.json +16 -7
  130. package/tools/shim_audit/main.go +777 -71
  131. package/tools/shim_audit/main_test.go +28 -1
  132. package/tools/shim_audit/producer_exemptions_require_rationale_test.go +42 -0
  133. package/tools/shim_audit/producer_surface_includes_exposed_methods_test.go +154 -0
  134. package/tools/shim_audit/producer_surface_rejects_unproducible_reference_test.go +142 -0
  135. package/utility/host.go +46 -20
  136. package/utility/serve.go +1 -1
  137. package/shim/checker/signature_api_completeness.go +0 -58
@@ -21,8 +21,9 @@ const nativePluginCommandStderrLimit = 1024 * 1024
21
21
  // passes through TTSC_LSP_PLUGINS_JSON after running normal project plugin
22
22
  // discovery and source-plugin builds.
23
23
  type NativePluginManifest struct {
24
- Plugins []NativePluginConfigEntry `json:"plugins"`
25
- LSPPlugins []NativeLSPPluginEntry `json:"lspPlugins"`
24
+ Plugins []NativePluginConfigEntry `json:"plugins"`
25
+ LSPPlugins []NativeLSPPluginEntry `json:"lspPlugins"`
26
+ ProjectContext json.RawMessage `json:"projectContext,omitempty"`
26
27
  }
27
28
 
28
29
  // NativePluginConfigEntry mirrors the compact sidecar protocol used by
@@ -36,9 +37,10 @@ type NativePluginConfigEntry struct {
36
37
  // NativeLSPPluginEntry names one built sidecar that opted into the LSP
37
38
  // protocol through its JavaScript descriptor capabilities.
38
39
  type NativeLSPPluginEntry struct {
39
- Binary string `json:"binary"`
40
- Name string `json:"name,omitempty"`
41
- Stage string `json:"stage,omitempty"`
40
+ Binary string `json:"binary"`
41
+ Name string `json:"name,omitempty"`
42
+ ProjectContextArgs bool `json:"projectContextArgs,omitempty"`
43
+ Stage string `json:"stage,omitempty"`
42
44
  }
43
45
 
44
46
  // NativePluginSourceOptions configures a sidecar-backed PluginSource.
@@ -52,11 +54,12 @@ type NativePluginSourceOptions struct {
52
54
  // NativePluginSource implements PluginSource by delegating to native sidecars
53
55
  // that explicitly support ttsc's LSP subcommands.
54
56
  type NativePluginSource struct {
55
- cwd string
56
- err io.Writer
57
- plugins []NativeLSPPluginEntry
58
- pluginsJSON string
59
- tsconfig string
57
+ cwd string
58
+ err io.Writer
59
+ plugins []NativeLSPPluginEntry
60
+ pluginsJSON string
61
+ projectContextJSON string
62
+ tsconfig string
60
63
 
61
64
  commandIDs []string
62
65
  codeActionKinds []string
@@ -110,40 +113,88 @@ func NewNativePluginSource(opts NativePluginSourceOptions) (*NativePluginSource,
110
113
  if err != nil {
111
114
  return nil, fmt.Errorf("ttscserver: encode plugin manifest: %w", err)
112
115
  }
116
+ sidecarCwd := opts.Cwd
117
+ sidecarTsconfig := opts.Tsconfig
118
+ if len(manifest.ProjectContext) > 0 {
119
+ var identity struct {
120
+ PhysicalConfigPath string `json:"physicalConfigPath"`
121
+ PhysicalProjectRoot string `json:"physicalProjectRoot"`
122
+ }
123
+ if err := json.Unmarshal(manifest.ProjectContext, &identity); err != nil {
124
+ return nil, fmt.Errorf("ttscserver: decode project context: %w", err)
125
+ }
126
+ if strings.TrimSpace(identity.PhysicalProjectRoot) != "" {
127
+ sidecarCwd = identity.PhysicalProjectRoot
128
+ }
129
+ if strings.TrimSpace(identity.PhysicalConfigPath) != "" {
130
+ sidecarTsconfig = identity.PhysicalConfigPath
131
+ }
132
+ }
113
133
  source := &NativePluginSource{
114
- cwd: opts.Cwd,
115
- err: opts.Err,
116
- plugins: manifest.LSPPlugins,
117
- pluginsJSON: string(pluginsJSON),
118
- tsconfig: opts.Tsconfig,
119
- owners: map[string]NativeLSPPluginEntry{},
134
+ cwd: sidecarCwd,
135
+ err: opts.Err,
136
+ plugins: manifest.LSPPlugins,
137
+ pluginsJSON: string(pluginsJSON),
138
+ projectContextJSON: string(manifest.ProjectContext),
139
+ tsconfig: sidecarTsconfig,
140
+ owners: map[string]NativeLSPPluginEntry{},
120
141
  }
121
142
  source.discoverCommandIDs()
122
143
  return source, nil
123
144
  }
124
145
 
125
- // Diagnostics asks every LSP-capable sidecar for diagnostics matching doc.URI.
126
- func (s *NativePluginSource) Diagnostics(doc LSPDocumentVersion) []LSPDiagnostic {
146
+ // Diagnostics asks every LSP-capable sidecar for document diagnostics and its
147
+ // separate project publication.
148
+ func (s *NativePluginSource) Diagnostics(doc LSPDocumentVersion) LSPDiagnosticsResult {
127
149
  if s == nil || doc.URI == "" {
128
- return nil
150
+ return LSPDiagnosticsResult{}
129
151
  }
130
- var out []LSPDiagnostic
152
+ out := LSPDiagnosticsResult{}
131
153
  for _, plugin := range s.plugins {
132
154
  body, err := s.run(plugin, "lsp-diagnostics", "--uri="+doc.URI)
133
155
  if err != nil {
134
156
  s.log("%v", err)
135
157
  continue
136
158
  }
137
- var diagnostics []LSPDiagnostic
138
- if err := json.Unmarshal(body, &diagnostics); err != nil {
159
+ result, err := decodeNativeDiagnostics(body)
160
+ if err != nil {
139
161
  s.log("ttscserver: %s lsp-diagnostics returned invalid JSON: %v", pluginLabel(plugin), err)
140
162
  continue
141
163
  }
142
- out = append(out, diagnostics...)
164
+ out.Document = append(out.Document, result.Document...)
165
+ if result.Project == nil {
166
+ continue
167
+ }
168
+ if out.Project == nil {
169
+ copied := *result.Project
170
+ copied.Diagnostics = append([]LSPDiagnostic(nil), result.Project.Diagnostics...)
171
+ out.Project = &copied
172
+ continue
173
+ }
174
+ if out.Project.URI != result.Project.URI {
175
+ s.log("ttscserver: %s returned project diagnostics for %s after %s; replacing the prior project publication", pluginLabel(plugin), result.Project.URI, out.Project.URI)
176
+ copied := *result.Project
177
+ copied.Diagnostics = append([]LSPDiagnostic(nil), result.Project.Diagnostics...)
178
+ out.Project = &copied
179
+ continue
180
+ }
181
+ out.Project.Diagnostics = append(out.Project.Diagnostics, result.Project.Diagnostics...)
143
182
  }
144
183
  return out
145
184
  }
146
185
 
186
+ func decodeNativeDiagnostics(body []byte) (LSPDiagnosticsResult, error) {
187
+ var result LSPDiagnosticsResult
188
+ if err := json.Unmarshal(body, &result); err == nil {
189
+ return result, nil
190
+ }
191
+ var legacy []LSPDiagnostic
192
+ if err := json.Unmarshal(body, &legacy); err != nil {
193
+ return LSPDiagnosticsResult{}, err
194
+ }
195
+ return LSPDiagnosticsResult{Document: legacy}, nil
196
+ }
197
+
147
198
  // CodeActions asks every LSP-capable sidecar for actions matching the request.
148
199
  func (s *NativePluginSource) CodeActions(uri string, rng LSPRange, ctx LSPCodeActionContext) []LSPCodeAction {
149
200
  if s == nil || uri == "" {
@@ -353,6 +404,9 @@ func (s *NativePluginSource) runWithStdin(plugin NativeLSPPluginEntry, command s
353
404
  "--tsconfig=" + s.tsconfig,
354
405
  "--plugins-json=" + s.pluginsJSON,
355
406
  }
407
+ if plugin.ProjectContextArgs && strings.TrimSpace(s.projectContextJSON) != "" {
408
+ allArgs = append(allArgs, "--project-context-json="+s.projectContextJSON)
409
+ }
356
410
  allArgs = append(allArgs, args...)
357
411
  cmd := exec.CommandContext(ctx, plugin.Binary, allArgs...)
358
412
  cmd.Dir = s.cwd
@@ -100,6 +100,21 @@ type LSPDocumentVersion struct {
100
100
  Version *int
101
101
  }
102
102
 
103
+ // LSPProjectDiagnostics is one project-scoped diagnostic publication. URI is
104
+ // the logical selected config URI and Diagnostics use a zero-width start range.
105
+ type LSPProjectDiagnostics struct {
106
+ URI string `json:"uri"`
107
+ Diagnostics []LSPDiagnostic `json:"diagnostics"`
108
+ }
109
+
110
+ // LSPDiagnosticsResult separates diagnostics for the requested document from
111
+ // the current project publication so the proxy never copies a project finding
112
+ // onto every open source document.
113
+ type LSPDiagnosticsResult struct {
114
+ Document []LSPDiagnostic `json:"document"`
115
+ Project *LSPProjectDiagnostics `json:"project,omitempty"`
116
+ }
117
+
103
118
  // PluginSource is the seam between the LSP proxy and ttsc's plugin
104
119
  // pipeline. Returning empty slices/nil is a valid "no contribution"
105
120
  // answer; ttscserver still forwards the upstream tsgo response verbatim.
@@ -112,7 +127,7 @@ type PluginSource interface {
112
127
  // the field. The proxy appends these to whatever upstream tsgo
113
128
  // published, so duplicates between ttsc and tsgo must be deduplicated
114
129
  // on the source side.
115
- Diagnostics(doc LSPDocumentVersion) []LSPDiagnostic
130
+ Diagnostics(doc LSPDocumentVersion) LSPDiagnosticsResult
116
131
 
117
132
  // CodeActions contributes additional actions for the given range. The
118
133
  // proxy appends them to upstream responses, or answers locally when
@@ -138,7 +153,9 @@ type PluginSource interface {
138
153
  type NullPluginSource struct{}
139
154
 
140
155
  // Diagnostics returns no plugin diagnostics.
141
- func (NullPluginSource) Diagnostics(LSPDocumentVersion) []LSPDiagnostic { return nil }
156
+ func (NullPluginSource) Diagnostics(LSPDocumentVersion) LSPDiagnosticsResult {
157
+ return LSPDiagnosticsResult{}
158
+ }
142
159
 
143
160
  // CodeActions returns no plugin code actions.
144
161
  func (NullPluginSource) CodeActions(string, LSPRange, LSPCodeActionContext) []LSPCodeAction {
@@ -44,6 +44,11 @@ const (
44
44
  // formats the live (possibly dirty) buffer rather than the on-disk file.
45
45
  const formatDocumentCommand = "ttsc.format.document"
46
46
 
47
+ // utf8BOM is the UTF-8 byte-order mark (U+FEFF, bytes EF BB BF). It is stripped
48
+ // from both the on-disk bytes and the editor buffer before a clean/dirty
49
+ // comparison so a BOM present on only one side does not read as an edit.
50
+ const utf8BOM = "\uFEFF"
51
+
47
52
  // ProxyOptions wires the byte-level proxy together. ttscserver creates
48
53
  // the upstream pipes around `tsgo --lsp --stdio` and hands the proxy
49
54
  // editor stdio plus those pipe ends.
@@ -64,10 +69,18 @@ type ProxyOptions struct {
64
69
  // avoid collisions; incoming prefixed ids are mapped back before dispatch.
65
70
  ExecuteCommandIDPrefix string
66
71
  // 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.
72
+ // textDocument/references from ttsc's compiler-backed code graph. tsgo
73
+ // implements both methods, so the proxy forwards to tsgo whenever it
74
+ // advertises the capability and consults this provider only as a fallback
75
+ // (tsgo did not advertise) or when ForceLocalSymbolProvider is set. Nil leaves
76
+ // both methods forwarded to tsgo unconditionally.
70
77
  SymbolProvider SymbolProvider
78
+ // ForceLocalSymbolProvider answers documentSymbol/references from
79
+ // SymbolProvider even when upstream tsgo advertises the capability. It serves
80
+ // a raw-LSP graph consumer (such as @samchon/graph) that wants graph-derived
81
+ // declarations and usages instead of tsgo's language-service answers. Ignored
82
+ // when SymbolProvider is nil.
83
+ ForceLocalSymbolProvider bool
71
84
  }
72
85
 
73
86
  // Proxy bridges the editor and an upstream tsgo LSP process, intercepting
@@ -83,6 +96,7 @@ type Proxy struct {
83
96
  suppressedExecuteCommandIDs map[string]struct{}
84
97
  executeCommandIDPrefix string
85
98
  symbolProvider SymbolProvider
99
+ forceLocalSymbolProvider bool
86
100
 
87
101
  writeMu sync.Mutex // serializes WriteFrame calls to editorOut
88
102
  upstreamWriteMu sync.Mutex // serializes writes to upstreamIn
@@ -95,16 +109,21 @@ type Proxy struct {
95
109
  pendingCommands map[string]struct{}
96
110
  pendingInitialize map[string]struct{}
97
111
 
98
- capabilityMu sync.Mutex
99
- upstreamCodeActionProvider bool
100
-
101
- diagnosticsMu sync.Mutex
102
- upstreamDiagnostics map[string]cachedDiagnostics
103
- pluginDiagnostics map[string]cachedDiagnostics
104
- diagnosticGeneration map[string]uint64
105
- documentGeneration map[string]uint64
106
- dirtyDocuments map[string]struct{}
107
- dirtyVersions map[string]*int
112
+ capabilityMu sync.Mutex
113
+ upstreamCodeActionProvider bool
114
+ upstreamDocumentSymbolProvider bool
115
+ upstreamReferencesProvider bool
116
+
117
+ diagnosticsMu sync.Mutex
118
+ upstreamDiagnostics map[string]cachedDiagnostics
119
+ pluginDiagnostics map[string]cachedDiagnostics
120
+ projectDiagnostics cachedDiagnostics
121
+ projectDiagnosticsURI string
122
+ projectDiagnosticGeneration uint64
123
+ diagnosticGeneration map[string]uint64
124
+ documentGeneration map[string]uint64
125
+ dirtyDocuments map[string]struct{}
126
+ dirtyVersions map[string]*int
108
127
  // documentText caches the live editor buffer per uri so the
109
128
  // textDocument/formatting handler can format the in-memory text instead
110
129
  // of the on-disk file. didOpen / full-sync didChange seed it with the full
@@ -152,6 +171,7 @@ func NewProxy(opts ProxyOptions) *Proxy {
152
171
  suppressedExecuteCommandIDs: commandIDSet(opts.SuppressedExecuteCommandIDs),
153
172
  executeCommandIDPrefix: opts.ExecuteCommandIDPrefix,
154
173
  symbolProvider: opts.SymbolProvider,
174
+ forceLocalSymbolProvider: opts.ForceLocalSymbolProvider,
155
175
  asyncErrCh: make(chan error, 1),
156
176
  pendingActions: make(map[string]pendingCodeActionRequest),
157
177
  pendingAugmentingActions: make(map[string]struct{}),
@@ -268,10 +288,12 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
268
288
  }
269
289
  case methodDidSave:
270
290
  if env.IsNotification() {
291
+ p.invalidateSymbolProvider()
271
292
  p.publishPluginDiagnosticsForDocumentNotification(env)
272
293
  }
273
294
  case methodDidChange:
274
295
  if env.IsNotification() {
296
+ p.invalidateSymbolProvider()
275
297
  p.cacheDidChangeText(env)
276
298
  if err := p.markDocumentDirty(env); err != nil {
277
299
  return false, err
@@ -427,6 +449,16 @@ func (p *Proxy) takePendingCommand(key string) bool {
427
449
  return true
428
450
  }
429
451
 
452
+ type codeActionPositionWire struct {
453
+ Line *int `json:"line"`
454
+ Character *int `json:"character"`
455
+ }
456
+
457
+ type codeActionRangeWire struct {
458
+ Start *codeActionPositionWire `json:"start"`
459
+ End *codeActionPositionWire `json:"end"`
460
+ }
461
+
430
462
  // decodeCodeActionRequest extracts the request payload so the matching
431
463
  // response from upstream can be augmented with ttsc-owned code actions
432
464
  // for the same range.
@@ -435,15 +467,37 @@ func (p *Proxy) decodeCodeActionRequest(env Envelope) (pendingCodeActionRequest,
435
467
  TextDocument struct {
436
468
  URI string `json:"uri"`
437
469
  } `json:"textDocument"`
438
- Range LSPRange `json:"range"`
470
+ Range *codeActionRangeWire `json:"range"`
439
471
  Context LSPCodeActionContext `json:"context"`
440
472
  }
441
473
  if err := json.Unmarshal(env.Params, &params); err != nil {
442
474
  return pendingCodeActionRequest{}, false
443
475
  }
476
+ if params.TextDocument.URI == "" || params.Range == nil ||
477
+ params.Range.Start == nil || params.Range.End == nil ||
478
+ params.Range.Start.Line == nil || params.Range.Start.Character == nil ||
479
+ params.Range.End.Line == nil || params.Range.End.Character == nil {
480
+ return pendingCodeActionRequest{}, false
481
+ }
482
+ rng := LSPRange{
483
+ Start: LSPPosition{
484
+ Line: *params.Range.Start.Line,
485
+ Character: *params.Range.Start.Character,
486
+ },
487
+ End: LSPPosition{
488
+ Line: *params.Range.End.Line,
489
+ Character: *params.Range.End.Character,
490
+ },
491
+ }
492
+ if rng.Start.Line < 0 || rng.Start.Character < 0 ||
493
+ rng.End.Line < 0 || rng.End.Character < 0 ||
494
+ rng.Start.Line > rng.End.Line ||
495
+ (rng.Start.Line == rng.End.Line && rng.Start.Character > rng.End.Character) {
496
+ return pendingCodeActionRequest{}, false
497
+ }
444
498
  return pendingCodeActionRequest{
445
499
  uri: params.TextDocument.URI,
446
- rng: params.Range,
500
+ rng: rng,
447
501
  ctx: params.Context,
448
502
  }, true
449
503
  }
@@ -928,9 +982,9 @@ func (p *Proxy) prepareUpstreamPublishDiagnostics(env Envelope) func() {
928
982
  return nil
929
983
  }
930
984
  p.rememberUpstreamDiagnostics(params.URI, params.Version, params.Diagnostics)
931
- generation := p.nextPluginDiagnosticsGeneration(params.URI)
985
+ generation, projectGeneration := p.nextDiagnosticsGenerations(params.URI)
932
986
  return func() {
933
- p.publishMergedPluginDiagnostics(params.URI, params.Version, true, generation)
987
+ p.publishMergedPluginDiagnostics(params.URI, params.Version, true, generation, projectGeneration)
934
988
  }
935
989
  }
936
990
 
@@ -945,8 +999,8 @@ func (p *Proxy) publishPluginDiagnosticsForDocumentNotification(env Envelope) {
945
999
  return
946
1000
  }
947
1001
  p.markDocumentClean(params.TextDocument.URI)
948
- generation := p.nextPluginDiagnosticsGeneration(params.TextDocument.URI)
949
- go p.publishMergedPluginDiagnostics(params.TextDocument.URI, params.TextDocument.Version, false, generation)
1002
+ generation, projectGeneration := p.nextDiagnosticsGenerations(params.TextDocument.URI)
1003
+ go p.publishMergedPluginDiagnostics(params.TextDocument.URI, params.TextDocument.Version, false, generation, projectGeneration)
950
1004
  }
951
1005
 
952
1006
  func (p *Proxy) publishPluginDiagnosticsForDidOpen(env Envelope) error {
@@ -967,12 +1021,12 @@ func (p *Proxy) publishPluginDiagnosticsForDidOpen(env Envelope) error {
967
1021
  return nil
968
1022
  }
969
1023
  p.markDocumentClean(params.TextDocument.URI)
970
- generation := p.nextPluginDiagnosticsGeneration(params.TextDocument.URI)
971
- go p.publishMergedPluginDiagnostics(params.TextDocument.URI, params.TextDocument.Version, false, generation)
1024
+ generation, projectGeneration := p.nextDiagnosticsGenerations(params.TextDocument.URI)
1025
+ go p.publishMergedPluginDiagnostics(params.TextDocument.URI, params.TextDocument.Version, false, generation, projectGeneration)
972
1026
  return nil
973
1027
  }
974
1028
 
975
- func (p *Proxy) publishMergedPluginDiagnostics(uri string, version *int, adoptCachedVersion bool, generation uint64) {
1029
+ func (p *Proxy) publishMergedPluginDiagnostics(uri string, version *int, adoptCachedVersion bool, generation uint64, projectGeneration uint64) {
976
1030
  if !p.isLatestPluginDiagnosticsGeneration(uri, generation) || p.isDocumentDirty(uri) {
977
1031
  return
978
1032
  }
@@ -980,11 +1034,13 @@ func (p *Proxy) publishMergedPluginDiagnostics(uri string, version *int, adoptCa
980
1034
  URI: uri,
981
1035
  Version: version,
982
1036
  })
983
- version, merged, ok := p.prepareMergedPluginDiagnostics(uri, version, adoptCachedVersion, generation, diagnostics)
984
- if !ok {
985
- return
1037
+ version, merged, ok := p.prepareMergedPluginDiagnostics(uri, version, adoptCachedVersion, generation, diagnostics.Document)
1038
+ if ok {
1039
+ p.reportAsyncError(p.writePublishDiagnosticsIfCurrent(uri, version, merged, generation))
1040
+ }
1041
+ if diagnostics.Project != nil {
1042
+ p.reportAsyncError(p.writeProjectDiagnosticsIfCurrent(diagnostics.Project, projectGeneration))
986
1043
  }
987
- p.reportAsyncError(p.writePublishDiagnosticsIfCurrent(uri, version, merged, generation))
988
1044
  }
989
1045
 
990
1046
  func (p *Proxy) writePublishDiagnostics(uri string, version *int, diagnostics []json.RawMessage) error {
@@ -1005,6 +1061,71 @@ func (p *Proxy) writePublishDiagnosticsIfCurrent(uri string, version *int, diagn
1005
1061
  return WriteFrame(p.editorOut, body)
1006
1062
  }
1007
1063
 
1064
+ func (p *Proxy) writeProjectDiagnosticsIfCurrent(publication *LSPProjectDiagnostics, generation uint64) error {
1065
+ if publication == nil || publication.URI == "" {
1066
+ return nil
1067
+ }
1068
+ diagnostics := append([]LSPDiagnostic(nil), publication.Diagnostics...)
1069
+ for index := range diagnostics {
1070
+ diagnostics[index].Range = LSPRange{}
1071
+ }
1072
+ rawDiagnostics := marshalLSPDiagnostics(diagnostics)
1073
+ p.writeMu.Lock()
1074
+ defer p.writeMu.Unlock()
1075
+ p.diagnosticsMu.Lock()
1076
+ defer p.diagnosticsMu.Unlock()
1077
+ if p.projectDiagnosticGeneration != generation {
1078
+ return nil
1079
+ }
1080
+
1081
+ previousURI := p.projectDiagnosticsURI
1082
+ previousHadDiagnostics := len(p.projectDiagnostics.diagnostics) > 0
1083
+ changedURI := previousURI != "" && previousURI != publication.URI
1084
+ if changedURI {
1085
+ p.projectDiagnosticsURI = ""
1086
+ p.projectDiagnostics = cachedDiagnostics{}
1087
+ if err := WriteFrame(
1088
+ p.editorOut,
1089
+ p.publishDiagnosticsBody(previousURI, nil, p.mergedDiagnosticsLocked(previousURI)),
1090
+ ); err != nil {
1091
+ return err
1092
+ }
1093
+ }
1094
+
1095
+ p.projectDiagnosticsURI = publication.URI
1096
+ p.projectDiagnostics = cachedDiagnostics{diagnostics: copyRawDiagnostics(rawDiagnostics)}
1097
+ if len(rawDiagnostics) == 0 && !previousHadDiagnostics && !changedURI {
1098
+ return nil
1099
+ }
1100
+ return WriteFrame(
1101
+ p.editorOut,
1102
+ p.publishDiagnosticsBody(publication.URI, nil, p.mergedDiagnosticsLocked(publication.URI)),
1103
+ )
1104
+ }
1105
+
1106
+ func marshalLSPDiagnostics(diagnostics []LSPDiagnostic) []json.RawMessage {
1107
+ rawDiagnostics := make([]json.RawMessage, 0, len(diagnostics))
1108
+ for _, diagnostic := range diagnostics {
1109
+ raw, _ := json.Marshal(diagnostic)
1110
+ rawDiagnostics = append(rawDiagnostics, raw)
1111
+ }
1112
+ return rawDiagnostics
1113
+ }
1114
+
1115
+ func (p *Proxy) mergedDiagnosticsLocked(uri string) []json.RawMessage {
1116
+ upstream := p.upstreamDiagnostics[uri].diagnostics
1117
+ document := p.pluginDiagnostics[uri].diagnostics
1118
+ project := []json.RawMessage(nil)
1119
+ if p.projectDiagnosticsURI == uri {
1120
+ project = p.projectDiagnostics.diagnostics
1121
+ }
1122
+ merged := make([]json.RawMessage, 0, len(upstream)+len(document)+len(project))
1123
+ merged = append(merged, copyRawDiagnostics(upstream)...)
1124
+ merged = append(merged, copyRawDiagnostics(document)...)
1125
+ merged = append(merged, copyRawDiagnostics(project)...)
1126
+ return merged
1127
+ }
1128
+
1008
1129
  func (p *Proxy) writeLocalCodeActionsResultIfCurrent(id json.RawMessage, actions []LSPCodeAction, pending pendingCodeActionRequest) error {
1009
1130
  p.writeMu.Lock()
1010
1131
  defer p.writeMu.Unlock()
@@ -1288,6 +1409,7 @@ func (p *Proxy) markDocumentDirtyURI(uri string, version *int) bool {
1288
1409
  delete(p.pluginDiagnostics, uri)
1289
1410
  delete(p.upstreamDiagnostics, uri)
1290
1411
  p.diagnosticGeneration[uri]++
1412
+ p.projectDiagnosticGeneration++
1291
1413
  p.documentGeneration[uri]++
1292
1414
  return len(previous.diagnostics) > 0
1293
1415
  }
@@ -1338,7 +1460,14 @@ func documentTextMatchesDisk(uri string, text string) bool {
1338
1460
  return false
1339
1461
  }
1340
1462
  disk, err := os.ReadFile(file)
1341
- return err == nil && string(disk) == text
1463
+ if err != nil {
1464
+ return false
1465
+ }
1466
+ // Editors commonly strip a leading UTF-8 BOM from the buffer text they send
1467
+ // while it stays on disk (or add one the disk lacks). A raw byte compare would
1468
+ // then misclassify an unedited file as dirty and suppress plugin diagnostics
1469
+ // until the first save, so a single leading BOM is dropped from both sides.
1470
+ return strings.TrimPrefix(string(disk), utf8BOM) == strings.TrimPrefix(text, utf8BOM)
1342
1471
  }
1343
1472
 
1344
1473
  func filePathFromURI(raw string) (string, bool) {
@@ -1363,12 +1492,13 @@ func filePathFromURI(raw string) (string, bool) {
1363
1492
  return abs, true
1364
1493
  }
1365
1494
 
1366
- func (p *Proxy) nextPluginDiagnosticsGeneration(uri string) uint64 {
1495
+ func (p *Proxy) nextDiagnosticsGenerations(uri string) (uint64, uint64) {
1367
1496
  p.diagnosticsMu.Lock()
1368
1497
  defer p.diagnosticsMu.Unlock()
1369
1498
  next := p.diagnosticGeneration[uri] + 1
1370
1499
  p.diagnosticGeneration[uri] = next
1371
- return next
1500
+ p.projectDiagnosticGeneration++
1501
+ return next, p.projectDiagnosticGeneration
1372
1502
  }
1373
1503
 
1374
1504
  func (p *Proxy) isLatestPluginDiagnosticsGeneration(uri string, generation uint64) bool {
@@ -1401,11 +1531,7 @@ func (p *Proxy) prepareMergedPluginDiagnostics(
1401
1531
  if uri == "" {
1402
1532
  return nil, nil, false
1403
1533
  }
1404
- rawDiagnostics := make([]json.RawMessage, 0, len(diagnostics))
1405
- for _, diagnostic := range diagnostics {
1406
- raw, _ := json.Marshal(diagnostic)
1407
- rawDiagnostics = append(rawDiagnostics, raw)
1408
- }
1534
+ rawDiagnostics := marshalLSPDiagnostics(diagnostics)
1409
1535
  inputVersion := copyIntPtr(version)
1410
1536
 
1411
1537
  p.diagnosticsMu.Lock()
@@ -1433,10 +1559,7 @@ func (p *Proxy) prepareMergedPluginDiagnostics(
1433
1559
  if len(rawDiagnostics) == 0 && !previousPluginDiagnostics {
1434
1560
  return nil, nil, false
1435
1561
  }
1436
- cachedDiagnostics := copyRawDiagnostics(cached.diagnostics)
1437
- merged := make([]json.RawMessage, 0, len(cachedDiagnostics)+len(rawDiagnostics))
1438
- merged = append(merged, cachedDiagnostics...)
1439
- merged = append(merged, rawDiagnostics...)
1562
+ merged := p.mergedDiagnosticsLocked(uri)
1440
1563
  return copyIntPtr(version), merged, true
1441
1564
  }
1442
1565
 
@@ -1483,6 +1606,11 @@ func (p *Proxy) augmentInitializeResult(env Envelope) ([]byte, bool) {
1483
1606
  }
1484
1607
  codeActionProvider := caps["codeActionProvider"]
1485
1608
  p.setUpstreamCodeActionProvider(codeActionProvider)
1609
+ // Record whether upstream tsgo answers documentSymbol/references itself. The
1610
+ // per-method handlers forward to tsgo when it does, so this must be set before
1611
+ // the shouldAnswer* checks below read it.
1612
+ p.setUpstreamDocumentSymbolProvider(caps["documentSymbolProvider"])
1613
+ p.setUpstreamReferencesProvider(caps["referencesProvider"])
1486
1614
  codeActionKinds := p.pluginCodeActionKinds()
1487
1615
  changed := false
1488
1616
  if (len(sourceCommands) > 0 || len(codeActionKinds) > 0) && codeActionProvider == nil {
@@ -1516,20 +1644,19 @@ func (p *Proxy) augmentInitializeResult(env Envelope) ([]byte, bool) {
1516
1644
  changed = true
1517
1645
  }
1518
1646
  }
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
- }
1647
+ // Advertise documentSymbol/references only when the proxy will actually answer
1648
+ // them locally — upstream tsgo did not advertise the capability (fallback) or
1649
+ // ForceLocalSymbolProvider is set. When tsgo advertises and the flag is off,
1650
+ // the method is forwarded to tsgo, whose own capability already tells the
1651
+ // editor to send the request, so its (possibly richer) capability is left
1652
+ // untouched here.
1653
+ if p.shouldAnswerDocumentSymbolLocally() && !capabilityAdvertised(caps["documentSymbolProvider"]) {
1654
+ caps["documentSymbolProvider"] = true
1655
+ changed = true
1656
+ }
1657
+ if p.shouldAnswerReferencesLocally() && !capabilityAdvertised(caps["referencesProvider"]) {
1658
+ caps["referencesProvider"] = true
1659
+ changed = true
1533
1660
  }
1534
1661
  if !changed {
1535
1662
  return nil, false
@@ -1635,6 +1762,71 @@ func (p *Proxy) setUpstreamCodeActionProvider(value any) {
1635
1762
  p.upstreamCodeActionProvider = provides
1636
1763
  }
1637
1764
 
1765
+ func (p *Proxy) setUpstreamDocumentSymbolProvider(value any) {
1766
+ p.capabilityMu.Lock()
1767
+ defer p.capabilityMu.Unlock()
1768
+ p.upstreamDocumentSymbolProvider = capabilityAdvertised(value)
1769
+ }
1770
+
1771
+ func (p *Proxy) setUpstreamReferencesProvider(value any) {
1772
+ p.capabilityMu.Lock()
1773
+ defer p.capabilityMu.Unlock()
1774
+ p.upstreamReferencesProvider = capabilityAdvertised(value)
1775
+ }
1776
+
1777
+ // shouldAnswerDocumentSymbolLocally reports whether the proxy answers
1778
+ // textDocument/documentSymbol from the local SymbolProvider instead of
1779
+ // forwarding to upstream tsgo: a provider must be wired, and either the caller
1780
+ // forced it or tsgo did not advertise the capability.
1781
+ func (p *Proxy) shouldAnswerDocumentSymbolLocally() bool {
1782
+ if p.symbolProvider == nil {
1783
+ return false
1784
+ }
1785
+ if p.forceLocalSymbolProvider {
1786
+ return true
1787
+ }
1788
+ p.capabilityMu.Lock()
1789
+ defer p.capabilityMu.Unlock()
1790
+ return !p.upstreamDocumentSymbolProvider
1791
+ }
1792
+
1793
+ // shouldAnswerReferencesLocally mirrors shouldAnswerDocumentSymbolLocally for
1794
+ // textDocument/references.
1795
+ func (p *Proxy) shouldAnswerReferencesLocally() bool {
1796
+ if p.symbolProvider == nil {
1797
+ return false
1798
+ }
1799
+ if p.forceLocalSymbolProvider {
1800
+ return true
1801
+ }
1802
+ p.capabilityMu.Lock()
1803
+ defer p.capabilityMu.Unlock()
1804
+ return !p.upstreamReferencesProvider
1805
+ }
1806
+
1807
+ // invalidateSymbolProvider drops the SymbolProvider's cached compiler state so
1808
+ // the next documentSymbol/references request reflects the latest sources. It is
1809
+ // a no-op when no provider is wired.
1810
+ func (p *Proxy) invalidateSymbolProvider() {
1811
+ if p.symbolProvider != nil {
1812
+ p.symbolProvider.Invalidate()
1813
+ }
1814
+ }
1815
+
1816
+ // capabilityAdvertised reports whether an LSP server-capability value means the
1817
+ // server implements the capability: boolean true, or an options object. Absent
1818
+ // (nil), false, or any other shape counts as unsupported.
1819
+ func capabilityAdvertised(value any) bool {
1820
+ switch v := value.(type) {
1821
+ case bool:
1822
+ return v
1823
+ case map[string]any:
1824
+ return v != nil
1825
+ default:
1826
+ return false
1827
+ }
1828
+ }
1829
+
1638
1830
  func mergeCommandIDs(existing any, additions []string) []string {
1639
1831
  seen := map[string]struct{}{}
1640
1832
  out := make([]string, 0, len(additions))