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
package/driver/lsp_proxy.go
DELETED
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
package driver
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"bytes"
|
|
5
|
-
"context"
|
|
6
|
-
"encoding/json"
|
|
7
|
-
"errors"
|
|
8
|
-
"fmt"
|
|
9
|
-
"io"
|
|
10
|
-
"sync"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// ErrCommandNotHandled is returned by PluginSource.ExecuteCommand for
|
|
15
|
-
// commands ttsc does not own. Callers should fall through to upstream.
|
|
16
|
-
var ErrCommandNotHandled = errors.New("lsp: command not handled by ttsc")
|
|
17
|
-
|
|
18
|
-
const (
|
|
19
|
-
methodPublishDiagnostics = "textDocument/publishDiagnostics"
|
|
20
|
-
methodCodeAction = "textDocument/codeAction"
|
|
21
|
-
methodExecuteCommand = "workspace/executeCommand"
|
|
22
|
-
methodCancelRequest = "$/cancelRequest"
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
// ProxyOptions wires the byte-level proxy together. ttscserver creates
|
|
26
|
-
// the upstream pipes around an embedded tsgo lsp.Server and hands the
|
|
27
|
-
// proxy editor stdio plus those pipe ends.
|
|
28
|
-
type ProxyOptions struct {
|
|
29
|
-
EditorIn io.Reader
|
|
30
|
-
EditorOut io.Writer
|
|
31
|
-
UpstreamIn io.Writer // we write here; tsgo lsp.Server reads
|
|
32
|
-
UpstreamOut io.Reader // tsgo lsp.Server writes here; we read
|
|
33
|
-
Source PluginSource
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Proxy bridges the editor and an upstream tsgo lsp.Server, intercepting
|
|
37
|
-
// the message types ttsc cares about (publishDiagnostics merge, code
|
|
38
|
-
// action augmentation, executeCommand for ttsc-owned commands).
|
|
39
|
-
type Proxy struct {
|
|
40
|
-
editorIn io.Reader
|
|
41
|
-
editorOut io.Writer
|
|
42
|
-
upstreamIn io.Writer
|
|
43
|
-
upstreamOut io.Reader
|
|
44
|
-
source PluginSource
|
|
45
|
-
|
|
46
|
-
writeMu sync.Mutex // serializes WriteFrame calls to editorOut
|
|
47
|
-
|
|
48
|
-
pendingMu sync.Mutex
|
|
49
|
-
pendingActions map[string]pendingCodeActionRequest
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type pendingCodeActionRequest struct {
|
|
53
|
-
uri string
|
|
54
|
-
rng LSPRange
|
|
55
|
-
ctx LSPCodeActionContext
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// NewProxy returns a Proxy ready to Run. The PluginSource is required;
|
|
59
|
-
// pass NullPluginSource{} for a no-contribution setup.
|
|
60
|
-
func NewProxy(opts ProxyOptions) *Proxy {
|
|
61
|
-
source := opts.Source
|
|
62
|
-
if source == nil {
|
|
63
|
-
source = NullPluginSource{}
|
|
64
|
-
}
|
|
65
|
-
return &Proxy{
|
|
66
|
-
editorIn: opts.EditorIn,
|
|
67
|
-
editorOut: opts.EditorOut,
|
|
68
|
-
upstreamIn: opts.UpstreamIn,
|
|
69
|
-
upstreamOut: opts.UpstreamOut,
|
|
70
|
-
source: source,
|
|
71
|
-
pendingActions: make(map[string]pendingCodeActionRequest),
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Run drives both pump goroutines until they return. Pumps return when
|
|
76
|
-
// their input stream closes (ErrFrameClosed), when context cancellation
|
|
77
|
-
// has already been observed by the upstream/editor closers, or when a
|
|
78
|
-
// pipe write fails. ErrFrameClosed and context.Canceled are folded into
|
|
79
|
-
// a nil result so editor shutdown does not look like a crash.
|
|
80
|
-
func (p *Proxy) Run(ctx context.Context) error {
|
|
81
|
-
errCh := make(chan error, 2)
|
|
82
|
-
go func() { errCh <- p.pumpEditorToUpstream(ctx) }()
|
|
83
|
-
go func() { errCh <- p.pumpUpstreamToEditor(ctx) }()
|
|
84
|
-
|
|
85
|
-
var first error
|
|
86
|
-
for i := 0; i < 2; i++ {
|
|
87
|
-
err := <-errCh
|
|
88
|
-
if first == nil && err != nil && !errors.Is(err, ErrFrameClosed) {
|
|
89
|
-
first = err
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return first
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// pumpEditorToUpstream reads frames from the editor, decides whether to
|
|
96
|
-
// forward verbatim or handle locally (executeCommand for ttsc commands,
|
|
97
|
-
// codeAction request bookkeeping), and writes the chosen frame. When the
|
|
98
|
-
// editor closes its end (ErrFrameClosed) the pump closes the upstream
|
|
99
|
-
// writer so tsgo's server-side Read returns EOF and its Run loop drains;
|
|
100
|
-
// without that nudge tsgo would wait forever for more input.
|
|
101
|
-
func (p *Proxy) pumpEditorToUpstream(_ context.Context) error {
|
|
102
|
-
fr := NewFrameReader(p.editorIn)
|
|
103
|
-
for {
|
|
104
|
-
_, body, err := fr.Read()
|
|
105
|
-
if err != nil {
|
|
106
|
-
if errors.Is(err, ErrFrameClosed) {
|
|
107
|
-
p.closeUpstreamInput()
|
|
108
|
-
}
|
|
109
|
-
return err
|
|
110
|
-
}
|
|
111
|
-
env, parseErr := ParseEnvelope(body)
|
|
112
|
-
if parseErr != nil {
|
|
113
|
-
if forwardErr := WriteFrame(p.upstreamIn, body); forwardErr != nil {
|
|
114
|
-
return forwardErr
|
|
115
|
-
}
|
|
116
|
-
continue
|
|
117
|
-
}
|
|
118
|
-
if p.handleEditorEnvelope(env, body) {
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
if err := WriteFrame(p.upstreamIn, body); err != nil {
|
|
122
|
-
return err
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// closeUpstreamInput closes the upstream writer (if it is also an
|
|
128
|
-
// io.Closer) so the embedded server reads io.EOF and exits its readLoop.
|
|
129
|
-
// We type-assert because the public ProxyOptions surface promises only
|
|
130
|
-
// io.Writer; RunLSPServer always passes an *io.PipeWriter in practice.
|
|
131
|
-
func (p *Proxy) closeUpstreamInput() {
|
|
132
|
-
if closer, ok := p.upstreamIn.(io.Closer); ok {
|
|
133
|
-
_ = closer.Close()
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// handleEditorEnvelope returns true if the envelope was fully processed
|
|
138
|
-
// locally (responded to without forwarding upstream).
|
|
139
|
-
func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) bool {
|
|
140
|
-
switch env.Method {
|
|
141
|
-
case methodExecuteCommand:
|
|
142
|
-
if env.IsRequest() && p.tryExecuteCommand(env) {
|
|
143
|
-
return true
|
|
144
|
-
}
|
|
145
|
-
case methodCodeAction:
|
|
146
|
-
if env.IsRequest() {
|
|
147
|
-
p.rememberCodeActionRequest(env)
|
|
148
|
-
}
|
|
149
|
-
case methodCancelRequest:
|
|
150
|
-
// $/cancelRequest names an in-flight id the editor has given up on.
|
|
151
|
-
// The proxy drops any pending codeAction entry for that id so the
|
|
152
|
-
// map cannot grow unbounded across long editor sessions, then lets
|
|
153
|
-
// the notification continue to upstream.
|
|
154
|
-
p.forgetCancelledRequest(env)
|
|
155
|
-
}
|
|
156
|
-
return false
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// forgetCancelledRequest removes any pending codeAction entry whose id
|
|
160
|
-
// the editor cancelled. The notification still flows to upstream so
|
|
161
|
-
// tsgo can respond with its own cancellation error. The id is keyed
|
|
162
|
-
// through the shared normalizer so a cancel for `1.0` deletes an entry
|
|
163
|
-
// stored under `1` (and vice versa for string-vs-numeric encodings).
|
|
164
|
-
func (p *Proxy) forgetCancelledRequest(env Envelope) {
|
|
165
|
-
var params struct {
|
|
166
|
-
ID json.RawMessage `json:"id"`
|
|
167
|
-
}
|
|
168
|
-
if err := json.Unmarshal(env.Params, ¶ms); err != nil {
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
key := idKeyFromRaw(params.ID)
|
|
172
|
-
if key == "" {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
p.pendingMu.Lock()
|
|
176
|
-
defer p.pendingMu.Unlock()
|
|
177
|
-
delete(p.pendingActions, key)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// rememberCodeActionRequest stores the request payload so the matching
|
|
181
|
-
// response from upstream can be augmented with ttsc-owned code actions
|
|
182
|
-
// for the same range.
|
|
183
|
-
func (p *Proxy) rememberCodeActionRequest(env Envelope) {
|
|
184
|
-
var params struct {
|
|
185
|
-
TextDocument struct {
|
|
186
|
-
URI string `json:"uri"`
|
|
187
|
-
} `json:"textDocument"`
|
|
188
|
-
Range LSPRange `json:"range"`
|
|
189
|
-
Context LSPCodeActionContext `json:"context"`
|
|
190
|
-
}
|
|
191
|
-
if err := json.Unmarshal(env.Params, ¶ms); err != nil {
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
p.pendingMu.Lock()
|
|
195
|
-
defer p.pendingMu.Unlock()
|
|
196
|
-
p.pendingActions[env.IDKey()] = pendingCodeActionRequest{
|
|
197
|
-
uri: params.TextDocument.URI,
|
|
198
|
-
rng: params.Range,
|
|
199
|
-
ctx: params.Context,
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// tryExecuteCommand handles workspace/executeCommand requests whose
|
|
204
|
-
// command id is registered with the PluginSource. Returns true on a
|
|
205
|
-
// successful local response; false when the command should fall through
|
|
206
|
-
// to upstream tsgo.
|
|
207
|
-
func (p *Proxy) tryExecuteCommand(env Envelope) bool {
|
|
208
|
-
var params struct {
|
|
209
|
-
Command string `json:"command"`
|
|
210
|
-
Arguments []json.RawMessage `json:"arguments,omitempty"`
|
|
211
|
-
}
|
|
212
|
-
if err := json.Unmarshal(env.Params, ¶ms); err != nil {
|
|
213
|
-
return false
|
|
214
|
-
}
|
|
215
|
-
if !p.ownsCommand(params.Command) {
|
|
216
|
-
return false
|
|
217
|
-
}
|
|
218
|
-
edit, err := p.source.ExecuteCommand(params.Command, params.Arguments)
|
|
219
|
-
if errors.Is(err, ErrCommandNotHandled) {
|
|
220
|
-
return false
|
|
221
|
-
}
|
|
222
|
-
if err != nil {
|
|
223
|
-
p.writeError(env.ID, fmt.Sprintf("ttsc command %q failed: %v", params.Command, err))
|
|
224
|
-
return true
|
|
225
|
-
}
|
|
226
|
-
// Cycle 1 returns the WorkspaceEdit inside the executeCommand response
|
|
227
|
-
// instead of sending workspace/applyEdit as a server→client request.
|
|
228
|
-
// ttsc owns both ends (its VSCode extension), so the extension applies
|
|
229
|
-
// the edit on its side. Sticking to one direction avoids tracking our
|
|
230
|
-
// own outgoing request ids in the proxy.
|
|
231
|
-
if edit == nil {
|
|
232
|
-
p.writeResult(env.ID, nil)
|
|
233
|
-
return true
|
|
234
|
-
}
|
|
235
|
-
p.writeResult(env.ID, edit)
|
|
236
|
-
return true
|
|
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 embedded 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
|
-
if err := json.Unmarshal(trimmed, &existing); err != nil {
|
|
360
|
-
return nil, false
|
|
361
|
-
}
|
|
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
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
func (p *Proxy) writeEditorFrame(body []byte) error {
|
|
373
|
-
p.writeMu.Lock()
|
|
374
|
-
defer p.writeMu.Unlock()
|
|
375
|
-
return WriteFrame(p.editorOut, body)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
func (p *Proxy) writeResult(id json.RawMessage, result any) error {
|
|
379
|
-
rawResult, _ := json.Marshal(result)
|
|
380
|
-
env := Envelope{JSONRPC: "2.0", ID: id, Result: rawResult}
|
|
381
|
-
body, _ := json.Marshal(env)
|
|
382
|
-
return p.writeEditorFrame(body)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
func (p *Proxy) writeError(id json.RawMessage, message string) error {
|
|
386
|
-
errPayload, _ := json.Marshal(struct {
|
|
387
|
-
Code int `json:"code"`
|
|
388
|
-
Message string `json:"message"`
|
|
389
|
-
}{Code: -32603, Message: message})
|
|
390
|
-
env := Envelope{JSONRPC: "2.0", ID: id, Error: errPayload}
|
|
391
|
-
body, _ := json.Marshal(env)
|
|
392
|
-
return p.writeEditorFrame(body)
|
|
393
|
-
}
|
|
394
|
-
|
package/driver/lsp_server.go
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
package driver
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
"fmt"
|
|
7
|
-
"io"
|
|
8
|
-
"runtime/debug"
|
|
9
|
-
"sync"
|
|
10
|
-
"time"
|
|
11
|
-
|
|
12
|
-
"github.com/microsoft/typescript-go/shim/bundled"
|
|
13
|
-
shimlsp "github.com/microsoft/typescript-go/shim/lsp"
|
|
14
|
-
"github.com/microsoft/typescript-go/shim/vfs/osvfs"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
// ErrLSPUpstreamPanic wraps a panic recovered from inside the embedded
|
|
18
|
-
// tsgo lsp.Server. Without recovery the host process would die when
|
|
19
|
-
// tsgo crashed; with this wrapper the proxy can surface a typed error
|
|
20
|
-
// to the editor instead.
|
|
21
|
-
var ErrLSPUpstreamPanic = errors.New("ttscserver: embedded tsgo server panicked")
|
|
22
|
-
|
|
23
|
-
// RecoverPanicAs runs fn and converts a panic into an
|
|
24
|
-
// ErrLSPUpstreamPanic-wrapped error. defaultUpstreamRunner uses it and
|
|
25
|
-
// downstream LSP-host embeddings can opt into the same recovery
|
|
26
|
-
// contract; the recovered stack is attached for diagnostics.
|
|
27
|
-
//
|
|
28
|
-
// recover() per the Go spec catches panics but NOT runtime.Goexit, so
|
|
29
|
-
// a Goexit raised from inside fn surfaces as a clean nil return here
|
|
30
|
-
// (the upstream goroutine exits without an error). Hosting code that
|
|
31
|
-
// must turn Goexit into a typed error should run fn in a separate
|
|
32
|
-
// goroutine and join on a sentinel channel — outside this helper's
|
|
33
|
-
// scope today.
|
|
34
|
-
func RecoverPanicAs(fn func() error) (err error) {
|
|
35
|
-
defer func() {
|
|
36
|
-
if r := recover(); r != nil {
|
|
37
|
-
err = fmt.Errorf("%w: %v\n%s", ErrLSPUpstreamPanic, r, debug.Stack())
|
|
38
|
-
}
|
|
39
|
-
}()
|
|
40
|
-
return fn()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// LSPServerOptions wires ttscserver to its three channels of state:
|
|
44
|
-
// editor stdio for the LSP transport, an optional ttsc PluginSource for
|
|
45
|
-
// merging plugin diagnostics into the stream, and a working directory
|
|
46
|
-
// the embedded tsgo server treats as the project root.
|
|
47
|
-
type LSPServerOptions struct {
|
|
48
|
-
// In is the editor-side reader; ttscserver reads JSON-RPC frames from
|
|
49
|
-
// it and forwards or handles them.
|
|
50
|
-
In io.Reader
|
|
51
|
-
// Out is the editor-side writer; ttscserver writes both upstream
|
|
52
|
-
// responses and locally-synthesized messages to it.
|
|
53
|
-
Out io.Writer
|
|
54
|
-
// Err is the upstream tsgo server's stderr sink. ttscserver does not
|
|
55
|
-
// log to it directly.
|
|
56
|
-
Err io.Writer
|
|
57
|
-
|
|
58
|
-
// Cwd is the project root passed to tsgo's LSP. An empty string
|
|
59
|
-
// panics inside tsgo, so RunLSPServer validates it up front.
|
|
60
|
-
Cwd string
|
|
61
|
-
// Source contributes ttsc plugin diagnostics / code actions /
|
|
62
|
-
// executeCommand handling. Nil falls back to NullPluginSource{}.
|
|
63
|
-
Source PluginSource
|
|
64
|
-
// ProgressDelay matches tsgo's option for delaying the progress UI;
|
|
65
|
-
// zero disables the delay.
|
|
66
|
-
ProgressDelay time.Duration
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ErrLSPCwdRequired is returned when LSPServerOptions.Cwd is empty.
|
|
70
|
-
// ttsc surfaces a clean error here instead of letting tsgo's panic
|
|
71
|
-
// reach the editor.
|
|
72
|
-
var ErrLSPCwdRequired = errors.New("ttscserver: cwd is required")
|
|
73
|
-
|
|
74
|
-
// lspUpstreamRunner is the seam tests use to substitute the embedded
|
|
75
|
-
// tsgo lsp.Server with a controllable fake. Production wires it to the
|
|
76
|
-
// real shim-backed server.
|
|
77
|
-
type lspUpstreamRunner func(ctx context.Context, in io.Reader, out io.Writer, opts LSPServerOptions) error
|
|
78
|
-
|
|
79
|
-
// upstreamRunner is replaced in tests via WithUpstreamRunnerForTest.
|
|
80
|
-
var upstreamRunner lspUpstreamRunner = defaultUpstreamRunner
|
|
81
|
-
|
|
82
|
-
// WithUpstreamRunnerForTest substitutes the upstream runner used by
|
|
83
|
-
// RunLSPServer. Returns a function the caller defers to restore the
|
|
84
|
-
// production runner. The seam stays in the public API because tests
|
|
85
|
-
// live in driver_test and otherwise have no way to bypass tsgo.
|
|
86
|
-
func WithUpstreamRunnerForTest(runner lspUpstreamRunner) func() {
|
|
87
|
-
prev := upstreamRunner
|
|
88
|
-
upstreamRunner = runner
|
|
89
|
-
return func() { upstreamRunner = prev }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
func defaultUpstreamRunner(ctx context.Context, in io.Reader, out io.Writer, opts LSPServerOptions) error {
|
|
93
|
-
return RecoverPanicAs(func() error {
|
|
94
|
-
server := shimlsp.NewServer(&shimlsp.ServerOptions{
|
|
95
|
-
In: shimlsp.ToReader(in),
|
|
96
|
-
Out: shimlsp.ToWriter(out),
|
|
97
|
-
Err: opts.Err,
|
|
98
|
-
Cwd: opts.Cwd,
|
|
99
|
-
FS: DefaultFS(),
|
|
100
|
-
DefaultLibraryPath: bundled.LibPath(),
|
|
101
|
-
TypingsLocation: osvfs.GetGlobalTypingsCacheLocation(),
|
|
102
|
-
NpmInstall: DenyNpmInstall,
|
|
103
|
-
ProgressDelay: opts.ProgressDelay,
|
|
104
|
-
})
|
|
105
|
-
return server.Run(ctx)
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// RunLSPServer starts the embedded tsgo lsp.Server and the byte-level
|
|
110
|
-
// proxy, blocking until either side returns. The first non-graceful
|
|
111
|
-
// error wins; ErrFrameClosed and context cancellation are treated as
|
|
112
|
-
// clean shutdown so editor close sequences do not look like crashes.
|
|
113
|
-
//
|
|
114
|
-
// The lifecycle is:
|
|
115
|
-
//
|
|
116
|
-
// 1. Open two pipes around the embedded server (editor->server, server->editor).
|
|
117
|
-
// 2. Spawn the upstream runner (real tsgo or a test fake) reading/writing those pipes.
|
|
118
|
-
// 3. Run the proxy in parallel.
|
|
119
|
-
// 4. A watchdog cascades context cancellation by closing every pipe so both
|
|
120
|
-
// halves unblock; the goroutines' own defers close the rest.
|
|
121
|
-
func RunLSPServer(ctx context.Context, opts LSPServerOptions) error {
|
|
122
|
-
if opts.Cwd == "" {
|
|
123
|
-
return ErrLSPCwdRequired
|
|
124
|
-
}
|
|
125
|
-
source := opts.Source
|
|
126
|
-
if source == nil {
|
|
127
|
-
source = NullPluginSource{}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
upstreamInR, upstreamInW := io.Pipe()
|
|
131
|
-
upstreamOutR, upstreamOutW := io.Pipe()
|
|
132
|
-
|
|
133
|
-
proxy := NewProxy(ProxyOptions{
|
|
134
|
-
EditorIn: opts.In,
|
|
135
|
-
EditorOut: opts.Out,
|
|
136
|
-
UpstreamIn: upstreamInW,
|
|
137
|
-
UpstreamOut: upstreamOutR,
|
|
138
|
-
Source: source,
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
serverCtx, cancel := context.WithCancel(ctx)
|
|
142
|
-
defer cancel()
|
|
143
|
-
|
|
144
|
-
// Watchdog: on cancel, close the writer ends of the upstream pipes so
|
|
145
|
-
// both halves unblock with a clean io.EOF. Closing readers directly
|
|
146
|
-
// would surface as io.ErrClosedPipe and make ttsc's error fold
|
|
147
|
-
// ambiguous; closing writers preserves the ErrFrameClosed signal.
|
|
148
|
-
go func() {
|
|
149
|
-
<-serverCtx.Done()
|
|
150
|
-
upstreamInW.Close()
|
|
151
|
-
upstreamOutW.Close()
|
|
152
|
-
}()
|
|
153
|
-
|
|
154
|
-
var wg sync.WaitGroup
|
|
155
|
-
var serverErr, proxyErr error
|
|
156
|
-
wg.Add(2)
|
|
157
|
-
go func() {
|
|
158
|
-
defer wg.Done()
|
|
159
|
-
defer cancel()
|
|
160
|
-
defer upstreamOutW.Close()
|
|
161
|
-
defer upstreamInR.Close()
|
|
162
|
-
serverErr = upstreamRunner(serverCtx, upstreamInR, upstreamOutW, opts)
|
|
163
|
-
}()
|
|
164
|
-
go func() {
|
|
165
|
-
defer wg.Done()
|
|
166
|
-
defer cancel()
|
|
167
|
-
defer upstreamInW.Close()
|
|
168
|
-
defer upstreamOutR.Close()
|
|
169
|
-
proxyErr = proxy.Run(serverCtx)
|
|
170
|
-
}()
|
|
171
|
-
wg.Wait()
|
|
172
|
-
|
|
173
|
-
for _, err := range []error{serverErr, proxyErr} {
|
|
174
|
-
if err == nil {
|
|
175
|
-
continue
|
|
176
|
-
}
|
|
177
|
-
if errors.Is(err, context.Canceled) {
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
if errors.Is(err, ErrFrameClosed) {
|
|
181
|
-
continue
|
|
182
|
-
}
|
|
183
|
-
if errors.Is(err, io.ErrClosedPipe) {
|
|
184
|
-
continue
|
|
185
|
-
}
|
|
186
|
-
return err
|
|
187
|
-
}
|
|
188
|
-
return nil
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// DenyNpmInstall is the NpmInstall callback ttscserver passes to tsgo's
|
|
192
|
-
// LSP. ttsc has its own plugin / cache pipeline; we never want the LSP
|
|
193
|
-
// host running npm under the user's editor. Exposed so other LSP host
|
|
194
|
-
// embeddings can opt into the same behavior without copying the body.
|
|
195
|
-
func DenyNpmInstall(_ string, args []string) ([]byte, error) {
|
|
196
|
-
return nil, fmt.Errorf("ttscserver: npm install disabled in LSP host (args=%v)", args)
|
|
197
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { ITtscPlugin } from "./structures/ITtscPlugin";
|
|
2
|
-
/**
|
|
3
|
-
* Identity helper for authoring `ITtscPlugin` descriptors with full literal
|
|
4
|
-
* type preservation.
|
|
5
|
-
*
|
|
6
|
-
* Two call shapes:
|
|
7
|
-
*
|
|
8
|
-
* - Object form: returns the descriptor unchanged but pins its narrowed type so
|
|
9
|
-
* editor tooling and `satisfies` clauses can see exact field values.
|
|
10
|
-
* - Factory form: wraps a `(context) => descriptor` factory so the returned
|
|
11
|
-
* function carries the literal descriptor type through to consumers without
|
|
12
|
-
* forcing a manual `as const`.
|
|
13
|
-
*
|
|
14
|
-
* Pure pass-through at runtime — the function exists to teach TypeScript the
|
|
15
|
-
* const-narrowing the user would otherwise have to spell out.
|
|
16
|
-
*/
|
|
17
|
-
export declare function defineTtscPlugin<const T extends ITtscPlugin>(plugin: T): T;
|
|
18
|
-
export declare function defineTtscPlugin<TContext, const T extends ITtscPlugin>(factory: (context: TContext) => T): (context: TContext) => T;
|
package/lib/defineTtscPlugin.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"defineTtscPlugin.js","sourceRoot":"","sources":["../src/defineTtscPlugin.ts"],"names":[],"mappings":";;;AAqBA,0BACE,KAAwD;IAExD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/src/defineTtscPlugin.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { ITtscPlugin } from "./structures/ITtscPlugin";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Identity helper for authoring `ITtscPlugin` descriptors with full literal
|
|
5
|
-
* type preservation.
|
|
6
|
-
*
|
|
7
|
-
* Two call shapes:
|
|
8
|
-
*
|
|
9
|
-
* - Object form: returns the descriptor unchanged but pins its narrowed type so
|
|
10
|
-
* editor tooling and `satisfies` clauses can see exact field values.
|
|
11
|
-
* - Factory form: wraps a `(context) => descriptor` factory so the returned
|
|
12
|
-
* function carries the literal descriptor type through to consumers without
|
|
13
|
-
* forcing a manual `as const`.
|
|
14
|
-
*
|
|
15
|
-
* Pure pass-through at runtime — the function exists to teach TypeScript the
|
|
16
|
-
* const-narrowing the user would otherwise have to spell out.
|
|
17
|
-
*/
|
|
18
|
-
export function defineTtscPlugin<const T extends ITtscPlugin>(plugin: T): T;
|
|
19
|
-
export function defineTtscPlugin<TContext, const T extends ITtscPlugin>(
|
|
20
|
-
factory: (context: TContext) => T,
|
|
21
|
-
): (context: TContext) => T;
|
|
22
|
-
export function defineTtscPlugin(
|
|
23
|
-
value: ITtscPlugin | ((context: unknown) => ITtscPlugin),
|
|
24
|
-
): ITtscPlugin | ((context: unknown) => ITtscPlugin) {
|
|
25
|
-
return value;
|
|
26
|
-
}
|