ttsc 0.20.1 → 0.22.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/cmd/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
|
@@ -10,6 +10,7 @@ import (
|
|
|
10
10
|
"os/exec"
|
|
11
11
|
"strings"
|
|
12
12
|
"sync"
|
|
13
|
+
"sync/atomic"
|
|
13
14
|
"time"
|
|
14
15
|
)
|
|
15
16
|
|
|
@@ -18,12 +19,13 @@ const nativePluginCommandStdoutLimit = 4 * 1024 * 1024
|
|
|
18
19
|
const nativePluginCommandStderrLimit = 1024 * 1024
|
|
19
20
|
|
|
20
21
|
// NativePluginManifest is the JSON shape the JavaScript ttscserver launcher
|
|
21
|
-
//
|
|
22
|
+
// writes to its private manifest file after running normal project plugin
|
|
22
23
|
// discovery and source-plugin builds.
|
|
23
24
|
type NativePluginManifest struct {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
InitialProjectInputs map[string]LSPProjectInputSnapshot `json:"initialProjectInputs,omitempty"`
|
|
26
|
+
Plugins []NativePluginConfigEntry `json:"plugins"`
|
|
27
|
+
LSPPlugins []NativeLSPPluginEntry `json:"lspPlugins"`
|
|
28
|
+
ProjectContext json.RawMessage `json:"projectContext,omitempty"`
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// NativePluginConfigEntry mirrors the compact sidecar protocol used by
|
|
@@ -37,10 +39,14 @@ type NativePluginConfigEntry struct {
|
|
|
37
39
|
// NativeLSPPluginEntry names one built sidecar that opted into the LSP
|
|
38
40
|
// protocol through its JavaScript descriptor capabilities.
|
|
39
41
|
type NativeLSPPluginEntry struct {
|
|
40
|
-
Binary
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
Binary string `json:"binary"`
|
|
43
|
+
InitialProjectInputKey string `json:"initialProjectInputKey,omitempty"`
|
|
44
|
+
InitialProjectInputs *LSPProjectInputSnapshot `json:"initialProjectInputs,omitempty"`
|
|
45
|
+
Name string `json:"name,omitempty"`
|
|
46
|
+
ProjectDiagnostics bool `json:"projectDiagnostics,omitempty"`
|
|
47
|
+
ProjectInputs bool `json:"projectInputs,omitempty"`
|
|
48
|
+
ProjectContextArgs bool `json:"projectContextArgs,omitempty"`
|
|
49
|
+
Stage string `json:"stage,omitempty"`
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
// NativePluginSourceOptions configures a sidecar-backed PluginSource.
|
|
@@ -60,6 +66,15 @@ type NativePluginSource struct {
|
|
|
60
66
|
pluginsJSON string
|
|
61
67
|
projectContextJSON string
|
|
62
68
|
tsconfig string
|
|
69
|
+
clientTsconfig string
|
|
70
|
+
clientCwd string
|
|
71
|
+
|
|
72
|
+
// The client's project resolves once. Both halves are fixed for the
|
|
73
|
+
// session, while the answer costs a symlink walk per ancestor, and the
|
|
74
|
+
// question is asked again for every producer on every publication.
|
|
75
|
+
clientProjectOnce sync.Once
|
|
76
|
+
clientProject string
|
|
77
|
+
clientProjectKey string
|
|
63
78
|
|
|
64
79
|
commandIDs []string
|
|
65
80
|
codeActionKinds []string
|
|
@@ -88,6 +103,16 @@ type NativePluginSource struct {
|
|
|
88
103
|
owners map[string]NativeLSPPluginEntry
|
|
89
104
|
logMu sync.Mutex
|
|
90
105
|
|
|
106
|
+
projectInputsMu sync.RWMutex
|
|
107
|
+
projectInputs LSPProjectInputSnapshot
|
|
108
|
+
pluginProjectInputs map[string]projectInputRecord
|
|
109
|
+
projectInputsObserver func()
|
|
110
|
+
projectInputsRefresh coalescingRefresh
|
|
111
|
+
|
|
112
|
+
projectDiagnosticsMu sync.RWMutex
|
|
113
|
+
pluginProjectDiagnostics map[string]projectDiagnosticRecord
|
|
114
|
+
projectDiagnosticsSequence atomic.Uint64
|
|
115
|
+
|
|
91
116
|
// residentMu guards the resident-daemon table below. A resident sidecar keeps
|
|
92
117
|
// a warm Program across verbs, so lsp-diagnostics / lsp-code-actions reuse it
|
|
93
118
|
// instead of respawning per verb; serveUnsupported remembers a sidecar that
|
|
@@ -136,7 +161,9 @@ func NewNativePluginSource(opts NativePluginSourceOptions) (*NativePluginSource,
|
|
|
136
161
|
var manifest NativePluginManifest
|
|
137
162
|
if strings.TrimSpace(opts.ManifestJSON) != "" {
|
|
138
163
|
if err := json.Unmarshal([]byte(opts.ManifestJSON), &manifest); err != nil {
|
|
139
|
-
|
|
164
|
+
// The manifest reaches this constructor through three transports, so it
|
|
165
|
+
// is named by what it is rather than by the one that happened to carry it.
|
|
166
|
+
return nil, fmt.Errorf("ttscserver: invalid LSP plugin manifest: %w", err)
|
|
140
167
|
}
|
|
141
168
|
}
|
|
142
169
|
pluginsJSON, err := json.Marshal(manifest.Plugins)
|
|
@@ -167,9 +194,57 @@ func NewNativePluginSource(opts NativePluginSourceOptions) (*NativePluginSource,
|
|
|
167
194
|
pluginsJSON: string(pluginsJSON),
|
|
168
195
|
projectContextJSON: string(manifest.ProjectContext),
|
|
169
196
|
tsconfig: sidecarTsconfig,
|
|
197
|
+
clientTsconfig: opts.Tsconfig,
|
|
198
|
+
clientCwd: opts.Cwd,
|
|
170
199
|
owners: map[string]NativeLSPPluginEntry{},
|
|
171
200
|
}
|
|
172
201
|
source.discoverCommandIDs()
|
|
202
|
+
missingInitialProjectInputs := false
|
|
203
|
+
for _, plugin := range selectPluginTransports(
|
|
204
|
+
source.plugins,
|
|
205
|
+
func(plugin NativeLSPPluginEntry) bool {
|
|
206
|
+
return plugin.ProjectInputs
|
|
207
|
+
},
|
|
208
|
+
source.projectContextJSON,
|
|
209
|
+
) {
|
|
210
|
+
initialProjectInputs := plugin.InitialProjectInputs
|
|
211
|
+
if plugin.InitialProjectInputKey != "" {
|
|
212
|
+
snapshot, ok := manifest.InitialProjectInputs[plugin.InitialProjectInputKey]
|
|
213
|
+
if !ok {
|
|
214
|
+
return nil, fmt.Errorf(
|
|
215
|
+
"ttscserver: %s initial project inputs key %q is missing from the manifest",
|
|
216
|
+
pluginLabel(plugin),
|
|
217
|
+
plugin.InitialProjectInputKey,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
initialProjectInputs = &snapshot
|
|
221
|
+
}
|
|
222
|
+
if initialProjectInputs == nil {
|
|
223
|
+
missingInitialProjectInputs = true
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
snapshot, err := normalizeLSPProjectInputSnapshot(
|
|
227
|
+
*initialProjectInputs,
|
|
228
|
+
source.cwd,
|
|
229
|
+
)
|
|
230
|
+
if err != nil {
|
|
231
|
+
return nil, fmt.Errorf(
|
|
232
|
+
"ttscserver: %s initial project inputs are invalid: %w",
|
|
233
|
+
pluginLabel(plugin),
|
|
234
|
+
err,
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
if !projectInputReloadFingerprintsAreCurrent(snapshot) {
|
|
238
|
+
return nil, fmt.Errorf(
|
|
239
|
+
"ttscserver: %s project selection inputs changed during startup",
|
|
240
|
+
pluginLabel(plugin),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
source.storeProjectInputs(plugin, 1, snapshot)
|
|
244
|
+
}
|
|
245
|
+
if missingInitialProjectInputs {
|
|
246
|
+
source.discoverProjectInputs(1)
|
|
247
|
+
}
|
|
173
248
|
// The corpus fetch loads a Program, so it runs off the construction path.
|
|
174
249
|
// Blocking here would delay initialize — and therefore the editor's first
|
|
175
250
|
// response — for a feature most projects do not use. Until it lands,
|
|
@@ -188,8 +263,15 @@ func (s *NativePluginSource) Diagnostics(doc LSPDocumentVersion) LSPDiagnosticsR
|
|
|
188
263
|
if s == nil || doc.URI == "" {
|
|
189
264
|
return LSPDiagnosticsResult{}
|
|
190
265
|
}
|
|
191
|
-
|
|
192
|
-
|
|
266
|
+
generation := s.projectDiagnosticsSequence.Add(1)
|
|
267
|
+
out := LSPDiagnosticsResult{
|
|
268
|
+
projectUpdatedProducers: map[string]struct{}{},
|
|
269
|
+
}
|
|
270
|
+
for _, plugin := range selectPluginTransports(
|
|
271
|
+
s.plugins,
|
|
272
|
+
nil,
|
|
273
|
+
s.projectContextJSON,
|
|
274
|
+
) {
|
|
193
275
|
body, err := s.run(plugin, "lsp-diagnostics", "--uri="+doc.URI)
|
|
194
276
|
if err != nil {
|
|
195
277
|
s.log("%v", err)
|
|
@@ -201,23 +283,13 @@ func (s *NativePluginSource) Diagnostics(doc LSPDocumentVersion) LSPDiagnosticsR
|
|
|
201
283
|
continue
|
|
202
284
|
}
|
|
203
285
|
out.Document = append(out.Document, result.Document...)
|
|
204
|
-
if result.Project
|
|
205
|
-
|
|
286
|
+
if result.Project != nil && result.Project.URI != "" {
|
|
287
|
+
s.storeProjectDiagnostics(plugin, generation, result.Project)
|
|
288
|
+
out.projectUpdatedProducers[pluginKey(plugin, s.projectContextJSON)] = struct{}{}
|
|
206
289
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
out.Project = &copied
|
|
211
|
-
continue
|
|
212
|
-
}
|
|
213
|
-
if out.Project.URI != result.Project.URI {
|
|
214
|
-
s.log("ttscserver: %s returned project diagnostics for %s after %s; replacing the prior project publication", pluginLabel(plugin), result.Project.URI, out.Project.URI)
|
|
215
|
-
copied := *result.Project
|
|
216
|
-
copied.Diagnostics = append([]LSPDiagnostic(nil), result.Project.Diagnostics...)
|
|
217
|
-
out.Project = &copied
|
|
218
|
-
continue
|
|
219
|
-
}
|
|
220
|
-
out.Project.Diagnostics = append(out.Project.Diagnostics, result.Project.Diagnostics...)
|
|
290
|
+
}
|
|
291
|
+
if len(out.projectUpdatedProducers) != 0 {
|
|
292
|
+
out.Project = s.projectDiagnosticsSnapshot()
|
|
221
293
|
}
|
|
222
294
|
return out
|
|
223
295
|
}
|
|
@@ -242,7 +314,11 @@ func (s *NativePluginSource) CodeActions(uri string, rng LSPRange, ctx LSPCodeAc
|
|
|
242
314
|
rangeJSON, _ := json.Marshal(rng)
|
|
243
315
|
contextJSON, _ := json.Marshal(ctx)
|
|
244
316
|
var out []LSPCodeAction
|
|
245
|
-
for _, plugin := range
|
|
317
|
+
for _, plugin := range selectPluginTransports(
|
|
318
|
+
s.plugins,
|
|
319
|
+
nil,
|
|
320
|
+
s.projectContextJSON,
|
|
321
|
+
) {
|
|
246
322
|
body, err := s.run(
|
|
247
323
|
plugin,
|
|
248
324
|
"lsp-code-actions",
|
|
@@ -435,7 +511,11 @@ type completionHintRecord struct {
|
|
|
435
511
|
// so a fast producer's fresh corpus reaches the editor without waiting on a slow
|
|
436
512
|
// one, and a producer that failed keeps serving what it last published.
|
|
437
513
|
func (s *NativePluginSource) discoverCompletionHints(generation uint64) {
|
|
438
|
-
for _, plugin := range
|
|
514
|
+
for _, plugin := range selectPluginTransports(
|
|
515
|
+
s.plugins,
|
|
516
|
+
nil,
|
|
517
|
+
s.projectContextJSON,
|
|
518
|
+
) {
|
|
439
519
|
body, err := s.run(plugin, "lsp-hints")
|
|
440
520
|
if err != nil {
|
|
441
521
|
// Silent, unlike every other discovery failure here. A plugin that does
|
|
@@ -474,7 +554,7 @@ func (s *NativePluginSource) discoverCompletionHints(generation uint64) {
|
|
|
474
554
|
// being offered. An older generation is dropped: refresh cycles are scheduled by
|
|
475
555
|
// editor events, and the last event's answer is the one the user is waiting for.
|
|
476
556
|
func (s *NativePluginSource) storeCompletionHints(plugin NativeLSPPluginEntry, generation uint64, hints []LSPCompletionHint) {
|
|
477
|
-
key := pluginKey(plugin)
|
|
557
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
478
558
|
s.hintsMu.Lock()
|
|
479
559
|
defer s.hintsMu.Unlock()
|
|
480
560
|
if existing, ok := s.pluginHints[key]; ok && generation < existing.generation {
|
|
@@ -494,13 +574,12 @@ func (s *NativePluginSource) storeCompletionHints(plugin NativeLSPPluginEntry, g
|
|
|
494
574
|
// which a Go map's randomized range would not. The caller holds hintsMu.
|
|
495
575
|
func (s *NativePluginSource) flattenCompletionHintsLocked() []LSPCompletionHint {
|
|
496
576
|
hints := []LSPCompletionHint{}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
seen[key] = struct{}{}
|
|
577
|
+
for _, plugin := range selectPluginTransports(
|
|
578
|
+
s.plugins,
|
|
579
|
+
nil,
|
|
580
|
+
s.projectContextJSON,
|
|
581
|
+
) {
|
|
582
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
504
583
|
hints = append(hints, s.pluginHints[key].hints...)
|
|
505
584
|
}
|
|
506
585
|
return hints
|
|
@@ -518,12 +597,44 @@ func (s *NativePluginSource) notifyCompletionHintsObserver() {
|
|
|
518
597
|
}
|
|
519
598
|
}
|
|
520
599
|
|
|
521
|
-
// pluginKey identifies one
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
func pluginKey(
|
|
526
|
-
|
|
600
|
+
// pluginKey identifies one effective native launch transport. Every LSP verb
|
|
601
|
+
// receives the full plugin manifest and has no selected-entry argument, so two
|
|
602
|
+
// logical entries using the same binary and project-context argv return one
|
|
603
|
+
// aggregate result and must share one cache, owner scope, and resident daemon.
|
|
604
|
+
func pluginKey(
|
|
605
|
+
plugin NativeLSPPluginEntry,
|
|
606
|
+
projectContextJSON ...string,
|
|
607
|
+
) string {
|
|
608
|
+
projectContextArgs := "0"
|
|
609
|
+
if plugin.ProjectContextArgs &&
|
|
610
|
+
len(projectContextJSON) != 0 &&
|
|
611
|
+
strings.TrimSpace(projectContextJSON[0]) != "" {
|
|
612
|
+
projectContextArgs = "1"
|
|
613
|
+
}
|
|
614
|
+
return plugin.Binary + "\x00" + projectContextArgs
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// selectPluginTransports preserves manifest order while selecting at most one
|
|
618
|
+
// representative for each effective native launch identity.
|
|
619
|
+
func selectPluginTransports(
|
|
620
|
+
plugins []NativeLSPPluginEntry,
|
|
621
|
+
include func(NativeLSPPluginEntry) bool,
|
|
622
|
+
projectContextJSON ...string,
|
|
623
|
+
) []NativeLSPPluginEntry {
|
|
624
|
+
selected := make([]NativeLSPPluginEntry, 0, len(plugins))
|
|
625
|
+
seen := make(map[string]struct{}, len(plugins))
|
|
626
|
+
for _, plugin := range plugins {
|
|
627
|
+
if include != nil && !include(plugin) {
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
630
|
+
key := pluginKey(plugin, projectContextJSON...)
|
|
631
|
+
if _, duplicate := seen[key]; duplicate {
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
seen[key] = struct{}{}
|
|
635
|
+
selected = append(selected, plugin)
|
|
636
|
+
}
|
|
637
|
+
return selected
|
|
527
638
|
}
|
|
528
639
|
|
|
529
640
|
// decodeNativeCompletionHints accepts both generations of the lsp-hints wire.
|
|
@@ -616,7 +727,11 @@ func (s *NativePluginSource) CodeActionKinds() []string {
|
|
|
616
727
|
func (s *NativePluginSource) discoverCommandIDs() {
|
|
617
728
|
seen := map[string]struct{}{}
|
|
618
729
|
kindSeen := map[string]struct{}{}
|
|
619
|
-
for _, plugin := range
|
|
730
|
+
for _, plugin := range selectPluginTransports(
|
|
731
|
+
s.plugins,
|
|
732
|
+
nil,
|
|
733
|
+
s.projectContextJSON,
|
|
734
|
+
) {
|
|
620
735
|
body, err := s.run(plugin, "lsp-command-ids")
|
|
621
736
|
if err != nil {
|
|
622
737
|
s.log("%v", err)
|
|
@@ -670,7 +785,8 @@ func (s *NativePluginSource) pluginOwnsCommand(plugin NativeLSPPluginEntry, comm
|
|
|
670
785
|
if !ok {
|
|
671
786
|
return false
|
|
672
787
|
}
|
|
673
|
-
return owner
|
|
788
|
+
return pluginKey(owner, s.projectContextJSON) ==
|
|
789
|
+
pluginKey(plugin, s.projectContextJSON)
|
|
674
790
|
}
|
|
675
791
|
|
|
676
792
|
func (s *NativePluginSource) run(plugin NativeLSPPluginEntry, command string, args ...string) ([]byte, error) {
|
|
@@ -680,20 +796,20 @@ func (s *NativePluginSource) run(plugin NativeLSPPluginEntry, command string, ar
|
|
|
680
796
|
// spawn-per-verb path below with no behavior change. The static discovery
|
|
681
797
|
// verbs (lsp-command-ids / lsp-code-action-kinds) and lsp-execute-command stay
|
|
682
798
|
// on exec by design.
|
|
683
|
-
if command == serveVerbDiagnostics ||
|
|
799
|
+
if command == serveVerbDiagnostics ||
|
|
800
|
+
command == serveVerbCodeActions {
|
|
684
801
|
if body, served, err := s.serveRun(plugin, command, args); served {
|
|
685
802
|
return body, err
|
|
686
803
|
}
|
|
687
804
|
}
|
|
688
|
-
if command ==
|
|
689
|
-
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
// one extra spawn to fail there too, exactly as it did before.
|
|
805
|
+
if command == serveVerbProjectDiagnostics ||
|
|
806
|
+
command == serveVerbHints {
|
|
807
|
+
// These newer optional verbs join the daemon on a weaker condition than the
|
|
808
|
+
// original document reads. A staged sidecar may implement the direct verb
|
|
809
|
+
// while its older resident loop rejects it. A nonzero resident reply is
|
|
810
|
+
// indistinguishable from the verb itself failing, so retry once through the
|
|
811
|
+
// advertised direct command. A genuine failure pays one extra spawn and is
|
|
812
|
+
// then handled exactly as it was before lsp-serve.
|
|
697
813
|
if body, served, err := s.serveRun(plugin, command, args); served && err == nil {
|
|
698
814
|
return body, nil
|
|
699
815
|
}
|
|
@@ -18,15 +18,16 @@ import (
|
|
|
18
18
|
// the caller falls back to a fresh spawn.
|
|
19
19
|
const residentRequestTimeout = nativePluginCommandTimeout
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// The serve verbs below are routed to the resident daemon because they load a
|
|
22
|
+
// Program.
|
|
23
23
|
// lsp-command-ids / lsp-code-action-kinds run once at startup and never load one
|
|
24
24
|
// (routing them would spawn the daemon on the initialize path);
|
|
25
25
|
// lsp-execute-command is user-initiated and keeps its spawn-per-verb path.
|
|
26
26
|
const (
|
|
27
|
-
serveVerbDiagnostics
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
serveVerbDiagnostics = "lsp-diagnostics"
|
|
28
|
+
serveVerbProjectDiagnostics = "lsp-project-diagnostics"
|
|
29
|
+
serveVerbCodeActions = "lsp-code-actions"
|
|
30
|
+
serveVerbHints = "lsp-hints"
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
// serveClientRequest mirrors linthost's serveLSPRequest: the base project
|
|
@@ -38,6 +39,7 @@ type serveClientRequest struct {
|
|
|
38
39
|
ContextJSON string `json:"contextJson,omitempty"`
|
|
39
40
|
Invalidate bool `json:"invalidate,omitempty"`
|
|
40
41
|
Changed []string `json:"changed,omitempty"`
|
|
42
|
+
External []string `json:"external,omitempty"`
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// serveClientResponse mirrors linthost's serveLSPResponse: the verb's JSON
|
|
@@ -63,6 +65,9 @@ type residentSidecar struct {
|
|
|
63
65
|
// request, so the daemon updates the warm Program incrementally rather than
|
|
64
66
|
// rebuilding it.
|
|
65
67
|
changed []string
|
|
68
|
+
// external identifies changed entries that are declared ProjectRule inputs,
|
|
69
|
+
// allowing an unknown non-Program path to retain the warm Program.
|
|
70
|
+
external []string
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// serveRun routes a serve-able verb through the plugin's resident daemon.
|
|
@@ -75,18 +80,19 @@ func (s *NativePluginSource) serveRun(plugin NativeLSPPluginEntry, verb string,
|
|
|
75
80
|
if s == nil || strings.TrimSpace(plugin.Binary) == "" {
|
|
76
81
|
return nil, false, nil
|
|
77
82
|
}
|
|
83
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
78
84
|
s.residentMu.Lock()
|
|
79
|
-
if s.serveUnsupported[
|
|
85
|
+
if s.serveUnsupported[key] {
|
|
80
86
|
s.residentMu.Unlock()
|
|
81
87
|
return nil, false, nil
|
|
82
88
|
}
|
|
83
|
-
sc := s.residents[
|
|
89
|
+
sc := s.residents[key]
|
|
84
90
|
if sc == nil {
|
|
85
91
|
sc = &residentSidecar{}
|
|
86
92
|
if s.residents == nil {
|
|
87
93
|
s.residents = map[string]*residentSidecar{}
|
|
88
94
|
}
|
|
89
|
-
s.residents[
|
|
95
|
+
s.residents[key] = sc
|
|
90
96
|
}
|
|
91
97
|
s.residentMu.Unlock()
|
|
92
98
|
|
|
@@ -103,7 +109,7 @@ func (s *NativePluginSource) serveRun(plugin NativeLSPPluginEntry, verb string,
|
|
|
103
109
|
if s.serveUnsupported == nil {
|
|
104
110
|
s.serveUnsupported = map[string]bool{}
|
|
105
111
|
}
|
|
106
|
-
s.serveUnsupported[
|
|
112
|
+
s.serveUnsupported[key] = true
|
|
107
113
|
}
|
|
108
114
|
s.residentMu.Unlock()
|
|
109
115
|
return nil, false, nil
|
|
@@ -133,6 +139,10 @@ func (sc *residentSidecar) call(s *NativePluginSource, plugin NativeLSPPluginEnt
|
|
|
133
139
|
req.Changed = sc.changed
|
|
134
140
|
sc.changed = nil
|
|
135
141
|
}
|
|
142
|
+
if len(sc.external) > 0 {
|
|
143
|
+
req.External = sc.external
|
|
144
|
+
sc.external = nil
|
|
145
|
+
}
|
|
136
146
|
line, err := json.Marshal(req)
|
|
137
147
|
if err != nil {
|
|
138
148
|
return nil, 0, err
|
|
@@ -250,6 +260,97 @@ func (s *NativePluginSource) InvalidateResidentPrograms(changedURIs ...string) {
|
|
|
250
260
|
}
|
|
251
261
|
}
|
|
252
262
|
|
|
263
|
+
// InvalidateResidentProgramsForWatchedChanges distinguishes declared external
|
|
264
|
+
// inputs from ordinary watched files so the sidecar can retain its Program for
|
|
265
|
+
// data-only changes while still rebuilding fresh ProjectRule state.
|
|
266
|
+
func (s *NativePluginSource) InvalidateResidentProgramsForWatchedChanges(
|
|
267
|
+
changedURIs []string,
|
|
268
|
+
externalURIs []string,
|
|
269
|
+
) {
|
|
270
|
+
externalOwners := make(map[string][]string, len(externalURIs))
|
|
271
|
+
for _, uri := range externalURIs {
|
|
272
|
+
externalOwners[uri] = nil
|
|
273
|
+
}
|
|
274
|
+
s.InvalidateResidentProgramsForOwnedWatchedChanges(
|
|
275
|
+
changedURIs,
|
|
276
|
+
externalURIs,
|
|
277
|
+
externalOwners,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// InvalidateResidentProgramsForOwnedWatchedChanges sends data-only external
|
|
282
|
+
// changes only to resident binaries that own the matching snapshot. A path that
|
|
283
|
+
// can also belong to the Program reaches every resident, because each daemon
|
|
284
|
+
// must decide whether its own Program contains that source.
|
|
285
|
+
func (s *NativePluginSource) InvalidateResidentProgramsForOwnedWatchedChanges(
|
|
286
|
+
changedURIs []string,
|
|
287
|
+
externalURIs []string,
|
|
288
|
+
externalOwners map[string][]string,
|
|
289
|
+
) {
|
|
290
|
+
if s == nil {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
external := make(map[string]struct{}, len(externalURIs))
|
|
294
|
+
for _, uri := range externalURIs {
|
|
295
|
+
external[uri] = struct{}{}
|
|
296
|
+
}
|
|
297
|
+
ordinary := make([]string, 0, len(changedURIs))
|
|
298
|
+
for _, uri := range changedURIs {
|
|
299
|
+
if _, ok := external[uri]; !ok {
|
|
300
|
+
ordinary = append(ordinary, uri)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
ownerTransports := make(map[string]map[string]struct{}, len(externalURIs))
|
|
304
|
+
allTransports := make(map[string]bool, len(externalURIs))
|
|
305
|
+
pluginsByKey := make(map[string]NativeLSPPluginEntry, len(s.plugins))
|
|
306
|
+
for _, plugin := range s.plugins {
|
|
307
|
+
pluginsByKey[pluginKey(plugin, s.projectContextJSON)] = plugin
|
|
308
|
+
}
|
|
309
|
+
for _, uri := range externalURIs {
|
|
310
|
+
if watchedURIHasProgramInputExtension(uri) {
|
|
311
|
+
allTransports[uri] = true
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
owners, scoped := externalOwners[uri]
|
|
315
|
+
if !scoped || owners == nil {
|
|
316
|
+
allTransports[uri] = true
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
transports := map[string]struct{}{}
|
|
320
|
+
for _, owner := range owners {
|
|
321
|
+
if plugin, ok := pluginsByKey[owner]; ok {
|
|
322
|
+
transports[pluginKey(plugin, s.projectContextJSON)] = struct{}{}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
ownerTransports[uri] = transports
|
|
326
|
+
}
|
|
327
|
+
s.residentMu.Lock()
|
|
328
|
+
residents := make(map[string]*residentSidecar, len(s.residents))
|
|
329
|
+
for key, sc := range s.residents {
|
|
330
|
+
residents[key] = sc
|
|
331
|
+
}
|
|
332
|
+
s.residentMu.Unlock()
|
|
333
|
+
for key, sc := range residents {
|
|
334
|
+
changed := append([]string(nil), ordinary...)
|
|
335
|
+
selectedExternal := []string{}
|
|
336
|
+
for _, uri := range externalURIs {
|
|
337
|
+
_, owned := ownerTransports[uri][key]
|
|
338
|
+
if !allTransports[uri] && !owned {
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
changed = append(changed, uri)
|
|
342
|
+
selectedExternal = append(selectedExternal, uri)
|
|
343
|
+
}
|
|
344
|
+
if len(changed) == 0 {
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
sc.mu.Lock()
|
|
348
|
+
sc.changed = append(sc.changed, changed...)
|
|
349
|
+
sc.external = append(sc.external, selectedExternal...)
|
|
350
|
+
sc.mu.Unlock()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
253
354
|
// shutdownResidents kills every resident child. Called on server teardown; the
|
|
254
355
|
// children also exit on their own when the parent closes their stdin at process
|
|
255
356
|
// exit, so this is the graceful path, not the only one.
|
|
@@ -155,6 +155,12 @@ type LSPProjectDiagnostics struct {
|
|
|
155
155
|
type LSPDiagnosticsResult struct {
|
|
156
156
|
Document []LSPDiagnostic `json:"document"`
|
|
157
157
|
Project *LSPProjectDiagnostics `json:"project,omitempty"`
|
|
158
|
+
|
|
159
|
+
// projectUpdatedProducers names the sidecars that actually returned a
|
|
160
|
+
// project publication during this call. It is deliberately not serialized:
|
|
161
|
+
// the proxy uses it to distinguish a current computation from last-good
|
|
162
|
+
// aggregate state retained for another producer.
|
|
163
|
+
projectUpdatedProducers map[string]struct{}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
// PluginSource is the seam between the LSP proxy and ttsc's plugin
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestLSPProjectContextTransportIdentityTracksEffectiveArgv verifies resident
|
|
6
|
+
// identity follows the actual launch arguments, not a dormant capability flag.
|
|
7
|
+
func TestLSPProjectContextTransportIdentityTracksEffectiveArgv(t *testing.T) {
|
|
8
|
+
withoutContext := NativeLSPPluginEntry{
|
|
9
|
+
Binary: "ttsc-no-such-shared-sidecar",
|
|
10
|
+
Name: "@ttsc/without-context",
|
|
11
|
+
}
|
|
12
|
+
withContext := withoutContext
|
|
13
|
+
withContext.Name = "@ttsc/with-context"
|
|
14
|
+
withContext.ProjectContextArgs = true
|
|
15
|
+
|
|
16
|
+
if pluginKey(withoutContext) != pluginKey(withContext) {
|
|
17
|
+
t.Fatal("an absent project context split identical launch transports")
|
|
18
|
+
}
|
|
19
|
+
projectContextJSON := `{"physicalProjectRoot":"/project"}`
|
|
20
|
+
if pluginKey(withoutContext, projectContextJSON) ==
|
|
21
|
+
pluginKey(withContext, projectContextJSON) {
|
|
22
|
+
t.Fatal("effective project-context argv did not distinguish transports")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
source := &NativePluginSource{
|
|
26
|
+
cwd: t.TempDir(),
|
|
27
|
+
pluginsJSON: "[]",
|
|
28
|
+
projectContextJSON: projectContextJSON,
|
|
29
|
+
}
|
|
30
|
+
_, _, _ = source.serveRun(
|
|
31
|
+
withoutContext,
|
|
32
|
+
serveVerbDiagnostics,
|
|
33
|
+
[]string{"--uri=file:///project/a.ts"},
|
|
34
|
+
)
|
|
35
|
+
_, _, _ = source.serveRun(
|
|
36
|
+
withContext,
|
|
37
|
+
serveVerbDiagnostics,
|
|
38
|
+
[]string{"--uri=file:///project/a.ts"},
|
|
39
|
+
)
|
|
40
|
+
if len(source.residents) != 2 {
|
|
41
|
+
t.Fatalf("differing launch argv shared %d resident sessions", len(source.residents))
|
|
42
|
+
}
|
|
43
|
+
}
|