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.
Files changed (95) 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/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  5. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  6. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  7. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  8. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  9. package/internal/lspserver/lsp_native_resident.go +110 -9
  10. package/internal/lspserver/lsp_plugin.go +6 -0
  11. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  12. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  13. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  14. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  15. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  16. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  18. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  19. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  20. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  21. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  22. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  23. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  24. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  25. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  26. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  27. package/internal/lspserver/lsp_project_inputs.go +944 -0
  28. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  29. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  30. package/internal/lspserver/lsp_proxy.go +591 -28
  31. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  32. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  33. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  34. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  35. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  36. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  37. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  38. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  39. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  40. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  41. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  42. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  43. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  44. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  45. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  46. package/lib/compiler/internal/runBuild.d.ts +64 -1
  47. package/lib/compiler/internal/runBuild.js +484 -7
  48. package/lib/compiler/internal/runBuild.js.map +1 -1
  49. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  50. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  51. package/lib/flags/parser.js +13 -17
  52. package/lib/flags/parser.js.map +1 -1
  53. package/lib/flags/schema.d.ts +10 -3
  54. package/lib/flags/schema.js +99 -0
  55. package/lib/flags/schema.js.map +1 -1
  56. package/lib/internal/projectInputPathIdentity.d.ts +23 -0
  57. package/lib/internal/projectInputPathIdentity.js +222 -0
  58. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  59. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  60. package/lib/launcher/internal/runTtsc.js +121 -88
  61. package/lib/launcher/internal/runTtsc.js.map +1 -1
  62. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  63. package/lib/launcher/internal/runTtscserver.js +415 -45
  64. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  65. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  66. package/lib/launcher/internal/singleFileOutput.js +103 -0
  67. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  68. package/lib/launcher/internal/watchTopology.d.ts +214 -2
  69. package/lib/launcher/internal/watchTopology.js +1560 -41
  70. package/lib/launcher/internal/watchTopology.js.map +1 -1
  71. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  72. package/lib/plugin/internal/loadProjectPlugins.js +5 -0
  73. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  74. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  75. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  76. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  77. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  78. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  79. package/package.json +8 -8
  80. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  81. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  82. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  83. package/src/compiler/internal/runBuild.ts +728 -12
  84. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  85. package/src/flags/parser.ts +17 -17
  86. package/src/flags/schema.ts +111 -3
  87. package/src/internal/projectInputPathIdentity.ts +273 -0
  88. package/src/launcher/internal/runTtsc.ts +141 -117
  89. package/src/launcher/internal/runTtscserver.ts +561 -54
  90. package/src/launcher/internal/singleFileOutput.ts +133 -0
  91. package/src/launcher/internal/watchTopology.ts +2008 -59
  92. package/src/plugin/internal/loadProjectPlugins.ts +8 -0
  93. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  94. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  95. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
@@ -10,9 +10,11 @@ import (
10
10
  "net/url"
11
11
  "os"
12
12
  "path/filepath"
13
+ "sort"
13
14
  "strings"
14
15
  "sync"
15
16
  "sync/atomic"
17
+ "time"
16
18
  "unicode/utf16"
17
19
  "unicode/utf8"
18
20
  )
