ttsc 0.21.0 → 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
@@ -0,0 +1,504 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/url"
7
+ "os"
8
+ "path/filepath"
9
+ "sort"
10
+ "strings"
11
+ )
12
+
13
+ const (
14
+ methodRegisterCapability = "client/registerCapability"
15
+ methodUnregisterCapability = "client/unregisterCapability"
16
+ watchedFileKindAll = 7
17
+ )
18
+
19
+ type projectInputRelativePattern struct {
20
+ BaseURI string `json:"baseUri"`
21
+ Pattern string `json:"pattern"`
22
+ }
23
+
24
+ type projectInputFileWatcher struct {
25
+ GlobPattern projectInputRelativePattern `json:"globPattern"`
26
+ Kind int `json:"kind"`
27
+ }
28
+
29
+ type projectInputWatchRegistration struct {
30
+ ID string
31
+ Signature string
32
+ Watchers []projectInputFileWatcher
33
+ }
34
+
35
+ type projectInputSnapshotSource interface {
36
+ ProjectInputs() LSPProjectInputSnapshot
37
+ }
38
+
39
+ type projectInputReloadFingerprintSource interface {
40
+ ProjectInputReloadFingerprintsAreCurrent() bool
41
+ }
42
+
43
+ func (p *Proxy) projectInputWatchInitialized() {
44
+ p.projectInputWatchMu.Lock()
45
+ p.projectInputWatchReady = true
46
+ p.projectInputWatchMu.Unlock()
47
+ p.projectInputsRefreshed()
48
+ }
49
+
50
+ func (p *Proxy) projectInputsRefreshed() {
51
+ source, ok := p.source.(projectInputSnapshotSource)
52
+ if !ok {
53
+ return
54
+ }
55
+ desired := projectInputWatchRegistrationForSnapshot(source.ProjectInputs())
56
+
57
+ p.projectInputWatchMu.Lock()
58
+ changed := p.projectInputWatchDesired.Signature != desired.Signature
59
+ p.projectInputWatchDesired = desired
60
+ if changed {
61
+ p.projectInputWatchFailedSignature = ""
62
+ p.projectInputWatchUnregisterRetryBlocked = false
63
+ }
64
+ ready := p.projectInputWatchReady
65
+ p.projectInputWatchMu.Unlock()
66
+ if !ready {
67
+ return
68
+ }
69
+
70
+ p.capabilityMu.Lock()
71
+ supported :=
72
+ p.projectInputWatchDynamic && p.projectInputWatchRelative
73
+ p.capabilityMu.Unlock()
74
+ if len(desired.Watchers) != 0 && !supported {
75
+ p.projectInputWatchMu.Lock()
76
+ warn := !p.projectInputWatchWarningSent
77
+ p.projectInputWatchWarningSent = true
78
+ p.projectInputWatchMu.Unlock()
79
+ if warn {
80
+ p.reportAsyncError(p.writeProjectInputWatchWarning(
81
+ "the LSP client does not support dynamic relative file-pattern registration; declared Markdown and local Swagger diagnostics may remain stale until another project event",
82
+ ))
83
+ }
84
+ return
85
+ }
86
+ p.reconcileProjectInputWatchRegistration()
87
+ }
88
+
89
+ func (p *Proxy) reconcileProjectInputWatchRegistration() {
90
+ p.projectInputWatchMu.Lock()
91
+ if !p.projectInputWatchReady || p.projectInputWatchPending {
92
+ p.projectInputWatchMu.Unlock()
93
+ return
94
+ }
95
+ if len(p.projectInputWatchStaleIDs) != 0 &&
96
+ !p.projectInputWatchUnregisterRetryBlocked {
97
+ staleID := p.projectInputWatchStaleIDs[0]
98
+ p.projectInputWatchPending = true
99
+ p.projectInputWatchMu.Unlock()
100
+ if err := p.writeProjectInputWatchUnregistration(staleID); err != nil {
101
+ p.projectInputWatchMu.Lock()
102
+ p.projectInputWatchPending = false
103
+ p.projectInputWatchUnregisterRetryBlocked = true
104
+ p.projectInputWatchMu.Unlock()
105
+ p.reportAsyncError(err)
106
+ p.reconcileProjectInputWatchRegistration()
107
+ }
108
+ return
109
+ }
110
+ desired := p.projectInputWatchDesired
111
+ active := p.projectInputWatchActive
112
+ if desired.Signature == active.Signature {
113
+ p.projectInputWatchMu.Unlock()
114
+ return
115
+ }
116
+ if len(desired.Watchers) == 0 {
117
+ if active.ID != "" {
118
+ p.projectInputWatchStaleIDs = append(
119
+ p.projectInputWatchStaleIDs,
120
+ active.ID,
121
+ )
122
+ p.projectInputWatchActive = projectInputWatchRegistration{}
123
+ }
124
+ p.projectInputWatchMu.Unlock()
125
+ p.reconcileProjectInputWatchRegistration()
126
+ return
127
+ }
128
+ if p.projectInputWatchFailedSignature == desired.Signature {
129
+ p.projectInputWatchMu.Unlock()
130
+ return
131
+ }
132
+ p.projectInputWatchRegistrationSequence++
133
+ desired.ID = fmt.Sprintf(
134
+ "ttsc-project-input-watch-%d",
135
+ p.projectInputWatchRegistrationSequence,
136
+ )
137
+ p.projectInputWatchPending = true
138
+ p.projectInputWatchMu.Unlock()
139
+ if err := p.writeProjectInputWatchRegistration(desired); err != nil {
140
+ p.projectInputWatchMu.Lock()
141
+ p.projectInputWatchPending = false
142
+ p.projectInputWatchFailedSignature = desired.Signature
143
+ p.projectInputWatchMu.Unlock()
144
+ p.reportAsyncError(err)
145
+ p.reconcileProjectInputWatchRegistration()
146
+ }
147
+ }
148
+
149
+ func (p *Proxy) writeProjectInputWatchRegistration(
150
+ registration projectInputWatchRegistration,
151
+ ) error {
152
+ params := map[string]any{
153
+ "registrations": []any{
154
+ map[string]any{
155
+ "id": registration.ID,
156
+ "method": methodDidChangeWatchedFiles,
157
+ "registerOptions": map[string]any{
158
+ "watchers": registration.Watchers,
159
+ },
160
+ },
161
+ },
162
+ }
163
+ return p.writeClientRequest(
164
+ methodRegisterCapability,
165
+ params,
166
+ func(env Envelope) {
167
+ p.projectInputWatchMu.Lock()
168
+ p.projectInputWatchPending = false
169
+ if env.IsErrorResponse() {
170
+ p.projectInputWatchFailedSignature = registration.Signature
171
+ p.projectInputWatchMu.Unlock()
172
+ p.reportAsyncError(p.writeProjectInputWatchWarning(
173
+ fmt.Sprintf(
174
+ "the LSP client rejected declared external-input watcher registration: %s",
175
+ strings.TrimSpace(string(env.Error)),
176
+ ),
177
+ ))
178
+ p.reconcileProjectInputWatchRegistration()
179
+ return
180
+ }
181
+ previous := p.projectInputWatchActive
182
+ p.projectInputWatchActive = registration
183
+ p.projectInputWatchFailedSignature = ""
184
+ if previous.ID != "" && previous.ID != registration.ID {
185
+ p.projectInputWatchStaleIDs = append(
186
+ p.projectInputWatchStaleIDs,
187
+ previous.ID,
188
+ )
189
+ }
190
+ p.projectInputWatchMu.Unlock()
191
+ if source, ok := p.source.(projectInputReloadFingerprintSource); ok &&
192
+ !source.ProjectInputReloadFingerprintsAreCurrent() {
193
+ if err := p.writePluginSelectionChanged(); err != nil {
194
+ p.reportAsyncError(err)
195
+ } else {
196
+ p.reportAsyncError(ErrLSPPluginSelectionChanged)
197
+ }
198
+ return
199
+ }
200
+ p.reconcileProjectInputWatchRegistration()
201
+ },
202
+ )
203
+ }
204
+
205
+ func (p *Proxy) writeProjectInputWatchUnregistration(
206
+ registrationID string,
207
+ ) error {
208
+ params := map[string]any{
209
+ // LSP 3.17 standardized this misspelling; clients consume it verbatim.
210
+ "unregisterations": []any{
211
+ map[string]any{
212
+ "id": registrationID,
213
+ "method": methodDidChangeWatchedFiles,
214
+ },
215
+ },
216
+ }
217
+ return p.writeClientRequest(
218
+ methodUnregisterCapability,
219
+ params,
220
+ func(env Envelope) {
221
+ p.projectInputWatchMu.Lock()
222
+ p.projectInputWatchPending = false
223
+ if env.IsErrorResponse() {
224
+ p.projectInputWatchUnregisterRetryBlocked = true
225
+ p.projectInputWatchMu.Unlock()
226
+ p.reportAsyncError(p.writeProjectInputWatchWarning(
227
+ fmt.Sprintf(
228
+ "the LSP client rejected stale external-input watcher cleanup: %s",
229
+ strings.TrimSpace(string(env.Error)),
230
+ ),
231
+ ))
232
+ p.reconcileProjectInputWatchRegistration()
233
+ return
234
+ }
235
+ for index, staleID := range p.projectInputWatchStaleIDs {
236
+ if staleID == registrationID {
237
+ p.projectInputWatchStaleIDs = append(
238
+ p.projectInputWatchStaleIDs[:index],
239
+ p.projectInputWatchStaleIDs[index+1:]...,
240
+ )
241
+ break
242
+ }
243
+ }
244
+ p.projectInputWatchUnregisterRetryBlocked = false
245
+ p.projectInputWatchMu.Unlock()
246
+ p.reconcileProjectInputWatchRegistration()
247
+ },
248
+ )
249
+ }
250
+
251
+ func (p *Proxy) writeClientRequest(
252
+ method string,
253
+ params any,
254
+ callback func(Envelope),
255
+ ) error {
256
+ id := fmt.Sprintf(
257
+ "ttsc/project-input-watch/request/%d",
258
+ p.clientRequestSequence.Add(1),
259
+ )
260
+ rawID, _ := json.Marshal(id)
261
+ body, err := json.Marshal(map[string]any{
262
+ "jsonrpc": "2.0",
263
+ "id": id,
264
+ "method": method,
265
+ "params": params,
266
+ })
267
+ if err != nil {
268
+ return err
269
+ }
270
+ key := idKeyFromRaw(rawID)
271
+ p.pendingMu.Lock()
272
+ p.pendingClientRequests[key] = callback
273
+ p.pendingMu.Unlock()
274
+ if err := p.writeEditorFrame(body); err != nil {
275
+ p.pendingMu.Lock()
276
+ delete(p.pendingClientRequests, key)
277
+ p.pendingMu.Unlock()
278
+ return err
279
+ }
280
+ return nil
281
+ }
282
+
283
+ func (p *Proxy) handleClientRequestResponse(env Envelope) bool {
284
+ key := env.IDKey()
285
+ if key == "" {
286
+ return false
287
+ }
288
+ p.pendingMu.Lock()
289
+ callback, ok := p.pendingClientRequests[key]
290
+ if ok {
291
+ delete(p.pendingClientRequests, key)
292
+ }
293
+ p.pendingMu.Unlock()
294
+ if !ok {
295
+ return false
296
+ }
297
+ callback(env)
298
+ return true
299
+ }
300
+
301
+ func (p *Proxy) writeProjectInputWatchWarning(message string) error {
302
+ params, err := json.Marshal(map[string]any{
303
+ "type": lspMessageTypeInfo,
304
+ "message": "ttscserver: " + message,
305
+ })
306
+ if err != nil {
307
+ return err
308
+ }
309
+ body, err := json.Marshal(Envelope{
310
+ JSONRPC: "2.0",
311
+ Method: methodLogMessage,
312
+ Params: params,
313
+ })
314
+ if err != nil {
315
+ return err
316
+ }
317
+ return p.writeEditorFrame(body)
318
+ }
319
+
320
+ func projectInputWatchRegistrationForSnapshot(
321
+ snapshot LSPProjectInputSnapshot,
322
+ ) projectInputWatchRegistration {
323
+ watchers := projectInputFileWatchers(snapshot)
324
+ if len(watchers) == 0 {
325
+ return projectInputWatchRegistration{}
326
+ }
327
+ encoded, _ := json.Marshal(watchers)
328
+ return projectInputWatchRegistration{
329
+ Signature: string(encoded),
330
+ Watchers: watchers,
331
+ }
332
+ }
333
+
334
+ func projectInputFileWatchers(
335
+ snapshot LSPProjectInputSnapshot,
336
+ ) []projectInputFileWatcher {
337
+ unique := map[string]projectInputFileWatcher{}
338
+ for _, file := range snapshot.Files {
339
+ native := filepath.FromSlash(file)
340
+ desiredBase := filepath.Dir(native)
341
+ base := nearestExistingProjectInputDirectory(desiredBase)
342
+ pattern := escapeLSPGlobLiteral(relativeProjectInputPattern(base, native))
343
+ watcher := newProjectInputFileWatcher(base, pattern)
344
+ unique[projectInputFileWatcherKey(watcher)] = watcher
345
+ }
346
+ for _, file := range snapshot.ReloadFiles {
347
+ native := filepath.FromSlash(file)
348
+ desiredBase := filepath.Dir(native)
349
+ base := nearestExistingProjectInputDirectory(desiredBase)
350
+ pattern := escapeLSPGlobLiteral(relativeProjectInputPattern(base, native))
351
+ watcher := newProjectInputFileWatcher(base, pattern)
352
+ unique[projectInputFileWatcherKey(watcher)] = watcher
353
+ }
354
+ for _, directory := range snapshot.ReloadDirectories {
355
+ native := filepath.FromSlash(directory)
356
+ identityBase := nearestExistingProjectInputDirectory(filepath.Dir(native))
357
+ identityPattern := escapeLSPGlobLiteral(
358
+ relativeProjectInputPattern(identityBase, native),
359
+ )
360
+ identityWatcher := newProjectInputFileWatcher(
361
+ identityBase,
362
+ identityPattern,
363
+ )
364
+ unique[projectInputFileWatcherKey(identityWatcher)] = identityWatcher
365
+
366
+ childrenBase := nearestExistingProjectInputDirectory(native)
367
+ prefix := relativeProjectInputPattern(childrenBase, native)
368
+ pattern := "*"
369
+ if prefix != "." {
370
+ pattern = escapeLSPGlobLiteral(prefix) + "/*"
371
+ }
372
+ watcher := newProjectInputFileWatcher(childrenBase, pattern)
373
+ unique[projectInputFileWatcherKey(watcher)] = watcher
374
+ }
375
+ for _, pattern := range snapshot.Globs {
376
+ desiredBase, relative := projectInputGlobRelativePattern(pattern)
377
+ base := nearestExistingProjectInputDirectory(desiredBase)
378
+ prefix := relativeProjectInputPattern(base, desiredBase)
379
+ if prefix != "." {
380
+ relative = escapeLSPGlobLiteral(prefix) + "/" + relative
381
+ }
382
+ watcher := newProjectInputFileWatcher(base, relative)
383
+ unique[projectInputFileWatcherKey(watcher)] = watcher
384
+ }
385
+ keys := make([]string, 0, len(unique))
386
+ for key := range unique {
387
+ keys = append(keys, key)
388
+ }
389
+ sort.Strings(keys)
390
+ watchers := make([]projectInputFileWatcher, 0, len(keys))
391
+ for _, key := range keys {
392
+ watchers = append(watchers, unique[key])
393
+ }
394
+ return watchers
395
+ }
396
+
397
+ func nearestExistingProjectInputDirectory(location string) string {
398
+ current := filepath.Clean(location)
399
+ for {
400
+ if info, err := os.Stat(current); err == nil && info.IsDir() {
401
+ return current
402
+ }
403
+ parent := filepath.Dir(current)
404
+ if parent == current {
405
+ return filepath.Clean(location)
406
+ }
407
+ current = parent
408
+ }
409
+ }
410
+
411
+ func relativeProjectInputPattern(base string, target string) string {
412
+ relative, err := filepath.Rel(base, target)
413
+ if err != nil {
414
+ return filepath.ToSlash(target)
415
+ }
416
+ return filepath.ToSlash(relative)
417
+ }
418
+
419
+ func newProjectInputFileWatcher(
420
+ base string,
421
+ pattern string,
422
+ ) projectInputFileWatcher {
423
+ return projectInputFileWatcher{
424
+ GlobPattern: projectInputRelativePattern{
425
+ BaseURI: projectInputFileURI(base),
426
+ Pattern: filepath.ToSlash(pattern),
427
+ },
428
+ Kind: watchedFileKindAll,
429
+ }
430
+ }
431
+
432
+ func projectInputFileWatcherKey(watcher projectInputFileWatcher) string {
433
+ return watcher.GlobPattern.BaseURI + "\x00" +
434
+ watcher.GlobPattern.Pattern
435
+ }
436
+
437
+ func projectInputGlobRelativePattern(
438
+ pattern string,
439
+ ) (string, string) {
440
+ normalized := filepath.ToSlash(filepath.Clean(filepath.FromSlash(pattern)))
441
+ wildcard := strings.IndexAny(normalized, "*?")
442
+ if wildcard == -1 {
443
+ native := filepath.FromSlash(normalized)
444
+ return filepath.Dir(native), escapeLSPGlobLiteral(filepath.Base(native))
445
+ }
446
+ separator := strings.LastIndex(normalized[:wildcard], "/")
447
+ base := normalized[:separator]
448
+ if base == "" {
449
+ base = "/"
450
+ } else if len(base) == 2 && base[1] == ':' {
451
+ base += "/"
452
+ }
453
+ relative := normalized[separator+1:]
454
+ return filepath.FromSlash(base), escapeLSPGlobClasses(relative)
455
+ }
456
+
457
+ func escapeLSPGlobLiteral(input string) string {
458
+ var out strings.Builder
459
+ for _, char := range input {
460
+ switch char {
461
+ case '*', '?', '[', ']', '{', '}':
462
+ out.WriteByte('[')
463
+ out.WriteRune(char)
464
+ out.WriteByte(']')
465
+ default:
466
+ out.WriteRune(char)
467
+ }
468
+ }
469
+ return out.String()
470
+ }
471
+
472
+ func escapeLSPGlobClasses(input string) string {
473
+ var out strings.Builder
474
+ for _, char := range input {
475
+ switch char {
476
+ case '[', ']', '{', '}':
477
+ out.WriteByte('[')
478
+ out.WriteRune(char)
479
+ out.WriteByte(']')
480
+ default:
481
+ out.WriteRune(char)
482
+ }
483
+ }
484
+ return out.String()
485
+ }
486
+
487
+ func projectInputFileURI(location string) string {
488
+ normalized := filepath.ToSlash(location)
489
+ if strings.HasPrefix(normalized, "//") {
490
+ remainder := strings.TrimPrefix(normalized, "//")
491
+ separator := strings.IndexByte(remainder, '/')
492
+ if separator > 0 {
493
+ return (&url.URL{
494
+ Scheme: "file",
495
+ Host: remainder[:separator],
496
+ Path: "/" + remainder[separator+1:],
497
+ }).String()
498
+ }
499
+ }
500
+ if len(normalized) >= 2 && normalized[1] == ':' {
501
+ normalized = "/" + normalized
502
+ }
503
+ return (&url.URL{Scheme: "file", Path: normalized}).String()
504
+ }
@@ -0,0 +1,71 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "bytes"
5
+ "errors"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ type driftingProjectInputRegistrationSource struct {
11
+ NullPluginSource
12
+ current bool
13
+ snapshot LSPProjectInputSnapshot
14
+ }
15
+
16
+ func (s *driftingProjectInputRegistrationSource) ProjectInputs() LSPProjectInputSnapshot {
17
+ return copyProjectInputSnapshot(s.snapshot)
18
+ }
19
+
20
+ func (s *driftingProjectInputRegistrationSource) ProjectInputReloadFingerprintsAreCurrent() bool {
21
+ return s.current
22
+ }
23
+
24
+ // TestProjectInputRegistrationRechecksSelectionBaseline verifies the client
25
+ // registration response closes the last unobserved startup interval.
26
+ //
27
+ // Constructor validation happens before initialize and dynamic registration.
28
+ // A reload input can change in that interval without producing a replayable
29
+ // event. Once the client confirms registration, a second baseline check must
30
+ // either cover the earlier change or hand all later changes to the watcher.
31
+ //
32
+ // 1. Publish one exact reload-file watcher with a current baseline.
33
+ // 2. Leave the client registration request pending.
34
+ // 3. Mark the selection baseline stale before accepting registration.
35
+ // 4. Prove the proxy notifies the client and requests an expected restart.
36
+ func TestProjectInputRegistrationRechecksSelectionBaseline(t *testing.T) {
37
+ root := t.TempDir()
38
+ source := &driftingProjectInputRegistrationSource{
39
+ current: true,
40
+ snapshot: LSPProjectInputSnapshot{
41
+ Root: filepath.ToSlash(root),
42
+ ReloadFiles: []string{
43
+ filepath.ToSlash(filepath.Join(root, "lint.config.cjs")),
44
+ },
45
+ },
46
+ }
47
+ editorOut := &bytes.Buffer{}
48
+ proxy := NewProxy(ProxyOptions{
49
+ EditorOut: editorOut,
50
+ Source: source,
51
+ })
52
+ proxy.projectInputWatchReady = true
53
+ proxy.projectInputWatchDynamic = true
54
+ proxy.projectInputWatchRelative = true
55
+
56
+ proxy.projectInputsRefreshed()
57
+ source.current = false
58
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
59
+
60
+ if !bytes.Contains(editorOut.Bytes(), []byte(methodPluginSelectionChanged)) {
61
+ t.Fatalf("selection-change notification missing from %q", editorOut.String())
62
+ }
63
+ select {
64
+ case err := <-proxy.asyncErrCh:
65
+ if !errors.Is(err, ErrLSPPluginSelectionChanged) {
66
+ t.Fatalf("registration drift error = %v", err)
67
+ }
68
+ default:
69
+ t.Fatal("registration drift did not request a launcher restart")
70
+ }
71
+ }