ttsc 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/cmd/ttscserver/main.go +79 -3
  2. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
  3. package/driver/lsp.go +4 -0
  4. package/go.mod +1 -1
  5. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  6. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  7. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  8. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  9. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  10. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  11. package/internal/lspserver/lsp_native_resident.go +110 -9
  12. package/internal/lspserver/lsp_plugin.go +6 -0
  13. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  14. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  15. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  16. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  18. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  19. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  20. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  21. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  22. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  23. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  24. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  25. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  26. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  27. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  28. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  29. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  30. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  31. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  32. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  33. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  34. package/internal/lspserver/lsp_project_inputs.go +1012 -0
  35. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  36. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  37. package/internal/lspserver/lsp_proxy.go +591 -28
  38. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  39. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  40. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  41. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  42. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  43. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  44. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  45. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  46. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  47. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  48. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  49. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  50. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  51. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  52. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  53. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  54. package/lib/compiler/internal/runBuild.d.ts +64 -1
  55. package/lib/compiler/internal/runBuild.js +484 -7
  56. package/lib/compiler/internal/runBuild.js.map +1 -1
  57. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  58. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  59. package/lib/flags/parser.js +13 -17
  60. package/lib/flags/parser.js.map +1 -1
  61. package/lib/flags/schema.d.ts +10 -3
  62. package/lib/flags/schema.js +99 -0
  63. package/lib/flags/schema.js.map +1 -1
  64. package/lib/internal/projectInputPathIdentity.d.ts +31 -0
  65. package/lib/internal/projectInputPathIdentity.js +276 -0
  66. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  67. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  68. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  69. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  70. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  71. package/lib/launcher/internal/runTtsc.js +126 -88
  72. package/lib/launcher/internal/runTtsc.js.map +1 -1
  73. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  74. package/lib/launcher/internal/runTtscserver.js +468 -47
  75. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  76. package/lib/launcher/internal/runTtsx.js +61 -7
  77. package/lib/launcher/internal/runTtsx.js.map +1 -1
  78. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  79. package/lib/launcher/internal/runtimeHooks.js +7 -15
  80. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  81. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  82. package/lib/launcher/internal/singleFileOutput.js +103 -0
  83. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  84. package/lib/launcher/internal/watchTopology.d.ts +288 -2
  85. package/lib/launcher/internal/watchTopology.js +1921 -54
  86. package/lib/launcher/internal/watchTopology.js.map +1 -1
  87. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  88. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  89. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  90. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  91. package/lib/plugin/internal/loadProjectPlugins.js +34 -8
  92. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  93. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  94. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  95. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  96. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  97. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  98. package/package.json +12 -8
  99. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  100. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  101. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  102. package/src/compiler/internal/runBuild.ts +728 -12
  103. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  104. package/src/flags/parser.ts +17 -17
  105. package/src/flags/schema.ts +112 -3
  106. package/src/internal/projectInputPathIdentity.ts +377 -0
  107. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  108. package/src/launcher/internal/runTtsc.ts +148 -117
  109. package/src/launcher/internal/runTtscserver.ts +633 -56
  110. package/src/launcher/internal/runTtsx.ts +87 -10
  111. package/src/launcher/internal/runtimeHooks.ts +16 -16
  112. package/src/launcher/internal/singleFileOutput.ts +133 -0
  113. package/src/launcher/internal/watchTopology.ts +2494 -74
  114. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  115. package/src/plugin/internal/loadProjectPlugins.ts +49 -8
  116. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  117. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  118. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
  119. package/tools/shim_audit/baseline.json +0 -9
@@ -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
- // passes through TTSC_LSP_PLUGINS_JSON after running normal project plugin
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
- Plugins []NativePluginConfigEntry `json:"plugins"`
25
- LSPPlugins []NativeLSPPluginEntry `json:"lspPlugins"`
26
- ProjectContext json.RawMessage `json:"projectContext,omitempty"`
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 string `json:"binary"`
41
- Name string `json:"name,omitempty"`
42
- ProjectContextArgs bool `json:"projectContextArgs,omitempty"`
43
- Stage string `json:"stage,omitempty"`
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
- return nil, fmt.Errorf("ttscserver: invalid TTSC_LSP_PLUGINS_JSON: %w", err)
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
- out := LSPDiagnosticsResult{}
192
- for _, plugin := range s.plugins {
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 == nil {
205
- continue
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
- if out.Project == nil {
208
- copied := *result.Project
209
- copied.Diagnostics = append([]LSPDiagnostic(nil), result.Project.Diagnostics...)
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 s.plugins {
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 s.plugins {
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
- seen := make(map[string]struct{}, len(s.plugins))
498
- for _, plugin := range s.plugins {
499
- key := pluginKey(plugin)
500
- if _, duplicate := seen[key]; duplicate {
501
- continue
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 manifest entry. It matches the identity
522
- // pluginOwnsCommand compares, so a manifest carrying the same binary under two
523
- // stages keeps two independent corpora instead of overwriting one with the
524
- // other.
525
- func pluginKey(plugin NativeLSPPluginEntry) string {
526
- return plugin.Binary + "\x00" + plugin.Name + "\x00" + plugin.Stage
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 s.plugins {
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.Binary == plugin.Binary && owner.Name == plugin.Name && owner.Stage == plugin.Stage
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 || command == serveVerbCodeActions {
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 == serveVerbHints {
689
- // Hints join the daemon on a weaker condition than the other two: the
690
- // answer is used only when it arrives without error. A sidecar built after
691
- // the daemon landed but before this verb joined it answers lsp-serve and
692
- // rejects lsp-hints as an unknown verb, which reaches this side as a
693
- // nonzero code — indistinguishable over this protocol from a rule that
694
- // failed. Falling back to the spawn path on any nonzero reply keeps that
695
- // sidecar's corpus working, and a project whose hints genuinely fail pays
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
- // serveVerbDiagnostics, serveVerbCodeActions, and serveVerbHints are the verbs
22
- // routed to the resident daemon: the three that load a Program.
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 = "lsp-diagnostics"
28
- serveVerbCodeActions = "lsp-code-actions"
29
- serveVerbHints = "lsp-hints"
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[plugin.Binary] {
85
+ if s.serveUnsupported[key] {
80
86
  s.residentMu.Unlock()
81
87
  return nil, false, nil
82
88
  }
83
- sc := s.residents[plugin.Binary]
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[plugin.Binary] = sc
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[plugin.Binary] = true
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
+ }