ttsc 0.12.3 → 0.13.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 (151) hide show
  1. package/cmd/platform/main.go +9 -0
  2. package/cmd/ttsc/api_compile.go +122 -101
  3. package/cmd/ttsc/api_compile_branches_test.go +55 -57
  4. package/cmd/ttsc/api_transform.go +67 -49
  5. package/cmd/ttsc/api_transform_branches_test.go +30 -32
  6. package/cmd/ttsc/build.go +141 -108
  7. package/cmd/ttsc/build_branches_test.go +95 -97
  8. package/cmd/ttsc/filter.go +70 -0
  9. package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
  10. package/cmd/ttsc/flags_gen.go +30 -0
  11. package/cmd/ttsc/helpers_test.go +39 -28
  12. package/cmd/ttsc/main.go +111 -104
  13. package/cmd/ttsc/main_alias_branches_test.go +21 -23
  14. package/cmd/ttscserver/main.go +96 -90
  15. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
  16. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
  17. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
  18. package/cmd/utility-host/main.go +30 -23
  19. package/driver/lsp.go +53 -7
  20. package/driver/plugins.go +88 -72
  21. package/driver/program.go +464 -340
  22. package/driver/rewrite.go +477 -380
  23. package/internal/lspserver/lsp_envelope.go +64 -64
  24. package/internal/lspserver/lsp_frame.go +60 -60
  25. package/internal/lspserver/lsp_plugin.go +57 -57
  26. package/internal/lspserver/lsp_proxy.go +265 -251
  27. package/internal/lspserver/lsp_server.go +155 -144
  28. package/lib/TtscCompiler.d.ts +7 -0
  29. package/lib/TtscCompiler.js +7 -0
  30. package/lib/TtscCompiler.js.map +1 -1
  31. package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
  32. package/lib/compiler/internal/buildNativeCompiler.js +23 -3
  33. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  34. package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
  35. package/lib/compiler/internal/compileProjectInMemory.js +36 -21
  36. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  37. package/lib/compiler/internal/paths.d.ts +27 -0
  38. package/lib/compiler/internal/paths.js +70 -0
  39. package/lib/compiler/internal/paths.js.map +1 -0
  40. package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
  41. package/lib/compiler/internal/project/readProjectConfig.js +80 -1
  42. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
  44. package/lib/compiler/internal/project/resolveProjectConfig.js +42 -1
  45. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  46. package/lib/compiler/internal/resolveBinary.d.ts +9 -1
  47. package/lib/compiler/internal/resolveBinary.js +17 -1
  48. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  49. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
  50. package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
  51. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  52. package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
  53. package/lib/compiler/internal/resolveTsgo.js +23 -1
  54. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  55. package/lib/compiler/internal/runBuild.d.ts +24 -4
  56. package/lib/compiler/internal/runBuild.js +355 -93
  57. package/lib/compiler/internal/runBuild.js.map +1 -1
  58. package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
  59. package/lib/compiler/internal/runSingleFileEmit.js +14 -1
  60. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  61. package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
  62. package/lib/compiler/internal/sharedHostHelpers.js +6 -2
  63. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  64. package/lib/compiler/internal/spawnNative.d.ts +21 -0
  65. package/lib/compiler/internal/spawnNative.js +59 -0
  66. package/lib/compiler/internal/spawnNative.js.map +1 -0
  67. package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
  68. package/lib/compiler/internal/transformProjectInMemory.js +57 -61
  69. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  70. package/lib/flags/parser.d.ts +63 -0
  71. package/lib/flags/parser.js +254 -0
  72. package/lib/flags/parser.js.map +1 -0
  73. package/lib/flags/schema.d.ts +147 -0
  74. package/lib/flags/schema.js +405 -0
  75. package/lib/flags/schema.js.map +1 -0
  76. package/lib/launcher/internal/getCompilerVersionText.js +7 -0
  77. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  78. package/lib/launcher/internal/prepareExecution.js +35 -0
  79. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  80. package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
  81. package/lib/launcher/internal/resolveCacheDir.js +22 -0
  82. package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
  83. package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
  84. package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
  85. package/lib/launcher/internal/runTtsc.d.ts +10 -0
  86. package/lib/launcher/internal/runTtsc.js +90 -153
  87. package/lib/launcher/internal/runTtsc.js.map +1 -1
  88. package/lib/launcher/internal/runTtscserver.js +10 -35
  89. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  90. package/lib/launcher/internal/runTtsx.d.ts +8 -0
  91. package/lib/launcher/internal/runTtsx.js +137 -79
  92. package/lib/launcher/internal/runTtsx.js.map +1 -1
  93. package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
  94. package/lib/plugin/internal/buildSourcePlugin.js +40 -17
  95. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  96. package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
  97. package/lib/plugin/internal/loadProjectPlugins.js +90 -41
  98. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  99. package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
  100. package/lib/structures/ITtscPlugin.d.ts +24 -0
  101. package/lib/structures/ITtscPluginCapabilities.d.ts +29 -0
  102. package/lib/structures/ITtscPluginCapabilities.js +3 -0
  103. package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
  104. package/lib/structures/index.d.ts +1 -0
  105. package/lib/structures/index.js +1 -0
  106. package/lib/structures/index.js.map +1 -1
  107. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  108. package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
  109. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
  110. package/package.json +9 -9
  111. package/shim/ast/shim.go +24 -0
  112. package/shim/checker/shim.go +35 -0
  113. package/shim/core/shim.go +17 -0
  114. package/shim/diagnosticwriter/shim.go +4 -0
  115. package/shim/lsp/shim.go +3 -3
  116. package/shim/printer/shim.go +20 -0
  117. package/src/TtscCompiler.ts +7 -0
  118. package/src/compiler/internal/buildNativeCompiler.ts +23 -3
  119. package/src/compiler/internal/compileProjectInMemory.ts +34 -25
  120. package/src/compiler/internal/paths.ts +68 -0
  121. package/src/compiler/internal/project/readProjectConfig.ts +86 -1
  122. package/src/compiler/internal/project/resolveProjectConfig.ts +44 -1
  123. package/src/compiler/internal/resolveBinary.ts +17 -1
  124. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
  125. package/src/compiler/internal/resolveTsgo.ts +29 -1
  126. package/src/compiler/internal/runBuild.ts +398 -112
  127. package/src/compiler/internal/runSingleFileEmit.ts +14 -1
  128. package/src/compiler/internal/sharedHostHelpers.ts +7 -7
  129. package/src/compiler/internal/spawnNative.ts +64 -0
  130. package/src/compiler/internal/transformProjectInMemory.ts +47 -67
  131. package/src/flags/parser.ts +367 -0
  132. package/src/flags/schema.ts +570 -0
  133. package/src/launcher/internal/getCompilerVersionText.ts +7 -0
  134. package/src/launcher/internal/prepareExecution.ts +35 -0
  135. package/src/launcher/internal/resolveCacheDir.ts +19 -0
  136. package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
  137. package/src/launcher/internal/runTtsc.ts +97 -143
  138. package/src/launcher/internal/runTtscserver.ts +9 -1
  139. package/src/launcher/internal/runTtsx.ts +149 -82
  140. package/src/plugin/internal/buildSourcePlugin.ts +40 -14
  141. package/src/plugin/internal/loadProjectPlugins.ts +116 -45
  142. package/src/structures/ITtscCompilerTransformation.ts +15 -2
  143. package/src/structures/ITtscPlugin.ts +26 -1
  144. package/src/structures/ITtscPluginCapabilities.ts +29 -0
  145. package/src/structures/index.ts +1 -0
  146. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  147. package/src/structures/internal/TtscCommonOptions.ts +19 -0
  148. package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
  149. package/tools/gen_shims/main.go +43 -8
  150. package/utility/flags_gen.go +30 -0
  151. package/utility/host.go +304 -245