@@ -24,22 +26,32 @@ import (
24
26
  // fallback.
25
27
  var ErrCommandNotHandled = errors.New("lsp: command not handled by ttsc")
26
28
 
29
+ // ErrLSPPluginSelectionChanged ends the current native host after a watched
30
+ // config dependency changes which source plugins the JavaScript launcher must
31
+ // rebuild. A fresh editor-managed process reruns descriptor discovery before it
32
+ // creates the next LSP stream.
33
+ var ErrLSPPluginSelectionChanged = errors.New(
34
+ "ttscserver: plugin selection input changed; restart required",
35
+ )
36
+
27
37
  const (
28
- methodPublishDiagnostics = "textDocument/publishDiagnostics"
29
- methodInitialize = "initialize"
30
- methodDidOpen = "textDocument/didOpen"
31
- methodDidChange = "textDocument/didChange"
32
- methodDidSave = "textDocument/didSave"
33
- methodDidClose = "textDocument/didClose"
34
- methodCodeAction = "textDocument/codeAction"
35
- methodExecuteCommand = "workspace/executeCommand"
36
- methodCancelRequest = "$/cancelRequest"
37
- methodFormatting = "textDocument/formatting"
38
- methodDocumentSymbol = "textDocument/documentSymbol"
39
- methodReferences = "textDocument/references"
40
- methodCompletion = "textDocument/completion"
41
- methodCompletionResolve = "completionItem/resolve"
42
- methodExit = "exit"
38
+ methodPublishDiagnostics = "textDocument/publishDiagnostics"
39
+ methodInitialize = "initialize"
40
+ methodDidOpen = "textDocument/didOpen"
41
+ methodDidChange = "textDocument/didChange"
42
+ methodDidSave = "textDocument/didSave"
43
+ methodDidClose = "textDocument/didClose"
44
+ methodCodeAction = "textDocument/codeAction"
45
+ methodExecuteCommand = "workspace/executeCommand"
46
+ methodCancelRequest = "$/cancelRequest"
47
+ methodFormatting = "textDocument/formatting"
48
+ methodDocumentSymbol = "textDocument/documentSymbol"
49
+ methodReferences = "textDocument/references"
50
+ methodCompletion = "textDocument/completion"
51
+ methodCompletionResolve = "completionItem/resolve"
52
+ methodInitialized = "initialized"
53
+ methodExit = "exit"
54
+ methodPluginSelectionChanged = "ttsc/pluginSelectionChanged"
43
55
 
44
56
  // methodDidChangeWatchedFiles is the only notification an editor sends for a
45
57
  // file it does not have open: a tsconfig edit, a generated file, a branch
@@ -138,7 +150,9 @@ type Proxy struct {
138
150
  pendingAugmentingActions map[string]struct{}
139
151
  pendingLocalActions map[string]struct{}
140
152
  pendingCommands map[string]struct{}
153
+ pendingClientRequests map[string]func(Envelope)
141
154
  pendingInitialize map[string]struct{}
155
+ clientRequestSequence atomic.Uint64
142
156
 
143
157
  capabilityMu sync.Mutex
144
158
  upstreamCodeActionProvider bool
@@ -153,6 +167,8 @@ type Proxy struct {
153
167
  // without a restart, so the proxy says so once per character.
154
168
  advertisedCompletionTriggers map[string]struct{}
155
169
  reportedCompletionTriggers map[string]struct{}
170
+ projectInputWatchDynamic bool
171
+ projectInputWatchRelative bool
156
172
 
157
173
  diagnosticsMu sync.Mutex
158
174
  upstreamDiagnostics map[string]cachedDiagnostics
@@ -174,6 +190,25 @@ type Proxy struct {
174
190
  // handler falls back to a disk read. Guarded by diagnosticsMu like the
175
191
  // other per-uri document state.
176
192
  documentText map[string]string
193
+
194
+ projectRefreshMu sync.Mutex
195
+ projectRefreshTimer *time.Timer
196
+ projectDiagnosticsRefresh coalescingRefresh
197
+ pendingProjectDiagnosticGeneration uint64
198
+ projectDiagnosticRefreshPending bool
199
+ pendingProjectDiagnosticAllOwners bool
200
+ pendingProjectDiagnosticOwners map[string]struct{}
201
+
202
+ projectInputWatchMu sync.Mutex
203
+ projectInputWatchReady bool
204
+ projectInputWatchDesired projectInputWatchRegistration
205
+ projectInputWatchActive projectInputWatchRegistration
206
+ projectInputWatchStaleIDs []string
207
+ projectInputWatchPending bool
208
+ projectInputWatchFailedSignature string
209
+ projectInputWatchUnregisterRetryBlocked bool
210
+ projectInputWatchWarningSent bool
211
+ projectInputWatchRegistrationSequence uint64
177
212
  }
178
213
 
179
214
  type pendingCodeActionRequest struct {
@@ -217,6 +252,7 @@ func NewProxy(opts ProxyOptions) *Proxy {
217
252
  pendingAugmentingActions: make(map[string]struct{}),
218
253
  pendingLocalActions: make(map[string]struct{}),
219
254
  pendingCommands: make(map[string]struct{}),
255
+ pendingClientRequests: make(map[string]func(Envelope)),
220
256
  pendingInitialize: make(map[string]struct{}),
221
257
  upstreamCodeActionProvider: true,
222
258
  upstreamDiagnostics: make(map[string]cachedDiagnostics),
@@ -236,6 +272,14 @@ func NewProxy(opts ProxyOptions) *Proxy {
236
272
  if observed, ok := source.(completionHintObserverSource); ok {
237
273
  observed.SetCompletionHintsObserver(proxy.completionHintsRefreshed)
238
274
  }
275
+ type projectInputObserverSource interface {
276
+ SetProjectInputsObserver(func())
277
+ }
278
+ if observed, ok := source.(projectInputObserverSource); ok {
279
+ observed.SetProjectInputsObserver(func() {
280
+ go proxy.projectInputsRefreshed()
281
+ })
282
+ }
239
283
  return proxy
240
284
  }
241
285
 
@@ -245,6 +289,7 @@ func NewProxy(opts ProxyOptions) *Proxy {
245
289
  // pipe write fails. ErrFrameClosed and context.Canceled are folded into
246
290
  // a nil result so editor shutdown does not look like a crash.
247
291
  func (p *Proxy) Run(ctx context.Context) error {
292
+ defer p.stopProjectDiagnosticRefresh()
248
293
  errCh := make(chan error, 2)
249
294
  go func() { errCh <- p.pumpEditorToUpstream(ctx) }()
250
295
  go func() { errCh <- p.pumpUpstreamToEditor(ctx) }()
@@ -308,6 +353,9 @@ func (p *Proxy) pumpEditorToUpstream(_ context.Context) error {
308
353
  if err := p.writeUpstreamFrame(constrainInitializePositionEncoding(env, body)); err != nil {
309
354
  return err
310
355
  }
356
+ if env.IsNotification() && env.Method == methodInitialized {
357
+ go p.projectInputWatchInitialized()
358
+ }
311
359
  }
312
360
  }
313
361
 
@@ -324,6 +372,9 @@ func (p *Proxy) closeUpstreamInput() {
324
372
  // handleEditorEnvelope returns true if the envelope was fully processed
325
373
  // locally (responded to without forwarding upstream).
326
374
  func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
375
+ if env.IsResponse() && p.handleClientRequestResponse(env) {
376
+ return true, nil
377
+ }
327
378
  switch env.Method {
328
379
  case methodInitialize:
329
380
  if env.IsRequest() {
@@ -342,6 +393,7 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
342
393
  p.invalidateResidentPlugins(env)
343
394
  p.refreshPluginCompletionHints()
344
395
  p.publishPluginDiagnosticsForDocumentNotification(env)
396
+ p.resumePendingProjectDiagnosticRefresh()
345
397
  }
346
398
  case methodDidChangeConfiguration:
347
399
  if env.IsNotification() {
@@ -350,6 +402,7 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
350
402
  // invalidateForWatchedFileChanges, which knows whether anything actually
351
403
  // changed.
352
404
  p.refreshPluginCompletionHints()
405
+ p.refreshProjectInputs()
353
406
  }
354
407
  case methodDidChange:
355
408
  if env.IsNotification() {
@@ -369,6 +422,7 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
369
422
  if env.IsNotification() {
370
423
  p.evictDocumentText(env)
371
424
  p.clearDocumentDiagnostics(env)
425
+ p.resumePendingProjectDiagnosticRefresh()
372
426
  }
373
427
  case methodDidChangeWatchedFiles:
374
428
  if env.IsNotification() {
@@ -537,6 +591,24 @@ func (p *Proxy) rememberInitializeRequest(env Envelope) {
537
591
  if key == "" {
538
592
  return
539
593
  }
594
+ var params struct {
595
+ Capabilities struct {
596
+ Workspace struct {
597
+ DidChangeWatchedFiles struct {
598
+ DynamicRegistration bool `json:"dynamicRegistration"`
599
+ RelativePatternSupport bool `json:"relativePatternSupport"`
600
+ } `json:"didChangeWatchedFiles"`
601
+ } `json:"workspace"`
602
+ } `json:"capabilities"`
603
+ }
604
+ if json.Unmarshal(env.Params, &params) == nil {
605
+ p.capabilityMu.Lock()
606
+ p.projectInputWatchDynamic =
607
+ params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration
608
+ p.projectInputWatchRelative =
609
+ params.Capabilities.Workspace.DidChangeWatchedFiles.RelativePatternSupport
610
+ p.capabilityMu.Unlock()
611
+ }
540
612
  p.pendingMu.Lock()
541
613
  defer p.pendingMu.Unlock()
542
614
  p.pendingInitialize[key] = struct{}{}
@@ -1260,8 +1332,20 @@ func (p *Proxy) publishMergedPluginDiagnostics(uri string, version *int, adoptCa
1260
1332
  if ok {
1261
1333
  p.reportAsyncError(p.writePublishDiagnosticsIfCurrent(uri, version, merged, generation))
1262
1334
  }
1263
- if diagnostics.Project != nil {
1264
- p.reportAsyncError(p.writeProjectDiagnosticsIfCurrent(diagnostics.Project, projectGeneration))
1335
+ if diagnostics.Project != nil && !p.hasDirtyDocuments() {
1336
+ writeResult, err := p.writeProjectDiagnosticsIfCurrent(
1337
+ diagnostics.Project,
1338
+ projectGeneration,
1339
+ false,
1340
+ )
1341
+ p.reportAsyncError(err)
1342
+ if writeResult.frameWritten &&
1343
+ p.recordPendingProjectDiagnosticOwnersRefreshed(
1344
+ projectGeneration,
1345
+ diagnostics.projectUpdatedProducers,
1346
+ ) {
1347
+ p.completePendingProjectDiagnosticRefresh(projectGeneration)
1348
+ }
1265
1349
  }
1266
1350
  }
1267
1351
 
@@ -1283,9 +1367,13 @@ func (p *Proxy) writePublishDiagnosticsIfCurrent(uri string, version *int, diagn
1283
1367
  return WriteFrame(p.editorOut, body)
1284
1368
  }
1285
1369
 
1286
- func (p *Proxy) writeProjectDiagnosticsIfCurrent(publication *LSPProjectDiagnostics, generation uint64) error {
1370
+ func (p *Proxy) writeProjectDiagnosticsIfCurrent(
1371
+ publication *LSPProjectDiagnostics,
1372
+ generation uint64,
1373
+ publishEmpty bool,
1374
+ ) (projectDiagnosticsWriteResult, error) {
1287
1375
  if publication == nil || publication.URI == "" {
1288
- return nil
1376
+ return projectDiagnosticsWriteResult{}, nil
1289
1377
  }
1290
1378
  diagnostics := append([]LSPDiagnostic(nil), publication.Diagnostics...)
1291
1379
  for index := range diagnostics {
@@ -1296,8 +1384,9 @@ func (p *Proxy) writeProjectDiagnosticsIfCurrent(publication *LSPProjectDiagnost
1296
1384
  defer p.writeMu.Unlock()
1297
1385
  p.diagnosticsMu.Lock()
1298
1386
  defer p.diagnosticsMu.Unlock()
1299
- if p.projectDiagnosticGeneration != generation {
1300
- return nil
1387
+ if p.projectDiagnosticGeneration != generation ||
1388
+ len(p.dirtyDocuments) != 0 {
1389
+ return projectDiagnosticsWriteResult{}, nil
1301
1390
  }
1302
1391
 
1303
1392
  previousURI := p.projectDiagnosticsURI
@@ -1310,19 +1399,33 @@ func (p *Proxy) writeProjectDiagnosticsIfCurrent(publication *LSPProjectDiagnost
1310
1399
  p.editorOut,
1311
1400
  p.publishDiagnosticsBody(previousURI, nil, p.mergedDiagnosticsLocked(previousURI)),
1312
1401
  ); err != nil {
1313
- return err
1402
+ return projectDiagnosticsWriteResult{}, err
1314
1403
  }
1315
1404
  }
1316
1405
 
1317
1406
  p.projectDiagnosticsURI = publication.URI
1318
1407
  p.projectDiagnostics = cachedDiagnostics{diagnostics: copyRawDiagnostics(rawDiagnostics)}
1319
- if len(rawDiagnostics) == 0 && !previousHadDiagnostics && !changedURI {
1320
- return nil
1408
+ if len(rawDiagnostics) == 0 &&
1409
+ !publishEmpty &&
1410
+ !previousHadDiagnostics &&
1411
+ !changedURI {
1412
+ return projectDiagnosticsWriteResult{accepted: true}, nil
1321
1413
  }
1322
- return WriteFrame(
1414
+ if err := WriteFrame(
1323
1415
  p.editorOut,
1324
1416
  p.publishDiagnosticsBody(publication.URI, nil, p.mergedDiagnosticsLocked(publication.URI)),
1325
- )
1417
+ ); err != nil {
1418
+ return projectDiagnosticsWriteResult{}, err
1419
+ }
1420
+ return projectDiagnosticsWriteResult{
1421
+ accepted: true,
1422
+ frameWritten: true,
1423
+ }, nil
1424
+ }
1425
+
1426
+ type projectDiagnosticsWriteResult struct {
1427
+ accepted bool
1428
+ frameWritten bool
1326
1429
  }
1327
1430
 
1328
1431
  func marshalLSPDiagnostics(diagnostics []LSPDiagnostic) []json.RawMessage {
@@ -2255,6 +2358,44 @@ func (p *Proxy) invalidateResidentPluginsForURIs(uris ...string) {
2255
2358
  }
2256
2359
  }
2257
2360
 
2361
+ type watchedResidentInvalidator interface {
2362
+ InvalidateResidentProgramsForWatchedChanges(
2363
+ changedURIs []string,
2364
+ externalURIs []string,
2365
+ )
2366
+ }
2367
+
2368
+ type ownedWatchedResidentInvalidator interface {
2369
+ InvalidateResidentProgramsForOwnedWatchedChanges(
2370
+ changedURIs []string,
2371
+ externalURIs []string,
2372
+ externalOwners map[string][]string,
2373
+ )
2374
+ }
2375
+
2376
+ func (p *Proxy) invalidateResidentPluginsForWatchedChanges(
2377
+ changedURIs []string,
2378
+ externalURIs []string,
2379
+ externalOwners map[string][]string,
2380
+ ) {
2381
+ if source, ok := p.source.(ownedWatchedResidentInvalidator); ok {
2382
+ source.InvalidateResidentProgramsForOwnedWatchedChanges(
2383
+ changedURIs,
2384
+ externalURIs,
2385
+ externalOwners,
2386
+ )
2387
+ return
2388
+ }
2389
+ if source, ok := p.source.(watchedResidentInvalidator); ok {
2390
+ source.InvalidateResidentProgramsForWatchedChanges(
2391
+ changedURIs,
2392
+ externalURIs,
2393
+ )
2394
+ return
2395
+ }
2396
+ p.invalidateResidentPluginsForURIs(changedURIs...)
2397
+ }
2398
+
2258
2399
  // LSP FileChangeType. Only `changed` names an edit to a file whose place in the
2259
2400
  // project is unchanged; created and deleted both reshape the root set, which a
2260
2401
  // per-file UpdateProgram cannot express.
@@ -2302,13 +2443,51 @@ func (p *Proxy) invalidateForWatchedFileChanges(env Envelope) error {
2302
2443
  // per plugin, each loading its own Program.
2303
2444
  return nil
2304
2445
  }
2446
+ for _, change := range params.Changes {
2447
+ if p.projectInputReloadMatchesChange(change.URI, change.Type) {
2448
+ if err := p.writePluginSelectionChanged(); err != nil {
2449
+ return err
2450
+ }
2451
+ return ErrLSPPluginSelectionChanged
2452
+ }
2453
+ }
2305
2454
  uris := make([]string, 0, len(params.Changes))
2455
+ externalURIs := make([]string, 0, len(params.Changes))
2456
+ externalSet := make(map[string]struct{}, len(params.Changes))
2457
+ externalOwners := make(map[string][]string, len(params.Changes))
2458
+ affectedProjectDiagnosticOwners := map[string]struct{}{}
2459
+ allProjectDiagnosticOwners := false
2306
2460
  deleted := make([]string, 0, len(params.Changes))
2307
2461
  localizable := true
2308
2462
  for _, change := range params.Changes {
2309
2463
  if change.URI != "" && change.Type != nil && *change.Type == fileChangeTypeDeleted {
2310
2464
  deleted = append(deleted, change.URI)
2311
2465
  }
2466
+ ownerScope, matched := p.projectInputOwnerScope(change.URI)
2467
+ if matched {
2468
+ ownerScope = projectDiagnosticScopeForWatchedInput(
2469
+ change.URI,
2470
+ ownerScope,
2471
+ )
2472
+ externalSet[change.URI] = struct{}{}
2473
+ if ownerScope.all {
2474
+ allProjectDiagnosticOwners = true
2475
+ externalOwners[change.URI] = nil
2476
+ } else {
2477
+ owners := ownerScope.list()
2478
+ externalOwners[change.URI] = owners
2479
+ for _, owner := range owners {
2480
+ affectedProjectDiagnosticOwners[owner] = struct{}{}
2481
+ }
2482
+ }
2483
+ if externalWatchedChangeRetainsProgram(change.URI, change.Type) {
2484
+ externalURIs = append(externalURIs, change.URI)
2485
+ uris = append(uris, change.URI)
2486
+ } else {
2487
+ localizable = false
2488
+ }
2489
+ continue
2490
+ }
2312
2491
  if !watchedFileChangeIsLocalizable(change.URI, change.Type) {
2313
2492
  localizable = false
2314
2493
  continue
@@ -2319,11 +2498,24 @@ func (p *Proxy) invalidateForWatchedFileChanges(env Envelope) error {
2319
2498
  if !localizable || len(uris) == 0 {
2320
2499
  p.invalidateResidentPluginsForURIs()
2321
2500
  } else {
2322
- p.invalidateResidentPluginsForURIs(uris...)
2501
+ p.invalidateResidentPluginsForWatchedChanges(
2502
+ uris,
2503
+ externalURIs,
2504
+ externalOwners,
2505
+ )
2323
2506
  }
2324
2507
  // A completion corpus projects the whole Program, so it cannot be invalidated
2325
2508
  // per URI the way the resident daemon's is: any reported change can change it.
2326
- p.refreshPluginCompletionHints()
2509
+ if len(externalSet) > 0 || watchedChangesMayAffectProgram(params.Changes) {
2510
+ p.refreshPluginCompletionHints()
2511
+ }
2512
+ if len(externalSet) > 0 {
2513
+ p.refreshProjectInputs()
2514
+ p.scheduleProjectDiagnosticRefresh(projectDiagnosticOwnerScope{
2515
+ all: allProjectDiagnosticOwners,
2516
+ owners: affectedProjectDiagnosticOwners,
2517
+ })
2518
+ }
2327
2519
  for _, uri := range deleted {
2328
2520
  if err := p.withdrawPluginDiagnosticsForDeletedDocument(uri); err != nil {
2329
2521
  return err
@@ -2375,6 +2567,377 @@ func watchedFileChangeIsLocalizable(uri string, changeType *int) bool {
2375
2567
  }
2376
2568
  }
2377
2569
 
2570
+ type projectInputMatcher interface {
2571
+ ProjectInputMatchesURI(string) bool
2572
+ }
2573
+
2574
+ type projectInputReloadMatcher interface {
2575
+ ProjectInputReloadMatchesURI(string) bool
2576
+ }
2577
+
2578
+ type projectInputReloadChangeMatcher interface {
2579
+ ProjectInputReloadMatchesChange(string, *int) bool
2580
+ }
2581
+
2582
+ type projectInputOwnerMatcher interface {
2583
+ ProjectInputOwnersForURI(string) []string
2584
+ }
2585
+
2586
+ type projectDiagnosticOwnerScope struct {
2587
+ all bool
2588
+ owners map[string]struct{}
2589
+ }
2590
+
2591
+ func projectDiagnosticScopeForWatchedInput(
2592
+ uri string,
2593
+ scope projectDiagnosticOwnerScope,
2594
+ ) projectDiagnosticOwnerScope {
2595
+ if watchedURIHasProgramInputExtension(uri) {
2596
+ return projectDiagnosticOwnerScope{all: true}
2597
+ }
2598
+ return scope
2599
+ }
2600
+
2601
+ func (scope projectDiagnosticOwnerScope) list() []string {
2602
+ owners := make([]string, 0, len(scope.owners))
2603
+ for owner := range scope.owners {
2604
+ owners = append(owners, owner)
2605
+ }
2606
+ sort.Strings(owners)
2607
+ return owners
2608
+ }
2609
+
2610
+ func (p *Proxy) projectInputOwnerScope(
2611
+ uri string,
2612
+ ) (projectDiagnosticOwnerScope, bool) {
2613
+ if source, ok := p.source.(projectInputOwnerMatcher); ok {
2614
+ owners := source.ProjectInputOwnersForURI(uri)
2615
+ if len(owners) != 0 {
2616
+ scope := projectDiagnosticOwnerScope{owners: map[string]struct{}{}}
2617
+ for _, owner := range owners {
2618
+ scope.owners[owner] = struct{}{}
2619
+ }
2620
+ return scope, true
2621
+ }
2622
+ }
2623
+ source, ok := p.source.(projectInputMatcher)
2624
+ if !ok || !source.ProjectInputMatchesURI(uri) {
2625
+ return projectDiagnosticOwnerScope{}, false
2626
+ }
2627
+ return projectDiagnosticOwnerScope{all: true}, true
2628
+ }
2629
+
2630
+ func (p *Proxy) projectInputReloadMatchesChange(
2631
+ uri string,
2632
+ changeType *int,
2633
+ ) bool {
2634
+ if source, ok := p.source.(projectInputReloadChangeMatcher); ok {
2635
+ return source.ProjectInputReloadMatchesChange(uri, changeType)
2636
+ }
2637
+ source, ok := p.source.(projectInputReloadMatcher)
2638
+ return ok && source.ProjectInputReloadMatchesURI(uri)
2639
+ }
2640
+
2641
+ func (p *Proxy) writePluginSelectionChanged() error {
2642
+ params, err := json.Marshal(map[string]string{
2643
+ "reason": "projectInputChanged",
2644
+ })
2645
+ if err != nil {
2646
+ return err
2647
+ }
2648
+ body, err := json.Marshal(Envelope{
2649
+ JSONRPC: "2.0",
2650
+ Method: methodPluginSelectionChanged,
2651
+ Params: params,
2652
+ })
2653
+ if err != nil {
2654
+ return err
2655
+ }
2656
+ return p.writeEditorFrame(body)
2657
+ }
2658
+
2659
+ func (p *Proxy) refreshProjectInputs() {
2660
+ type projectInputRefresher interface {
2661
+ RefreshProjectInputs()
2662
+ }
2663
+ if source, ok := p.source.(projectInputRefresher); ok {
2664
+ source.RefreshProjectInputs()
2665
+ }
2666
+ }
2667
+
2668
+ type projectDiagnosticsSource interface {
2669
+ ProjectDiagnostics() *LSPProjectDiagnostics
2670
+ }
2671
+
2672
+ type ownedProjectDiagnosticsSource interface {
2673
+ ProjectDiagnosticsForOwners([]string) projectDiagnosticsRefreshResult
2674
+ }
2675
+
2676
+ // scheduleProjectDiagnosticRefresh debounces external-input events and
2677
+ // invalidates the prior generation immediately. One in-flight computation may
2678
+ // finish, but its generation can no longer publish after a newer event.
2679
+ func (p *Proxy) scheduleProjectDiagnosticRefresh(
2680
+ scope projectDiagnosticOwnerScope,
2681
+ ) {
2682
+ if _, owned := p.source.(ownedProjectDiagnosticsSource); !owned {
2683
+ if _, legacy := p.source.(projectDiagnosticsSource); !legacy {
2684
+ return
2685
+ }
2686
+ }
2687
+ if !scope.all && len(scope.owners) == 0 {
2688
+ if _, owned := p.source.(ownedProjectDiagnosticsSource); !owned {
2689
+ return
2690
+ }
2691
+ }
2692
+ // Generation ownership and pending scope form one transaction. Resume uses
2693
+ // the same diagnostics -> refresh lock order, so neither path can overwrite
2694
+ // the other's newer pending generation between the two assignments.
2695
+ p.diagnosticsMu.Lock()
2696
+ p.projectRefreshMu.Lock()
2697
+ p.projectDiagnosticGeneration++
2698
+ generation := p.projectDiagnosticGeneration
2699
+ p.pendingProjectDiagnosticGeneration = generation
2700
+ p.projectDiagnosticRefreshPending = true
2701
+ if scope.all {
2702
+ p.pendingProjectDiagnosticAllOwners = true
2703
+ p.pendingProjectDiagnosticOwners = nil
2704
+ } else if !p.pendingProjectDiagnosticAllOwners {
2705
+ if p.pendingProjectDiagnosticOwners == nil {
2706
+ p.pendingProjectDiagnosticOwners = map[string]struct{}{}
2707
+ }
2708
+ for owner := range scope.owners {
2709
+ p.pendingProjectDiagnosticOwners[owner] = struct{}{}
2710
+ }
2711
+ }
2712
+ p.armProjectDiagnosticRefreshLocked()
2713
+ p.projectRefreshMu.Unlock()
2714
+ p.diagnosticsMu.Unlock()
2715
+ }
2716
+
2717
+ func (p *Proxy) armProjectDiagnosticRefreshLocked() {
2718
+ if p.projectRefreshTimer != nil {
2719
+ p.projectRefreshTimer.Stop()
2720
+ }
2721
+ p.projectRefreshTimer = time.AfterFunc(60*time.Millisecond, func() {
2722
+ p.projectDiagnosticsRefresh.schedule(
2723
+ p.refreshProjectDiagnostics,
2724
+ )
2725
+ })
2726
+ }
2727
+
2728
+ func (p *Proxy) pendingProjectDiagnosticScopeLocked() projectDiagnosticOwnerScope {
2729
+ scope := projectDiagnosticOwnerScope{
2730
+ all: p.pendingProjectDiagnosticAllOwners,
2731
+ owners: map[string]struct{}{},
2732
+ }
2733
+ for owner := range p.pendingProjectDiagnosticOwners {
2734
+ scope.owners[owner] = struct{}{}
2735
+ }
2736
+ return scope
2737
+ }
2738
+
2739
+ func (p *Proxy) recordPendingProjectDiagnosticOwnersRefreshed(
2740
+ generation uint64,
2741
+ refreshed map[string]struct{},
2742
+ ) bool {
2743
+ p.projectRefreshMu.Lock()
2744
+ defer p.projectRefreshMu.Unlock()
2745
+ if !p.projectDiagnosticRefreshPending ||
2746
+ generation < p.pendingProjectDiagnosticGeneration ||
2747
+ p.pendingProjectDiagnosticAllOwners ||
2748
+ len(p.pendingProjectDiagnosticOwners) == 0 {
2749
+ return false
2750
+ }
2751
+ for owner := range refreshed {
2752
+ delete(p.pendingProjectDiagnosticOwners, owner)
2753
+ }
2754
+ return len(p.pendingProjectDiagnosticOwners) == 0
2755
+ }
2756
+
2757
+ func (p *Proxy) refreshProjectDiagnostics(uint64) {
2758
+ p.projectRefreshMu.Lock()
2759
+ generation := p.pendingProjectDiagnosticGeneration
2760
+ pending := p.projectDiagnosticRefreshPending
2761
+ scope := p.pendingProjectDiagnosticScopeLocked()
2762
+ p.projectRefreshMu.Unlock()
2763
+ if !pending {
2764
+ return
2765
+ }
2766
+ if p.hasDirtyDocuments() {
2767
+ return
2768
+ }
2769
+ refresh := projectDiagnosticsRefreshResult{complete: true}
2770
+ if source, ok := p.source.(ownedProjectDiagnosticsSource); ok {
2771
+ owners := scope.list()
2772
+ if scope.all {
2773
+ owners = nil
2774
+ }
2775
+ refresh = source.ProjectDiagnosticsForOwners(owners)
2776
+ } else if source, ok := p.source.(projectDiagnosticsSource); ok {
2777
+ refresh.publication = source.ProjectDiagnostics()
2778
+ refresh.complete =
2779
+ refresh.publication != nil && refresh.publication.URI != ""
2780
+ refresh.selected = 1
2781
+ } else {
2782
+ return
2783
+ }
2784
+ if refresh.selected == 0 {
2785
+ if refresh.complete {
2786
+ p.completePendingProjectDiagnosticRefresh(generation)
2787
+ }
2788
+ return
2789
+ }
2790
+ if refresh.publication == nil || refresh.publication.URI == "" {
2791
+ return
2792
+ }
2793
+ // The editor can retain a project diagnostic from before this proxy
2794
+ // generation. An external-input refresh therefore publishes an explicit
2795
+ // empty replacement even when the in-process cache was already empty.
2796
+ writeResult, err := p.writeProjectDiagnosticsIfCurrent(
2797
+ refresh.publication,
2798
+ generation,
2799
+ true,
2800
+ )
2801
+ p.reportAsyncError(err)
2802
+ if err != nil || p.hasDirtyDocuments() {
2803
+ return
2804
+ }
2805
+ if writeResult.frameWritten {
2806
+ ownersComplete :=
2807
+ !scope.all &&
2808
+ p.recordPendingProjectDiagnosticOwnersRefreshed(
2809
+ generation,
2810
+ refresh.refreshed,
2811
+ )
2812
+ if refresh.complete || ownersComplete {
2813
+ p.completePendingProjectDiagnosticRefresh(generation)
2814
+ }
2815
+ return
2816
+ }
2817
+ // A document diagnostics request may have advanced the project generation
2818
+ // without publishing project diagnostics, for example when parse diagnostics
2819
+ // caused the sidecar to omit its project result. Retain and rerun the direct
2820
+ // refresh instead of treating that rejected write as a successful clear.
2821
+ if !writeResult.accepted {
2822
+ p.resumePendingProjectDiagnosticRefresh()
2823
+ }
2824
+ }
2825
+
2826
+ func (p *Proxy) completePendingProjectDiagnosticRefresh(generation uint64) {
2827
+ p.projectRefreshMu.Lock()
2828
+ defer p.projectRefreshMu.Unlock()
2829
+ if !p.projectDiagnosticRefreshPending ||
2830
+ generation < p.pendingProjectDiagnosticGeneration {
2831
+ return
2832
+ }
2833
+ p.projectDiagnosticRefreshPending = false
2834
+ p.pendingProjectDiagnosticAllOwners = false
2835
+ p.pendingProjectDiagnosticOwners = nil
2836
+ if p.projectRefreshTimer != nil {
2837
+ p.projectRefreshTimer.Stop()
2838
+ p.projectRefreshTimer = nil
2839
+ }
2840
+ }
2841
+
2842
+ func (p *Proxy) stopProjectDiagnosticRefresh() {
2843
+ p.projectRefreshMu.Lock()
2844
+ defer p.projectRefreshMu.Unlock()
2845
+ if p.projectRefreshTimer != nil {
2846
+ p.projectRefreshTimer.Stop()
2847
+ p.projectRefreshTimer = nil
2848
+ }
2849
+ p.projectDiagnosticRefreshPending = false
2850
+ p.pendingProjectDiagnosticAllOwners = false
2851
+ p.pendingProjectDiagnosticOwners = nil
2852
+ }
2853
+
2854
+ func (p *Proxy) resumePendingProjectDiagnosticRefresh() {
2855
+ p.diagnosticsMu.Lock()
2856
+ defer p.diagnosticsMu.Unlock()
2857
+ if len(p.dirtyDocuments) != 0 {
2858
+ return
2859
+ }
2860
+ p.projectRefreshMu.Lock()
2861
+ defer p.projectRefreshMu.Unlock()
2862
+ if p.projectDiagnosticRefreshPending {
2863
+ p.projectDiagnosticGeneration++
2864
+ p.pendingProjectDiagnosticGeneration = p.projectDiagnosticGeneration
2865
+ p.armProjectDiagnosticRefreshLocked()
2866
+ }
2867
+ }
2868
+
2869
+ func (p *Proxy) hasDirtyDocuments() bool {
2870
+ p.diagnosticsMu.Lock()
2871
+ defer p.diagnosticsMu.Unlock()
2872
+ return len(p.dirtyDocuments) > 0
2873
+ }
2874
+
2875
+ func watchedChangesMayAffectProgram(changes []struct {
2876
+ URI string `json:"uri"`
2877
+ Type *int `json:"type"`
2878
+ }) bool {
2879
+ for _, change := range changes {
2880
+ if isProjectConfigURI(change.URI) {
2881
+ return true
2882
+ }
2883
+ if watchedURIHasProgramInputExtension(change.URI) {
2884
+ return true
2885
+ }
2886
+ }
2887
+ return false
2888
+ }
2889
+
2890
+ func externalWatchedChangeRetainsProgram(
2891
+ uri string,
2892
+ changeType *int,
2893
+ ) bool {
2894
+ if uri == "" || changeType == nil || isProjectConfigURI(uri) {
2895
+ return false
2896
+ }
2897
+ if _, ok := filePathFromURI(uri); !ok {
2898
+ return false
2899
+ }
2900
+ switch *changeType {
2901
+ case fileChangeTypeChanged:
2902
+ // Package metadata changes module resolution without changing the declared
2903
+ // input population. Ordinary data JSON remains a warm external update.
2904
+ return !watchedURIIsPackageJSON(uri)
2905
+ case fileChangeTypeCreated, fileChangeTypeDeleted:
2906
+ // A declared path may also satisfy a compiler root, module resolution, or
2907
+ // another compiler-recognized extension. Only those membership changes
2908
+ // cold-load the Program; data-only topology remains safely localizable.
2909
+ return !watchedURIHasProgramInputExtension(uri)
2910
+ default:
2911
+ return false
2912
+ }
2913
+ }
2914
+
2915
+ func watchedURIIsPackageJSON(uri string) bool {
2916
+ location, ok := filePathFromURI(uri)
2917
+ return ok && strings.EqualFold(filepath.Base(location), "package.json")
2918
+ }
2919
+
2920
+ func watchedURIHasProgramInputExtension(uri string) bool {
2921
+ if watchedURIHasProgramSourceExtension(uri) {
2922
+ return true
2923
+ }
2924
+ location, ok := filePathFromURI(uri)
2925
+ return ok && strings.EqualFold(filepath.Ext(location), ".json")
2926
+ }
2927
+
2928
+ func watchedURIHasProgramSourceExtension(uri string) bool {
2929
+ location, ok := filePathFromURI(uri)
2930
+ if !ok {
2931
+ return false
2932
+ }
2933
+ switch strings.ToLower(filepath.Ext(location)) {
2934
+ case ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs":
2935
+ return true
2936
+ default:
2937
+ return false
2938
+ }
2939
+ }
2940
+
2378
2941
  // isProjectConfigURI reports whether uri names a TypeScript project
2379
2942
  // configuration file — `tsconfig.json`, `jsconfig.json`, and the
2380
2943
  // `tsconfig.<name>.json` spellings the VS Code client watches. Such an edit can