ttsc 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/cmd/ttsc/api_compile.go +101 -106
- package/cmd/ttsc/api_compile_branches_test.go +81 -0
- package/cmd/ttsc/api_transform.go +49 -54
- package/cmd/ttsc/api_transform_branches_test.go +41 -0
- package/cmd/ttsc/build.go +108 -109
- package/cmd/ttsc/build_branches_test.go +130 -0
- package/cmd/ttsc/helpers_test.go +47 -0
- package/cmd/ttsc/main.go +104 -103
- package/cmd/ttsc/main_alias_branches_test.go +29 -0
- package/cmd/ttscserver/main.go +104 -96
- package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +42 -0
- package/cmd/ttscserver/run_lsp_reports_server_error_test.go +23 -25
- package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +44 -0
- package/cmd/utility-host/main.go +42 -0
- package/driver/lsp.go +60 -0
- package/driver/plugins.go +122 -0
- package/driver/program.go +346 -356
- package/driver/rewrite.go +336 -339
- package/{driver → internal/lspserver}/lsp_envelope.go +70 -64
- package/{driver → internal/lspserver}/lsp_frame.go +62 -62
- package/{driver → internal/lspserver}/lsp_plugin.go +58 -58
- package/internal/lspserver/lsp_proxy.go +391 -0
- package/internal/lspserver/lsp_server.go +231 -0
- package/lib/TtscCompiler.d.ts +3 -3
- package/lib/TtscCompiler.js +20 -6
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.d.ts +1 -0
- package/lib/compiler/internal/resolveTsgo.js +15 -4
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runBuild.js +12 -4
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -33
- package/lib/compiler/internal/sharedHostHelpers.js +27 -70
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +12 -5
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +9 -4
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +1 -0
- package/lib/launcher/internal/runTtscserver.js +38 -1
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +3 -0
- package/lib/plugin/internal/buildSourcePlugin.js +444 -41
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +153 -34
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +2 -1
- package/lib/structures/ITtscPlugin.d.ts +8 -13
- package/lib/structures/ITtscPluginFactoryContext.d.ts +4 -5
- package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +4 -2
- package/package.json +8 -8
- package/shim/lsp/shim.go +11 -17
- package/src/TtscCompiler.ts +20 -7
- package/src/compiler/internal/resolveTsgo.ts +18 -5
- package/src/compiler/internal/runBuild.ts +17 -4
- package/src/compiler/internal/sharedHostHelpers.ts +34 -67
- package/src/compiler/internal/transformProjectInMemory.ts +18 -5
- package/src/index.ts +0 -1
- package/src/launcher/internal/runTtsc.ts +9 -4
- package/src/launcher/internal/runTtscserver.ts +43 -1
- package/src/plugin/internal/buildSourcePlugin.ts +475 -41
- package/src/plugin/internal/loadProjectPlugins.ts +199 -39
- package/src/structures/ITtscCompilerContext.ts +2 -1
- package/src/structures/ITtscPlugin.ts +9 -13
- package/src/structures/ITtscPluginFactoryContext.ts +4 -5
- package/src/structures/ITtscProjectPluginConfig.ts +2 -2
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +4 -2
- package/utility/host.go +267 -1296
- package/driver/lsp_proxy.go +0 -394
- package/driver/lsp_server.go +0 -197
- package/lib/defineTtscPlugin.d.ts +0 -18
- package/lib/defineTtscPlugin.js +0 -7
- package/lib/defineTtscPlugin.js.map +0 -1
- package/src/defineTtscPlugin.ts +0 -26
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"sync"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// ErrCommandNotHandled is returned by PluginSource.ExecuteCommand for
|
|
14
|
+
// commands ttsc does not own. Callers should fall through to upstream.
|
|
15
|
+
var ErrCommandNotHandled = errors.New("lsp: command not handled by ttsc")
|
|
16
|
+
|
|
17
|
+
const (
|
|
18
|
+
methodPublishDiagnostics = "textDocument/publishDiagnostics"
|
|
19
|
+
methodCodeAction = "textDocument/codeAction"
|
|
20
|
+
methodExecuteCommand = "workspace/executeCommand"
|
|
21
|
+
methodCancelRequest = "$/cancelRequest"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// ProxyOptions wires the byte-level proxy together. ttscserver creates
|
|
25
|
+
// the upstream pipes around `tsgo --lsp --stdio` and hands the proxy
|
|
26
|
+
// editor stdio plus those pipe ends.
|
|
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
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Proxy bridges the editor and an upstream tsgo LSP process, intercepting
|
|
36
|
+
// the message types ttsc cares about (publishDiagnostics merge, code
|
|
37
|
+
// action augmentation, executeCommand for ttsc-owned commands).
|
|
38
|
+
type Proxy struct {
|
|
39
|
+
editorIn io.Reader
|
|
40
|
+
editorOut io.Writer
|
|
41
|
+
upstreamIn io.Writer
|
|
42
|
+
upstreamOut io.Reader
|
|
43
|
+
source PluginSource
|
|
44
|
+
|
|
45
|
+
writeMu sync.Mutex // serializes WriteFrame calls to editorOut
|
|
46
|
+
|
|
47
|
+
pendingMu sync.Mutex
|
|
48
|
+
pendingActions map[string]pendingCodeActionRequest
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type pendingCodeActionRequest struct {
|
|
52
|
+
uri string
|
|
53
|
+
rng LSPRange
|
|
54
|
+
ctx LSPCodeActionContext
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// NewProxy returns a Proxy ready to Run. The PluginSource is required;
|
|
58
|
+
// pass NullPluginSource{} for a no-contribution setup.
|
|
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
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run drives both pump goroutines until they return. Pumps return when
|
|
75
|
+
// their input stream closes (ErrFrameClosed), when context cancellation
|
|
76
|
+
// has already been observed by the upstream/editor closers, or when a
|
|
77
|
+
// pipe write fails. ErrFrameClosed and context.Canceled are folded into
|
|
78
|
+
// a nil result so editor shutdown does not look like a crash.
|
|
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) }()
|
|
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
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// pumpEditorToUpstream reads frames from the editor, decides whether to
|
|
95
|
+
// forward verbatim or handle locally (executeCommand for ttsc commands,
|
|
96
|
+
// codeAction request bookkeeping), and writes the chosen frame. When the
|
|
97
|
+
// editor closes its end (ErrFrameClosed) the pump closes the upstream
|
|
98
|
+
// writer so tsgo's server-side Read returns EOF and its Run loop drains;
|
|
99
|
+
// without that nudge tsgo would wait forever for more input.
|
|
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
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// closeUpstreamInput closes the upstream writer (if it is also an
|
|
131
|
+
// io.Closer) so the upstream process reads io.EOF and exits its read loop.
|
|
132
|
+
// We type-assert because the public ProxyOptions surface promises only
|
|
133
|
+
// io.Writer; RunLSPServer always passes an *io.PipeWriter in practice.
|
|
134
|
+
func (p *Proxy) closeUpstreamInput() {
|
|
135
|
+
if closer, ok := p.upstreamIn.(io.Closer); ok {
|
|
136
|
+
_ = closer.Close()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// handleEditorEnvelope returns true if the envelope was fully processed
|
|
141
|
+
// locally (responded to without forwarding upstream).
|
|
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
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// forgetCancelledRequest removes any pending codeAction entry whose id
|
|
163
|
+
// the editor cancelled. The notification still flows to upstream so
|
|
164
|
+
// tsgo can respond with its own cancellation error. The id is keyed
|
|
165
|
+
// through the shared normalizer so a cancel for `1.0` deletes an entry
|
|
166
|
+
// stored under `1` (and vice versa for string-vs-numeric encodings).
|
|
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, ¶ms); 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
|
+
}
|
|
182
|
+
|
|
183
|
+
// rememberCodeActionRequest stores the request payload so the matching
|
|
184
|
+
// response from upstream can be augmented with ttsc-owned code actions
|
|
185
|
+
// for the same range.
|
|
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, ¶ms); 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
|
+
}
|
|
205
|
+
|
|
206
|
+
// tryExecuteCommand handles workspace/executeCommand requests whose
|
|
207
|
+
// command id is registered with the PluginSource. Returns true on a
|
|
208
|
+
// successful local response; false when the command should fall through
|
|
209
|
+
// to upstream tsgo.
|
|
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, ¶ms); 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
|
+
}
|
|
238
|
+
|
|
239
|
+
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
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// pumpUpstreamToEditor reads frames from the upstream tsgo server,
|
|
249
|
+
// augments publishDiagnostics and codeAction responses, and forwards
|
|
250
|
+
// every other frame untouched. The loop terminates on Read error; ctx
|
|
251
|
+
// propagation happens through pipe closure by RunLSPServer's deferred
|
|
252
|
+
// cleanup goroutines.
|
|
253
|
+
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
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// augmentUpstream returns the (possibly rewritten) body to forward. For
|
|
275
|
+
// publishDiagnostics it merges ttsc plugin diagnostics; for codeAction
|
|
276
|
+
// responses tied to a remembered request it appends ttsc actions.
|
|
277
|
+
//
|
|
278
|
+
// Cancellation and response are handled in two independent goroutines
|
|
279
|
+
// (pumpEditorToUpstream owns $/cancelRequest cleanup, pumpUpstreamToEditor
|
|
280
|
+
// owns response augmentation). When the response wins the race against
|
|
281
|
+
// a pending cancel, pendingActions[env.IDKey()] is still populated and
|
|
282
|
+
// the response is augmented before forgetCancelledRequest runs — the
|
|
283
|
+
// editor is expected to discard the late response per LSP $/cancelRequest
|
|
284
|
+
// semantics. When the cancel wins, the pending entry is gone and
|
|
285
|
+
// augmentation skips cleanly.
|
|
286
|
+
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
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// mergePublishDiagnostics decodes the publishDiagnostics params, asks
|
|
309
|
+
// the PluginSource for the URI's contributions, appends them, and
|
|
310
|
+
// re-encodes the envelope. Returns the rewritten body and true on
|
|
311
|
+
// success; ok=false means "nothing to merge — forward the upstream body
|
|
312
|
+
// verbatim." Re-encoding our own types is guaranteed by their
|
|
313
|
+
// definitions in lsp_plugin.go, so the marshal errors are not checked.
|
|
314
|
+
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, ¶ms); 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
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// appendCodeActions adds ttsc-owned code actions to a forwarded
|
|
337
|
+
// codeAction response. The response body is either an array of actions
|
|
338
|
+
// or null; we preserve that shape so editors that special-case null
|
|
339
|
+
// keep working. The function refuses to splice into error responses
|
|
340
|
+
// (JSON-RPC §5.1 forbids both `result` and `error` on the same frame)
|
|
341
|
+
// and into non-array results (the LSP base protocol mandates an array
|
|
342
|
+
// or null for codeAction; anything else means upstream returned a
|
|
343
|
+
// shape ttsc cannot splice into, so the proxy forwards verbatim
|
|
344
|
+
// rather than corrupting it).
|
|
345
|
+
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
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
func (p *Proxy) writeEditorFrame(body []byte) error {
|
|
371
|
+
p.writeMu.Lock()
|
|
372
|
+
defer p.writeMu.Unlock()
|
|
373
|
+
return WriteFrame(p.editorOut, body)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
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)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
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)
|
|
391
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"os/exec"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"runtime/debug"
|
|
11
|
+
"sync"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// ErrLSPUpstreamPanic wraps a panic recovered from inside an upstream
|
|
16
|
+
// runner. The production runner is an external tsgo process, but tests
|
|
17
|
+
// and embedders can still install in-process runners through
|
|
18
|
+
// WithUpstreamRunnerForTest.
|
|
19
|
+
var ErrLSPUpstreamPanic = errors.New("ttscserver: tsgo upstream runner panicked")
|
|
20
|
+
|
|
21
|
+
// RecoverPanicAs runs fn and converts a panic into an
|
|
22
|
+
// ErrLSPUpstreamPanic-wrapped error. RunLSPServer uses it around the
|
|
23
|
+
// upstream runner seam; the recovered stack is attached for diagnostics.
|
|
24
|
+
//
|
|
25
|
+
// recover() per the Go spec catches panics but NOT runtime.Goexit, so
|
|
26
|
+
// a Goexit raised from inside fn surfaces as a clean nil return here
|
|
27
|
+
// (the upstream goroutine exits without an error). Hosting code that
|
|
28
|
+
// must turn Goexit into a typed error should run fn in a separate
|
|
29
|
+
// goroutine and join on a sentinel channel — outside this helper's
|
|
30
|
+
// scope today.
|
|
31
|
+
func RecoverPanicAs(fn func() error) (err error) {
|
|
32
|
+
defer func() {
|
|
33
|
+
if r := recover(); r != nil {
|
|
34
|
+
err = fmt.Errorf("%w: %v\n%s", ErrLSPUpstreamPanic, r, debug.Stack())
|
|
35
|
+
}
|
|
36
|
+
}()
|
|
37
|
+
return fn()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// LSPServerOptions wires ttscserver to its three channels of state:
|
|
41
|
+
// editor stdio for the LSP transport, an optional ttsc PluginSource for
|
|
42
|
+
// merging plugin diagnostics into the stream, and the tsgo binary that
|
|
43
|
+
// provides the upstream LSP server.
|
|
44
|
+
type LSPServerOptions struct {
|
|
45
|
+
// In is the editor-side reader; ttscserver reads JSON-RPC frames from
|
|
46
|
+
// it and forwards or handles them. RunLSPServer closes it on shutdown
|
|
47
|
+
// if it also implements io.Closer so blocked frame reads can unblock.
|
|
48
|
+
In io.Reader
|
|
49
|
+
// Out is the editor-side writer; ttscserver writes both upstream
|
|
50
|
+
// responses and locally-synthesized messages to it.
|
|
51
|
+
Out io.Writer
|
|
52
|
+
// Err is the upstream tsgo server's stderr sink. ttscserver does not
|
|
53
|
+
// log to it directly.
|
|
54
|
+
Err io.Writer
|
|
55
|
+
|
|
56
|
+
// Cwd is the project root used as the upstream tsgo process working
|
|
57
|
+
// directory. An empty string is rejected before any process starts.
|
|
58
|
+
Cwd string
|
|
59
|
+
// TsgoBinary is the absolute path to the project-selected
|
|
60
|
+
// @typescript/native-preview executable.
|
|
61
|
+
TsgoBinary string
|
|
62
|
+
// Source contributes ttsc plugin diagnostics / code actions /
|
|
63
|
+
// executeCommand handling. Nil falls back to NullPluginSource{}.
|
|
64
|
+
Source PluginSource
|
|
65
|
+
// ProgressDelay is accepted for CLI compatibility. The external tsgo
|
|
66
|
+
// LSP command does not currently expose a progress-delay flag.
|
|
67
|
+
ProgressDelay time.Duration
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ErrLSPCwdRequired is returned when LSPServerOptions.Cwd is empty.
|
|
71
|
+
// ttsc surfaces a clean error here instead of starting tsgo from an
|
|
72
|
+
// undefined project directory.
|
|
73
|
+
var ErrLSPCwdRequired = errors.New("ttscserver: cwd is required")
|
|
74
|
+
|
|
75
|
+
// ErrLSPTsgoBinaryRequired is returned when no upstream tsgo executable
|
|
76
|
+
// path was supplied by the JavaScript launcher or native caller.
|
|
77
|
+
var ErrLSPTsgoBinaryRequired = errors.New("ttscserver: tsgo binary is required")
|
|
78
|
+
|
|
79
|
+
// LSPUpstreamRunner is the seam tests use to substitute the external
|
|
80
|
+
// tsgo process with a controllable fake.
|
|
81
|
+
type LSPUpstreamRunner func(ctx context.Context, in io.Reader, out io.Writer, opts LSPServerOptions) error
|
|
82
|
+
|
|
83
|
+
// upstreamRunner is replaced in tests via WithUpstreamRunnerForTest.
|
|
84
|
+
var upstreamRunner LSPUpstreamRunner = defaultUpstreamRunner
|
|
85
|
+
|
|
86
|
+
// upstreamValidator checks production-only upstream requirements before any
|
|
87
|
+
// proxy goroutine starts. Test upstream runners replace it with a no-op.
|
|
88
|
+
var upstreamValidator = validateDefaultUpstreamOptions
|
|
89
|
+
|
|
90
|
+
// WithUpstreamRunnerForTest substitutes the upstream runner used by
|
|
91
|
+
// RunLSPServer. Returns a function the caller defers to restore the
|
|
92
|
+
// production runner. The seam stays in the public driver API because
|
|
93
|
+
// tests and embedders otherwise have no way to bypass tsgo.
|
|
94
|
+
func WithUpstreamRunnerForTest(runner LSPUpstreamRunner) func() {
|
|
95
|
+
prev := upstreamRunner
|
|
96
|
+
prevValidator := upstreamValidator
|
|
97
|
+
upstreamRunner = runner
|
|
98
|
+
upstreamValidator = func(LSPServerOptions) error { return nil }
|
|
99
|
+
return func() {
|
|
100
|
+
upstreamRunner = prev
|
|
101
|
+
upstreamValidator = prevValidator
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func validateDefaultUpstreamOptions(opts LSPServerOptions) error {
|
|
106
|
+
if opts.TsgoBinary == "" {
|
|
107
|
+
return ErrLSPTsgoBinaryRequired
|
|
108
|
+
}
|
|
109
|
+
if !filepath.IsAbs(opts.TsgoBinary) {
|
|
110
|
+
return fmt.Errorf("ttscserver: tsgo binary must be absolute: %s", opts.TsgoBinary)
|
|
111
|
+
}
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func defaultUpstreamRunner(ctx context.Context, in io.Reader, out io.Writer, opts LSPServerOptions) error {
|
|
116
|
+
cmd := exec.CommandContext(ctx, opts.TsgoBinary, "--lsp", "--stdio")
|
|
117
|
+
cmd.Dir = opts.Cwd
|
|
118
|
+
cmd.Stdin = in
|
|
119
|
+
cmd.Stdout = out
|
|
120
|
+
cmd.Stderr = opts.Err
|
|
121
|
+
if err := cmd.Run(); err != nil {
|
|
122
|
+
if ctx.Err() != nil {
|
|
123
|
+
return ctx.Err()
|
|
124
|
+
}
|
|
125
|
+
return fmt.Errorf("tsgo --lsp --stdio: %w", err)
|
|
126
|
+
}
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// RunLSPServer starts an upstream `tsgo --lsp --stdio` process and the byte-level
|
|
131
|
+
// proxy, blocking until either side returns. The first non-graceful
|
|
132
|
+
// error wins; ErrFrameClosed and context cancellation are treated as
|
|
133
|
+
// clean shutdown so editor close sequences do not look like crashes.
|
|
134
|
+
//
|
|
135
|
+
// The lifecycle is:
|
|
136
|
+
//
|
|
137
|
+
// 1. Open two pipes around the tsgo process (editor->server, server->editor).
|
|
138
|
+
// 2. Spawn the upstream runner (real tsgo process or a test fake) reading/writing those pipes.
|
|
139
|
+
// 3. Run the proxy in parallel.
|
|
140
|
+
// 4. A watchdog cascades context cancellation by closing every pipe so both
|
|
141
|
+
// halves unblock; the goroutines' own defers close the rest.
|
|
142
|
+
func RunLSPServer(ctx context.Context, opts LSPServerOptions) error {
|
|
143
|
+
if opts.Cwd == "" {
|
|
144
|
+
return ErrLSPCwdRequired
|
|
145
|
+
}
|
|
146
|
+
if err := upstreamValidator(opts); err != nil {
|
|
147
|
+
return err
|
|
148
|
+
}
|
|
149
|
+
source := opts.Source
|
|
150
|
+
if source == nil {
|
|
151
|
+
source = NullPluginSource{}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
upstreamInR, upstreamInW := io.Pipe()
|
|
155
|
+
upstreamOutR, upstreamOutW := io.Pipe()
|
|
156
|
+
|
|
157
|
+
proxy := NewProxy(ProxyOptions{
|
|
158
|
+
EditorIn: opts.In,
|
|
159
|
+
EditorOut: opts.Out,
|
|
160
|
+
UpstreamIn: upstreamInW,
|
|
161
|
+
UpstreamOut: upstreamOutR,
|
|
162
|
+
Source: source,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
serverCtx, cancel := context.WithCancel(ctx)
|
|
166
|
+
defer cancel()
|
|
167
|
+
|
|
168
|
+
// Watchdog: on cancel, close the writer ends of the upstream pipes so
|
|
169
|
+
// both halves unblock with a clean io.EOF. Closing readers directly
|
|
170
|
+
// would surface as io.ErrClosedPipe and make ttsc's error fold
|
|
171
|
+
// ambiguous; closing writers preserves the ErrFrameClosed signal. The
|
|
172
|
+
// editor input is also closed when possible so an upstream process-start
|
|
173
|
+
// failure cannot leave the editor->upstream pump blocked on stdin.
|
|
174
|
+
go func() {
|
|
175
|
+
<-serverCtx.Done()
|
|
176
|
+
closeIfCloser(opts.In)
|
|
177
|
+
upstreamInW.Close()
|
|
178
|
+
upstreamOutW.Close()
|
|
179
|
+
}()
|
|
180
|
+
|
|
181
|
+
var wg sync.WaitGroup
|
|
182
|
+
var serverErr, proxyErr error
|
|
183
|
+
wg.Add(2)
|
|
184
|
+
go func() {
|
|
185
|
+
defer wg.Done()
|
|
186
|
+
defer cancel()
|
|
187
|
+
defer upstreamOutW.Close()
|
|
188
|
+
defer upstreamInR.Close()
|
|
189
|
+
serverErr = RecoverPanicAs(func() error {
|
|
190
|
+
return upstreamRunner(serverCtx, upstreamInR, upstreamOutW, opts)
|
|
191
|
+
})
|
|
192
|
+
}()
|
|
193
|
+
go func() {
|
|
194
|
+
defer wg.Done()
|
|
195
|
+
defer cancel()
|
|
196
|
+
defer upstreamInW.Close()
|
|
197
|
+
defer upstreamOutR.Close()
|
|
198
|
+
proxyErr = proxy.Run(serverCtx)
|
|
199
|
+
}()
|
|
200
|
+
wg.Wait()
|
|
201
|
+
|
|
202
|
+
for _, err := range []error{serverErr, proxyErr} {
|
|
203
|
+
if err == nil {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
if errors.Is(err, context.Canceled) {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if errors.Is(err, ErrFrameClosed) {
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if errors.Is(err, io.ErrClosedPipe) {
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
return err
|
|
216
|
+
}
|
|
217
|
+
return nil
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// DenyNpmInstall is kept for source compatibility with older driver
|
|
221
|
+
// embedders that hosted tsgo in-process. The process wrapper cannot
|
|
222
|
+
// override tsgo's internal ATA callback.
|
|
223
|
+
func DenyNpmInstall(_ string, args []string) ([]byte, error) {
|
|
224
|
+
return nil, fmt.Errorf("ttscserver: npm install disabled in LSP host (args=%v)", args)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func closeIfCloser(value any) {
|
|
228
|
+
if closer, ok := value.(io.Closer); ok {
|
|
229
|
+
_ = closer.Close()
|
|
230
|
+
}
|
|
231
|
+
}
|
package/lib/TtscCompiler.d.ts
CHANGED
|
@@ -42,9 +42,9 @@ export declare class TtscCompiler {
|
|
|
42
42
|
* Remove compiled cache artifacts for this compiler instance.
|
|
43
43
|
*
|
|
44
44
|
* When the constructor received `cacheDir`, this method removes exactly that
|
|
45
|
-
* directory.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* directory. When `TTSC_CACHE_DIR` is active, it removes that override's
|
|
46
|
+
* plugin cache plus legacy project-local caches. Otherwise it removes the
|
|
47
|
+
* default global plugin cache plus legacy project-local caches.
|
|
48
48
|
*
|
|
49
49
|
* The cache target comes from the constructor context. Create another
|
|
50
50
|
* `TtscCompiler` instance to clean another project or another cache root.
|
package/lib/TtscCompiler.js
CHANGED
|
@@ -10,6 +10,7 @@ const compileProjectInMemory_1 = require("./compiler/internal/compileProjectInMe
|
|
|
10
10
|
const resolveProjectConfig_1 = require("./compiler/internal/project/resolveProjectConfig");
|
|
11
11
|
const resolveBinary_1 = require("./compiler/internal/resolveBinary");
|
|
12
12
|
const transformProjectInMemory_1 = require("./compiler/internal/transformProjectInMemory");
|
|
13
|
+
const buildSourcePlugin_1 = require("./plugin/internal/buildSourcePlugin");
|
|
13
14
|
const loadProjectPlugins_1 = require("./plugin/internal/loadProjectPlugins");
|
|
14
15
|
/**
|
|
15
16
|
* Programmatic compiler host for the `ttsc` TypeScript-Go pipeline.
|
|
@@ -71,9 +72,9 @@ class TtscCompiler {
|
|
|
71
72
|
* Remove compiled cache artifacts for this compiler instance.
|
|
72
73
|
*
|
|
73
74
|
* When the constructor received `cacheDir`, this method removes exactly that
|
|
74
|
-
* directory.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* directory. When `TTSC_CACHE_DIR` is active, it removes that override's
|
|
76
|
+
* plugin cache plus legacy project-local caches. Otherwise it removes the
|
|
77
|
+
* default global plugin cache plus legacy project-local caches.
|
|
77
78
|
*
|
|
78
79
|
* The cache target comes from the constructor context. Create another
|
|
79
80
|
* `TtscCompiler` instance to clean another project or another cache root.
|
|
@@ -85,12 +86,25 @@ class TtscCompiler {
|
|
|
85
86
|
if (cacheDir !== undefined) {
|
|
86
87
|
return removeExistingDirectories([cacheDir]);
|
|
87
88
|
}
|
|
88
|
-
const envCacheDir = this.context.env?.TTSC_CACHE_DIR ?? process.env.TTSC_CACHE_DIR;
|
|
89
89
|
const projectRoot = this.resolveCleanProjectRoot();
|
|
90
|
-
|
|
91
|
-
...(envCacheDir ? [node_path_1.default.resolve(envCacheDir, "plugins")] : []),
|
|
90
|
+
const legacyTargets = [
|
|
92
91
|
node_path_1.default.join(projectRoot, "node_modules", ".ttsc"),
|
|
93
92
|
node_path_1.default.join(projectRoot, ".ttsc"),
|
|
93
|
+
];
|
|
94
|
+
if (this.context.env?.TTSC_CACHE_DIR) {
|
|
95
|
+
return removeExistingDirectories([
|
|
96
|
+
node_path_1.default.resolve(projectRoot, this.context.env.TTSC_CACHE_DIR, "plugins"),
|
|
97
|
+
...legacyTargets,
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
if (process.env.TTSC_CACHE_DIR) {
|
|
101
|
+
return removeExistingDirectories([
|
|
102
|
+
node_path_1.default.resolve(process.env.TTSC_CACHE_DIR, "plugins"),
|
|
103
|
+
...legacyTargets,
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
return removeExistingDirectories([
|
|
107
|
+
...(0, buildSourcePlugin_1.defaultPluginCacheCleanTargets)(projectRoot),
|
|
94
108
|
]);
|
|
95
109
|
}
|
|
96
110
|
/**
|