@@ -1,13 +1,13 @@
1
1
  package lspserver
2
2
 
3
3
  import (
4
- "bytes"
5
- "context"
6
- "encoding/json"
7
- "errors"
8
- "fmt"
9
- "io"
10
- "sync"
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "sync"
11
11
  )
12
12
 
13
13
  // ErrCommandNotHandled is returned by PluginSource.ExecuteCommand for
@@ -15,60 +15,60 @@ import (
15
15
  var ErrCommandNotHandled = errors.New("lsp: command not handled by ttsc")
16
16
 
17
17
  const (
18
- methodPublishDiagnostics = "textDocument/publishDiagnostics"
19
- methodCodeAction = "textDocument/codeAction"
20
- methodExecuteCommand = "workspace/executeCommand"
21
- methodCancelRequest = "$/cancelRequest"
18
+ methodPublishDiagnostics = "textDocument/publishDiagnostics"
19
+ methodCodeAction = "textDocument/codeAction"
20
+ methodExecuteCommand = "workspace/executeCommand"
21
+ methodCancelRequest = "$/cancelRequest"
22
22
  )
23
23
 
24
24
  // ProxyOptions wires the byte-level proxy together. ttscserver creates
25
25
  // the upstream pipes around `tsgo --lsp --stdio` and hands the proxy
26
26
  // editor stdio plus those pipe ends.
27
27
  type ProxyOptions struct {
28
- EditorIn io.Reader
29
- EditorOut io.Writer
30
- UpstreamIn io.Writer // we write here; the tsgo LSP process reads
31
- UpstreamOut io.Reader // the tsgo LSP process writes here; we read
32
- Source PluginSource
28
+ EditorIn io.Reader
29
+ EditorOut io.Writer
30
+ UpstreamIn io.Writer // we write here; the tsgo LSP process reads
31
+ UpstreamOut io.Reader // the tsgo LSP process writes here; we read
32
+ Source PluginSource
33
33
  }
34
34
 
35
35
  // Proxy bridges the editor and an upstream tsgo LSP process, intercepting
36
36
  // the message types ttsc cares about (publishDiagnostics merge, code
37
37
  // action augmentation, executeCommand for ttsc-owned commands).
38
38
  type Proxy struct {
39
- editorIn io.Reader
40
- editorOut io.Writer
41
- upstreamIn io.Writer
42
- upstreamOut io.Reader
43
- source PluginSource
39
+ editorIn io.Reader
40
+ editorOut io.Writer
41
+ upstreamIn io.Writer
42
+ upstreamOut io.Reader
43
+ source PluginSource
44
44
 
45
- writeMu sync.Mutex // serializes WriteFrame calls to editorOut
45
+ writeMu sync.Mutex // serializes WriteFrame calls to editorOut
46
46
 
47
- pendingMu sync.Mutex
48
- pendingActions map[string]pendingCodeActionRequest
47
+ pendingMu sync.Mutex
48
+ pendingActions map[string]pendingCodeActionRequest
49
49
  }
50
50
 
51
51
  type pendingCodeActionRequest struct {
52
- uri string
53
- rng LSPRange
54
- ctx LSPCodeActionContext
52
+ uri string
53
+ rng LSPRange
54
+ ctx LSPCodeActionContext
55
55
  }
56
56
 
57
57
  // NewProxy returns a Proxy ready to Run. The PluginSource is required;
58
58
  // pass NullPluginSource{} for a no-contribution setup.
59
59
  func NewProxy(opts ProxyOptions) *Proxy {
60
- source := opts.Source
61
- if source == nil {
62
- source = NullPluginSource{}
63
- }
64
- return &Proxy{
65
- editorIn: opts.EditorIn,
66
- editorOut: opts.EditorOut,
67
- upstreamIn: opts.UpstreamIn,
68
- upstreamOut: opts.UpstreamOut,
69
- source: source,
70
- pendingActions: make(map[string]pendingCodeActionRequest),
71
- }
60
+ source := opts.Source
61
+ if source == nil {
62
+ source = NullPluginSource{}
63
+ }
64
+ return &Proxy{
65
+ editorIn: opts.EditorIn,
66
+ editorOut: opts.EditorOut,
67
+ upstreamIn: opts.UpstreamIn,
68
+ upstreamOut: opts.UpstreamOut,
69
+ source: source,
70
+ pendingActions: make(map[string]pendingCodeActionRequest),
71
+ }
72
72
  }
73
73
 
74
74
  // Run drives both pump goroutines until they return. Pumps return when
@@ -77,18 +77,18 @@ func NewProxy(opts ProxyOptions) *Proxy {
77
77
  // pipe write fails. ErrFrameClosed and context.Canceled are folded into
78
78
  // a nil result so editor shutdown does not look like a crash.
79
79
  func (p *Proxy) Run(ctx context.Context) error {
80
- errCh := make(chan error, 2)
81
- go func() { errCh <- p.pumpEditorToUpstream(ctx) }()
82
- go func() { errCh <- p.pumpUpstreamToEditor(ctx) }()
80
+ errCh := make(chan error, 2)
81
+ go func() { errCh <- p.pumpEditorToUpstream(ctx) }()
82
+ go func() { errCh <- p.pumpUpstreamToEditor(ctx) }()
83
83
 
84
- var first error
85
- for i := 0; i < 2; i++ {
86
- err := <-errCh
87
- if first == nil && err != nil && !errors.Is(err, ErrFrameClosed) {
88
- first = err
89
- }
90
- }
91
- return first
84
+ var first error
85
+ for i := 0; i < 2; i++ {
86
+ err := <-errCh
87
+ if first == nil && err != nil && !errors.Is(err, ErrFrameClosed) {
88
+ first = err
89
+ }
90
+ }
91
+ return first
92
92
  }
93
93
 
94
94
  // pumpEditorToUpstream reads frames from the editor, decides whether to
@@ -98,33 +98,33 @@ func (p *Proxy) Run(ctx context.Context) error {
98
98
  // writer so tsgo's server-side Read returns EOF and its Run loop drains;
99
99
  // without that nudge tsgo would wait forever for more input.
100
100
  func (p *Proxy) pumpEditorToUpstream(_ context.Context) error {
101
- fr := NewFrameReader(p.editorIn)
102
- for {
103
- _, body, err := fr.Read()
104
- if err != nil {
105
- if errors.Is(err, ErrFrameClosed) {
106
- p.closeUpstreamInput()
107
- }
108
- return err
109
- }
110
- env, parseErr := ParseEnvelope(body)
111
- if parseErr != nil {
112
- if forwardErr := WriteFrame(p.upstreamIn, body); forwardErr != nil {
113
- return forwardErr
114
- }
115
- continue
116
- }
117
- handled, handleErr := p.handleEditorEnvelope(env)
118
- if handleErr != nil {
119
- return handleErr
120
- }
121
- if handled {
122
- continue
123
- }
124
- if err := WriteFrame(p.upstreamIn, body); err != nil {
125
- return err
126
- }
127
- }
101
+ fr := NewFrameReader(p.editorIn)
102
+ for {
103
+ _, body, err := fr.Read()
104
+ if err != nil {
105
+ if errors.Is(err, ErrFrameClosed) {
106
+ p.closeUpstreamInput()
107
+ }
108
+ return err
109
+ }
110
+ env, parseErr := ParseEnvelope(body)
111
+ if parseErr != nil {
112
+ if forwardErr := WriteFrame(p.upstreamIn, body); forwardErr != nil {
113
+ return forwardErr
114
+ }
115
+ continue
116
+ }
117
+ handled, handleErr := p.handleEditorEnvelope(env)
118
+ if handleErr != nil {
119
+ return handleErr
120
+ }
121
+ if handled {
122
+ continue
123
+ }
124
+ if err := WriteFrame(p.upstreamIn, body); err != nil {
125
+ return err
126
+ }
127
+ }
128
128
  }
129
129
 
130
130
  // closeUpstreamInput closes the upstream writer (if it is also an
@@ -132,31 +132,31 @@ func (p *Proxy) pumpEditorToUpstream(_ context.Context) error {
132
132
  // We type-assert because the public ProxyOptions surface promises only
133
133
  // io.Writer; RunLSPServer always passes an *io.PipeWriter in practice.
134
134
  func (p *Proxy) closeUpstreamInput() {
135
- if closer, ok := p.upstreamIn.(io.Closer); ok {
136
- _ = closer.Close()
137
- }
135
+ if closer, ok := p.upstreamIn.(io.Closer); ok {
136
+ _ = closer.Close()
137
+ }
138
138
  }
139
139
 
140
140
  // handleEditorEnvelope returns true if the envelope was fully processed
141
141
  // locally (responded to without forwarding upstream).
142
142
  func (p *Proxy) handleEditorEnvelope(env Envelope) (bool, error) {
143
- switch env.Method {
144
- case methodExecuteCommand:
145
- if env.IsRequest() {
146
- return p.tryExecuteCommand(env)
147
- }
148
- case methodCodeAction:
149
- if env.IsRequest() {
150
- p.rememberCodeActionRequest(env)
151
- }
152
- case methodCancelRequest:
153
- // $/cancelRequest names an in-flight id the editor has given up on.
154
- // The proxy drops any pending codeAction entry for that id so the
155
- // map cannot grow unbounded across long editor sessions, then lets
156
- // the notification continue to upstream.
157
- p.forgetCancelledRequest(env)
158
- }
159
- return false, nil
143
+ switch env.Method {
144
+ case methodExecuteCommand:
145
+ if env.IsRequest() {
146
+ return p.tryExecuteCommand(env)
147
+ }
148
+ case methodCodeAction:
149
+ if env.IsRequest() {
150
+ p.rememberCodeActionRequest(env)
151
+ }
152
+ case methodCancelRequest:
153
+ // $/cancelRequest names an in-flight id the editor has given up on.
154
+ // The proxy drops any pending codeAction entry for that id so the
155
+ // map cannot grow unbounded across long editor sessions, then lets
156
+ // the notification continue to upstream.
157
+ p.forgetCancelledRequest(env)
158
+ }
159
+ return false, nil
160
160
  }
161
161
 
162
162
  // forgetCancelledRequest removes any pending codeAction entry whose id
@@ -165,42 +165,42 @@ func (p *Proxy) handleEditorEnvelope(env Envelope) (bool, error) {
165
165
  // through the shared normalizer so a cancel for `1.0` deletes an entry
166
166
  // stored under `1` (and vice versa for string-vs-numeric encodings).
167
167
  func (p *Proxy) forgetCancelledRequest(env Envelope) {
168
- var params struct {
169
- ID json.RawMessage `json:"id"`
170
- }
171
- if err := json.Unmarshal(env.Params, &params); err != nil {
172
- return
173
- }
174
- key := idKeyFromRaw(params.ID)
175
- if key == "" {
176
- return
177
- }
178
- p.pendingMu.Lock()
179
- defer p.pendingMu.Unlock()
180
- delete(p.pendingActions, key)
168
+ var params struct {
169
+ ID json.RawMessage `json:"id"`
170
+ }
171
+ if err := json.Unmarshal(env.Params, &params); err != nil {
172
+ return
173
+ }
174
+ key := idKeyFromRaw(params.ID)
175
+ if key == "" {
176
+ return
177
+ }
178
+ p.pendingMu.Lock()
179
+ defer p.pendingMu.Unlock()
180
+ delete(p.pendingActions, key)
181
181
  }
182
182
 
183
183
  // rememberCodeActionRequest stores the request payload so the matching
184
184
  // response from upstream can be augmented with ttsc-owned code actions
185
185
  // for the same range.
186
186
  func (p *Proxy) rememberCodeActionRequest(env Envelope) {
187
- var params struct {
188
- TextDocument struct {
189
- URI string `json:"uri"`
190
- } `json:"textDocument"`
191
- Range LSPRange `json:"range"`
192
- Context LSPCodeActionContext `json:"context"`
193
- }
194
- if err := json.Unmarshal(env.Params, &params); err != nil {
195
- return
196
- }
197
- p.pendingMu.Lock()
198
- defer p.pendingMu.Unlock()
199
- p.pendingActions[env.IDKey()] = pendingCodeActionRequest{
200
- uri: params.TextDocument.URI,
201
- rng: params.Range,
202
- ctx: params.Context,
203
- }
187
+ var params struct {
188
+ TextDocument struct {
189
+ URI string `json:"uri"`
190
+ } `json:"textDocument"`
191
+ Range LSPRange `json:"range"`
192
+ Context LSPCodeActionContext `json:"context"`
193
+ }
194
+ if err := json.Unmarshal(env.Params, &params); err != nil {
195
+ return
196
+ }
197
+ p.pendingMu.Lock()
198
+ defer p.pendingMu.Unlock()
199
+ p.pendingActions[env.IDKey()] = pendingCodeActionRequest{
200
+ uri: params.TextDocument.URI,
201
+ rng: params.Range,
202
+ ctx: params.Context,
203
+ }
204
204
  }
205
205
 
206
206
  // tryExecuteCommand handles workspace/executeCommand requests whose
@@ -208,41 +208,43 @@ func (p *Proxy) rememberCodeActionRequest(env Envelope) {
208
208
  // successful local response; false when the command should fall through
209
209
  // to upstream tsgo.
210
210
  func (p *Proxy) tryExecuteCommand(env Envelope) (bool, error) {
211
- var params struct {
212
- Command string `json:"command"`
213
- Arguments []json.RawMessage `json:"arguments,omitempty"`
214
- }
215
- if err := json.Unmarshal(env.Params, &params); err != nil {
216
- return false, nil
217
- }
218
- if !p.ownsCommand(params.Command) {
219
- return false, nil
220
- }
221
- edit, err := p.source.ExecuteCommand(params.Command, params.Arguments)
222
- if errors.Is(err, ErrCommandNotHandled) {
223
- return false, nil
224
- }
225
- if err != nil {
226
- return true, p.writeError(env.ID, fmt.Sprintf("ttsc command %q failed: %v", params.Command, err))
227
- }
228
- // Cycle 1 returns the WorkspaceEdit inside the executeCommand response
229
- // instead of sending workspace/applyEdit as a server→client request.
230
- // ttsc owns both ends (its VSCode extension), so the extension applies
231
- // the edit on its side. Sticking to one direction avoids tracking our
232
- // own outgoing request ids in the proxy.
233
- if edit == nil {
234
- return true, p.writeResult(env.ID, nil)
235
- }
236
- return true, p.writeResult(env.ID, edit)
211
+ var params struct {
212
+ Command string `json:"command"`
213
+ Arguments []json.RawMessage `json:"arguments,omitempty"`
214
+ }
215
+ if err := json.Unmarshal(env.Params, &params); err != nil {
216
+ return false, nil
217
+ }
218
+ if !p.ownsCommand(params.Command) {
219
+ return false, nil
220
+ }
221
+ edit, err := p.source.ExecuteCommand(params.Command, params.Arguments)
222
+ if errors.Is(err, ErrCommandNotHandled) {
223
+ return false, nil
224
+ }
225
+ if err != nil {
226
+ return true, p.writeError(env.ID, fmt.Sprintf("ttsc command %q failed: %v", params.Command, err))
227
+ }
228
+ // Cycle 1 returns the WorkspaceEdit inside the executeCommand response
229
+ // instead of sending workspace/applyEdit as a server→client request.
230
+ // ttsc owns both ends (its VSCode extension), so the extension applies
231
+ // the edit on its side. Sticking to one direction avoids tracking our
232
+ // own outgoing request ids in the proxy.
233
+ if edit == nil {
234
+ return true, p.writeResult(env.ID, nil)
235
+ }
236
+ return true, p.writeResult(env.ID, edit)
237
237
  }
238
238
 
239
+ // ownsCommand reports whether command is registered with the PluginSource
240
+ // and should be handled locally rather than forwarded to upstream tsgo.
239
241
  func (p *Proxy) ownsCommand(command string) bool {
240
- for _, id := range p.source.CommandIDs() {
241
- if id == command {
242
- return true
243
- }
244
- }
245
- return false
242
+ for _, id := range p.source.CommandIDs() {
243
+ if id == command {
244
+ return true
245
+ }
246
+ }
247
+ return false
246
248
  }
247
249
 
248
250
  // pumpUpstreamToEditor reads frames from the upstream tsgo server,
@@ -251,24 +253,24 @@ func (p *Proxy) ownsCommand(command string) bool {
251
253
  // propagation happens through pipe closure by RunLSPServer's deferred
252
254
  // cleanup goroutines.
253
255
  func (p *Proxy) pumpUpstreamToEditor(_ context.Context) error {
254
- fr := NewFrameReader(p.upstreamOut)
255
- for {
256
- _, body, err := fr.Read()
257
- if err != nil {
258
- return err
259
- }
260
- env, parseErr := ParseEnvelope(body)
261
- if parseErr != nil {
262
- if forwardErr := p.writeEditorFrame(body); forwardErr != nil {
263
- return forwardErr
264
- }
265
- continue
266
- }
267
- augmented := p.augmentUpstream(env, body)
268
- if err := p.writeEditorFrame(augmented); err != nil {
269
- return err
270
- }
271
- }
256
+ fr := NewFrameReader(p.upstreamOut)
257
+ for {
258
+ _, body, err := fr.Read()
259
+ if err != nil {
260
+ return err
261
+ }
262
+ env, parseErr := ParseEnvelope(body)
263
+ if parseErr != nil {
264
+ if forwardErr := p.writeEditorFrame(body); forwardErr != nil {
265
+ return forwardErr
266
+ }
267
+ continue
268
+ }
269
+ augmented := p.augmentUpstream(env, body)
270
+ if err := p.writeEditorFrame(augmented); err != nil {
271
+ return err
272
+ }
273
+ }
272
274
  }
273
275
 
274
276
  // augmentUpstream returns the (possibly rewritten) body to forward. For
@@ -284,25 +286,25 @@ func (p *Proxy) pumpUpstreamToEditor(_ context.Context) error {
284
286
  // semantics. When the cancel wins, the pending entry is gone and
285
287
  // augmentation skips cleanly.
286
288
  func (p *Proxy) augmentUpstream(env Envelope, body []byte) []byte {
287
- if env.IsNotification() && env.Method == methodPublishDiagnostics {
288
- if merged, ok := p.mergePublishDiagnostics(env); ok {
289
- return merged
290
- }
291
- }
292
- if env.IsResponse() {
293
- p.pendingMu.Lock()
294
- pending, ok := p.pendingActions[env.IDKey()]
295
- if ok {
296
- delete(p.pendingActions, env.IDKey())
297
- }
298
- p.pendingMu.Unlock()
299
- if ok {
300
- if augmented, augOk := p.appendCodeActions(env, pending); augOk {
301
- return augmented
302
- }
303
- }
304
- }
305
- return body
289
+ if env.IsNotification() && env.Method == methodPublishDiagnostics {
290
+ if merged, ok := p.mergePublishDiagnostics(env); ok {
291
+ return merged
292
+ }
293
+ }
294
+ if env.IsResponse() {
295
+ p.pendingMu.Lock()
296
+ pending, ok := p.pendingActions[env.IDKey()]
297
+ if ok {
298
+ delete(p.pendingActions, env.IDKey())
299
+ }
300
+ p.pendingMu.Unlock()
301
+ if ok {
302
+ if augmented, augOk := p.appendCodeActions(env, pending); augOk {
303
+ return augmented
304
+ }
305
+ }
306
+ }
307
+ return body
306
308
  }
307
309
 
308
310
  // mergePublishDiagnostics decodes the publishDiagnostics params, asks
@@ -312,25 +314,25 @@ func (p *Proxy) augmentUpstream(env Envelope, body []byte) []byte {
312
314
  // verbatim." Re-encoding our own types is guaranteed by their
313
315
  // definitions in lsp_plugin.go, so the marshal errors are not checked.
314
316
  func (p *Proxy) mergePublishDiagnostics(env Envelope) ([]byte, bool) {
315
- var params struct {
316
- URI string `json:"uri"`
317
- Version *int `json:"version,omitempty"`
318
- Diagnostics []json.RawMessage `json:"diagnostics"`
319
- }
320
- if err := json.Unmarshal(env.Params, &params); err != nil {
321
- return nil, false
322
- }
323
- extras := p.source.Diagnostics(LSPDocumentVersion{URI: params.URI, Version: params.Version})
324
- if len(extras) == 0 {
325
- return nil, false
326
- }
327
- for _, extra := range extras {
328
- raw, _ := json.Marshal(extra)
329
- params.Diagnostics = append(params.Diagnostics, raw)
330
- }
331
- env.Params, _ = json.Marshal(params)
332
- body, _ := json.Marshal(env)
333
- return body, true
317
+ var params struct {
318
+ URI string `json:"uri"`
319
+ Version *int `json:"version,omitempty"`
320
+ Diagnostics []json.RawMessage `json:"diagnostics"`
321
+ }
322
+ if err := json.Unmarshal(env.Params, &params); err != nil {
323
+ return nil, false
324
+ }
325
+ extras := p.source.Diagnostics(LSPDocumentVersion{URI: params.URI, Version: params.Version})
326
+ if len(extras) == 0 {
327
+ return nil, false
328
+ }
329
+ for _, extra := range extras {
330
+ raw, _ := json.Marshal(extra)
331
+ params.Diagnostics = append(params.Diagnostics, raw)
332
+ }
333
+ env.Params, _ = json.Marshal(params)
334
+ body, _ := json.Marshal(env)
335
+ return body, true
334
336
  }
335
337
 
336
338
  // appendCodeActions adds ttsc-owned code actions to a forwarded
@@ -343,49 +345,61 @@ func (p *Proxy) mergePublishDiagnostics(env Envelope) ([]byte, bool) {
343
345
  // shape ttsc cannot splice into, so the proxy forwards verbatim
344
346
  // rather than corrupting it).
345
347
  func (p *Proxy) appendCodeActions(env Envelope, pending pendingCodeActionRequest) ([]byte, bool) {
346
- if env.IsErrorResponse() {
347
- return nil, false
348
- }
349
- trimmed := bytes.TrimSpace(env.Result)
350
- if len(trimmed) > 0 && trimmed[0] != '[' && !bytes.Equal(trimmed, []byte("null")) {
351
- return nil, false
352
- }
353
- actions := p.source.CodeActions(pending.uri, pending.rng, pending.ctx)
354
- if len(actions) == 0 {
355
- return nil, false
356
- }
357
- var existing []json.RawMessage
358
- if len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null")) {
359
- _ = json.Unmarshal(trimmed, &existing)
360
- }
361
- for _, action := range actions {
362
- raw, _ := json.Marshal(action)
363
- existing = append(existing, raw)
364
- }
365
- env.Result, _ = json.Marshal(existing)
366
- body, _ := json.Marshal(env)
367
- return body, true
348
+ if env.IsErrorResponse() {
349
+ return nil, false
350
+ }
351
+ trimmed := bytes.TrimSpace(env.Result)
352
+ if len(trimmed) > 0 && trimmed[0] != '[' && !bytes.Equal(trimmed, []byte("null")) {
353
+ return nil, false
354
+ }
355
+ actions := p.source.CodeActions(pending.uri, pending.rng, pending.ctx)
356
+ if len(actions) == 0 {
357
+ return nil, false
358
+ }
359
+ var existing []json.RawMessage
360
+ if len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null")) {
361
+ _ = json.Unmarshal(trimmed, &existing)
362
+ }
363
+ for _, action := range actions {
364
+ raw, _ := json.Marshal(action)
365
+ existing = append(existing, raw)
366
+ }
367
+ env.Result, _ = json.Marshal(existing)
368
+ body, _ := json.Marshal(env)
369
+ return body, true
368
370
  }
369
371
 
372
+ // writeEditorFrame serializes body under writeMu so concurrent calls from
373
+ // pumpEditorToUpstream (local command responses) and pumpUpstreamToEditor
374
+ // (forwarded upstream frames) do not interleave partial writes.
370
375
  func (p *Proxy) writeEditorFrame(body []byte) error {
371
- p.writeMu.Lock()
372
- defer p.writeMu.Unlock()
373
- return WriteFrame(p.editorOut, body)
376
+ p.writeMu.Lock()
377
+ defer p.writeMu.Unlock()
378
+ return WriteFrame(p.editorOut, body)
374
379
  }
375
380
 
381
+ // writeResult sends a JSON-RPC success response with the given result to
382
+ // the editor. A nil result is marshalled as JSON null.
376
383
  func (p *Proxy) writeResult(id json.RawMessage, result any) error {
377
- rawResult, _ := json.Marshal(result)
378
- env := Envelope{JSONRPC: "2.0", ID: id, Result: rawResult}
379
- body, _ := json.Marshal(env)
380
- return p.writeEditorFrame(body)
384
+ rawResult, _ := json.Marshal(result)
385
+ env := Envelope{JSONRPC: "2.0", ID: id, Result: rawResult}
386
+ body, _ := json.Marshal(env)
387
+ return p.writeEditorFrame(body)
381
388
  }
382
389
 
390
+ // jsonRPCInternalError is the JSON-RPC 2.0 reserved error code for
391
+ // "Internal error" (spec §5.1).
392
+ const jsonRPCInternalError = -32603
393
+
394
+ // writeError sends a JSON-RPC error response to the editor. message is
395
+ // embedded verbatim in the error object; callers are responsible for
396
+ // keeping it concise and safe to surface in editor UI.
383
397
  func (p *Proxy) writeError(id json.RawMessage, message string) error {
384
- errPayload, _ := json.Marshal(struct {
385
- Code int `json:"code"`
386
- Message string `json:"message"`
387
- }{Code: -32603, Message: message})
388
- env := Envelope{JSONRPC: "2.0", ID: id, Error: errPayload}
389
- body, _ := json.Marshal(env)
390
- return p.writeEditorFrame(body)
398
+ errPayload, _ := json.Marshal(struct {
399
+ Code int `json:"code"`
400
+ Message string `json:"message"`
401
+ }{Code: jsonRPCInternalError, Message: message})
402
+ env := Envelope{JSONRPC: "2.0", ID: id, Error: errPayload}
403
+ body, _ := json.Marshal(env)
404
+ return p.writeEditorFrame(body)
391
405
  }