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
@@ -0,0 +1,268 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "path/filepath"
7
+ "testing"
8
+ )
9
+
10
+ type mutableProjectInputRegistrationSource struct {
11
+ NullPluginSource
12
+ snapshot LSPProjectInputSnapshot
13
+ }
14
+
15
+ func (s *mutableProjectInputRegistrationSource) ProjectInputs() LSPProjectInputSnapshot {
16
+ return copyProjectInputSnapshot(s.snapshot)
17
+ }
18
+
19
+ // TestProjectInputRegistrationReconcilesRejectedPendingRequest verifies
20
+ // rejected dynamic-registration requests cannot strand a newer project-input
21
+ // snapshot.
22
+ //
23
+ // A rejected replacement must preserve the last-good registration while the
24
+ // proxy advances to the latest desired snapshot. Rejected stale cleanup must
25
+ // likewise retain its work without blocking a newer registration or creating a
26
+ // duplicate active registration.
27
+ //
28
+ // 1. Register snapshot A, then leave replacement B pending.
29
+ // 2. Publish C and reject B; assert C registers while A remains active.
30
+ // 3. Register D, leave cleanup of C pending, then publish E and reject cleanup.
31
+ // 4. Assert E registers while the rejected cleanup remains deferred.
32
+ // 5. Publish F and assert retained cleanup completes once before F replaces E.
33
+ func TestProjectInputRegistrationReconcilesRejectedPendingRequest(
34
+ t *testing.T,
35
+ ) {
36
+ root := t.TempDir()
37
+ snapshot := func(name string) LSPProjectInputSnapshot {
38
+ return LSPProjectInputSnapshot{
39
+ Root: filepath.ToSlash(root),
40
+ Files: []string{
41
+ filepath.ToSlash(filepath.Join(root, "docs", name+".md")),
42
+ },
43
+ }
44
+ }
45
+ source := &mutableProjectInputRegistrationSource{snapshot: snapshot("a")}
46
+ proxy := NewProxy(ProxyOptions{
47
+ EditorOut: &bytes.Buffer{},
48
+ Source: source,
49
+ })
50
+ proxy.projectInputWatchReady = true
51
+ proxy.projectInputWatchDynamic = true
52
+ proxy.projectInputWatchRelative = true
53
+
54
+ proxy.projectInputsRefreshed()
55
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
56
+ registrationA := proxy.projectInputWatchActive
57
+ assertProjectInputWatchState(
58
+ t,
59
+ proxy,
60
+ projectInputWatchRegistrationForSnapshot(snapshot("a")).Signature,
61
+ nil,
62
+ false,
63
+ )
64
+
65
+ source.snapshot = snapshot("b")
66
+ proxy.projectInputsRefreshed()
67
+ source.snapshot = snapshot("c")
68
+ proxy.projectInputsRefreshed()
69
+ respondToPendingProjectInputWatchRequest(t, proxy, true, "reject B")
70
+ if proxy.projectInputWatchActive.ID != registrationA.ID {
71
+ t.Fatalf(
72
+ "rejected B replaced last-good A: active = %#v, A = %#v",
73
+ proxy.projectInputWatchActive,
74
+ registrationA,
75
+ )
76
+ }
77
+ assertProjectInputWatchState(
78
+ t,
79
+ proxy,
80
+ projectInputWatchRegistrationForSnapshot(snapshot("c")).Signature,
81
+ nil,
82
+ true,
83
+ )
84
+
85
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
86
+ registrationC := proxy.projectInputWatchActive
87
+ if registrationC.Signature !=
88
+ projectInputWatchRegistrationForSnapshot(snapshot("c")).Signature {
89
+ t.Fatalf("active after C success = %#v", registrationC)
90
+ }
91
+ assertProjectInputWatchState(
92
+ t,
93
+ proxy,
94
+ registrationC.Signature,
95
+ []string{registrationA.ID},
96
+ true,
97
+ )
98
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
99
+ assertProjectInputWatchState(t, proxy, registrationC.Signature, nil, false)
100
+
101
+ source.snapshot = snapshot("d")
102
+ proxy.projectInputsRefreshed()
103
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
104
+ registrationD := proxy.projectInputWatchActive
105
+ assertProjectInputWatchState(
106
+ t,
107
+ proxy,
108
+ registrationD.Signature,
109
+ []string{registrationC.ID},
110
+ true,
111
+ )
112
+
113
+ source.snapshot = snapshot("e")
114
+ proxy.projectInputsRefreshed()
115
+ respondToPendingProjectInputWatchRequest(
116
+ t,
117
+ proxy,
118
+ true,
119
+ "reject cleanup C",
120
+ )
121
+ if proxy.projectInputWatchActive.ID != registrationD.ID {
122
+ t.Fatalf(
123
+ "rejected cleanup replaced last-good D: active = %#v, D = %#v",
124
+ proxy.projectInputWatchActive,
125
+ registrationD,
126
+ )
127
+ }
128
+ assertProjectInputWatchState(
129
+ t,
130
+ proxy,
131
+ projectInputWatchRegistrationForSnapshot(snapshot("e")).Signature,
132
+ []string{registrationC.ID},
133
+ true,
134
+ )
135
+
136
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
137
+ registrationE := proxy.projectInputWatchActive
138
+ assertProjectInputWatchState(
139
+ t,
140
+ proxy,
141
+ registrationE.Signature,
142
+ []string{registrationC.ID, registrationD.ID},
143
+ false,
144
+ )
145
+
146
+ source.snapshot = snapshot("f")
147
+ proxy.projectInputsRefreshed()
148
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
149
+ assertProjectInputWatchState(
150
+ t,
151
+ proxy,
152
+ projectInputWatchRegistrationForSnapshot(snapshot("f")).Signature,
153
+ []string{registrationD.ID},
154
+ true,
155
+ )
156
+ if proxy.projectInputWatchActive.ID != registrationE.ID {
157
+ t.Fatal("retrying cleanup registered F before removing retained C")
158
+ }
159
+
160
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
161
+ assertProjectInputWatchState(
162
+ t,
163
+ proxy,
164
+ projectInputWatchRegistrationForSnapshot(snapshot("f")).Signature,
165
+ nil,
166
+ true,
167
+ )
168
+ if proxy.projectInputWatchActive.ID != registrationE.ID {
169
+ t.Fatal("retrying cleanup registered F before removing stale D")
170
+ }
171
+
172
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
173
+ registrationF := proxy.projectInputWatchActive
174
+ assertProjectInputWatchState(
175
+ t,
176
+ proxy,
177
+ registrationF.Signature,
178
+ []string{registrationE.ID},
179
+ true,
180
+ )
181
+ respondToPendingProjectInputWatchRequest(t, proxy, false, "")
182
+ assertProjectInputWatchState(t, proxy, registrationF.Signature, nil, false)
183
+
184
+ if proxy.projectInputWatchRegistrationSequence != 6 {
185
+ t.Fatalf(
186
+ "registration attempts = %d, want A through F exactly once",
187
+ proxy.projectInputWatchRegistrationSequence,
188
+ )
189
+ }
190
+ }
191
+
192
+ func respondToPendingProjectInputWatchRequest(
193
+ t *testing.T,
194
+ proxy *Proxy,
195
+ rejected bool,
196
+ message string,
197
+ ) {
198
+ t.Helper()
199
+ proxy.pendingMu.Lock()
200
+ if len(proxy.pendingClientRequests) != 1 {
201
+ proxy.pendingMu.Unlock()
202
+ t.Fatalf(
203
+ "pending client requests = %d, want 1",
204
+ len(proxy.pendingClientRequests),
205
+ )
206
+ }
207
+ var rawID json.RawMessage
208
+ for key := range proxy.pendingClientRequests {
209
+ rawID = json.RawMessage(key)
210
+ }
211
+ proxy.pendingMu.Unlock()
212
+
213
+ response := Envelope{
214
+ JSONRPC: "2.0",
215
+ ID: rawID,
216
+ Result: json.RawMessage("null"),
217
+ }
218
+ if rejected {
219
+ response.Result = nil
220
+ response.Error, _ = json.Marshal(map[string]any{
221
+ "code": -32603,
222
+ "message": message,
223
+ })
224
+ }
225
+ if !proxy.handleClientRequestResponse(response) {
226
+ t.Fatalf("pending client response %s was not handled", rawID)
227
+ }
228
+ }
229
+
230
+ func assertProjectInputWatchState(
231
+ t *testing.T,
232
+ proxy *Proxy,
233
+ desiredSignature string,
234
+ staleIDs []string,
235
+ pending bool,
236
+ ) {
237
+ t.Helper()
238
+ proxy.projectInputWatchMu.Lock()
239
+ defer proxy.projectInputWatchMu.Unlock()
240
+ if proxy.projectInputWatchDesired.Signature != desiredSignature ||
241
+ !equalProjectInputWatchIDs(proxy.projectInputWatchStaleIDs, staleIDs) ||
242
+ proxy.projectInputWatchPending != pending {
243
+ t.Fatalf(
244
+ "watch state = {desired:%q active:%#v stale:%#v pending:%v failed:%q retryBlocked:%v}, want desired %q stale %#v pending %v",
245
+ proxy.projectInputWatchDesired.Signature,
246
+ proxy.projectInputWatchActive,
247
+ proxy.projectInputWatchStaleIDs,
248
+ proxy.projectInputWatchPending,
249
+ proxy.projectInputWatchFailedSignature,
250
+ proxy.projectInputWatchUnregisterRetryBlocked,
251
+ desiredSignature,
252
+ staleIDs,
253
+ pending,
254
+ )
255
+ }
256
+ }
257
+
258
+ func equalProjectInputWatchIDs(left []string, right []string) bool {
259
+ if len(left) != len(right) {
260
+ return false
261
+ }
262
+ for index := range left {
263
+ if left[index] != right[index] {
264
+ return false
265
+ }
266
+ }
267
+ return true
268
+ }
@@ -0,0 +1,94 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+ )
7
+
8
+ // TestProjectInputRegistrationShapesRelativePatterns verifies exact paths and
9
+ // glob populations become deterministic LSP RelativePatterns without treating
10
+ // literal filename metacharacters as wildcards.
11
+ //
12
+ // Reload directories need two registrations: immediate children carry
13
+ // topology changes, while the exact directory identity carries deletion and
14
+ // replacement when a child-rooted watcher can no longer observe the object.
15
+ //
16
+ // 1. Declare duplicate exact files, portable globs, and one reload directory.
17
+ // 2. Build and index the deduplicated relative-pattern registrations.
18
+ // 3. Assert literals are escaped while portable wildcard segments survive.
19
+ // 4. Assert reload-directory identity and immediate-child patterns both exist.
20
+ // 5. Assert drive and UNC file URIs preserve their authority and escaping.
21
+ func TestProjectInputRegistrationShapesRelativePatterns(t *testing.T) {
22
+ root := t.TempDir()
23
+ exact := filepath.Join(
24
+ root,
25
+ "docs",
26
+ "contract *?[v1]{draft} #%25.md",
27
+ )
28
+ snapshot := LSPProjectInputSnapshot{
29
+ Root: filepath.ToSlash(root),
30
+ Files: []string{
31
+ filepath.ToSlash(exact),
32
+ filepath.ToSlash(exact),
33
+ },
34
+ Globs: []string{
35
+ filepath.ToSlash(filepath.Join(root, "api", "**", "*.json")),
36
+ filepath.ToSlash(filepath.Join(
37
+ root,
38
+ "api",
39
+ "**",
40
+ "v[12]",
41
+ "{openapi,swagger}.yaml",
42
+ )),
43
+ },
44
+ ReloadDirectories: []string{
45
+ filepath.ToSlash(filepath.Join(root, "config-deps")),
46
+ },
47
+ ReloadFiles: []string{
48
+ filepath.ToSlash(exact),
49
+ },
50
+ }
51
+ watchers := projectInputFileWatchers(snapshot)
52
+ if len(watchers) != 5 {
53
+ t.Fatalf("watchers = %#v, want 5 deduplicated entries", watchers)
54
+ }
55
+
56
+ byPattern := map[string]projectInputFileWatcher{}
57
+ for _, watcher := range watchers {
58
+ if watcher.Kind != watchedFileKindAll {
59
+ t.Fatalf("watcher kind = %d, want %d", watcher.Kind, watchedFileKindAll)
60
+ }
61
+ byPattern[watcher.GlobPattern.Pattern] = watcher
62
+ }
63
+ literal := "docs/contract [*][?][[]v1[]][{]draft[}] #%25.md"
64
+ if watcher, ok := byPattern[literal]; !ok {
65
+ t.Fatalf("escaped exact pattern missing from %#v", byPattern)
66
+ } else if watcher.GlobPattern.BaseURI == "" {
67
+ t.Fatal("exact watcher has an empty base URI")
68
+ }
69
+ if watcher, ok := byPattern["api/**/*.json"]; !ok {
70
+ t.Fatalf("glob pattern missing from %#v", byPattern)
71
+ } else if watcher.GlobPattern.BaseURI == "" {
72
+ t.Fatal("glob watcher has an empty base URI")
73
+ }
74
+ literalMetacharacters :=
75
+ "api/**/v[[]12[]]/[{]openapi,swagger[}].yaml"
76
+ if _, ok := byPattern[literalMetacharacters]; !ok {
77
+ t.Fatalf("literal glob metacharacters were not escaped in %#v", byPattern)
78
+ }
79
+ if _, ok := byPattern["config-deps/*"]; !ok {
80
+ t.Fatalf("reload-directory child watcher missing from %#v", byPattern)
81
+ }
82
+ if _, ok := byPattern["config-deps"]; !ok {
83
+ t.Fatalf("reload-directory identity watcher missing from %#v", byPattern)
84
+ }
85
+
86
+ if got := projectInputFileURI("C:/Program Files/a#b%25"); got !=
87
+ "file:///C:/Program%20Files/a%23b%2525" {
88
+ t.Fatalf("drive URI = %q", got)
89
+ }
90
+ if got := projectInputFileURI("//server/share/a b#c"); got !=
91
+ "file://server/share/a%20b%23c" {
92
+ t.Fatalf("UNC URI = %q", got)
93
+ }
94
+ }
@@ -0,0 +1,48 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+ )
7
+
8
+ // TestProjectInputWatchersAnchorMissingPaths verifies dynamic registrations use
9
+ // an existing ancestor while retaining the missing path in the glob.
10
+ //
11
+ // Editors cannot reliably root a RelativePattern at a directory that does not
12
+ // exist yet. Anchoring at the physical project lets the registration observe
13
+ // later directory creation without broadening beyond the declared pattern.
14
+ //
15
+ // 1. Declare one exact file and one glob under missing nested directories.
16
+ // 2. Build the client watcher registrations before either directory exists.
17
+ // 3. Assert both use the project URI and preserve their missing path segments.
18
+ func TestProjectInputWatchersAnchorMissingPaths(t *testing.T) {
19
+ root := t.TempDir()
20
+ watchers := projectInputFileWatchers(LSPProjectInputSnapshot{
21
+ Root: root,
22
+ Files: []string{
23
+ filepath.Join(root, "docs", "missing", "spec.md"),
24
+ },
25
+ Globs: []string{
26
+ filepath.Join(root, "api", "v1", "**", "*.json"),
27
+ },
28
+ })
29
+ if len(watchers) != 2 {
30
+ t.Fatalf("watchers = %#v", watchers)
31
+ }
32
+ wantBase := projectInputFileURI(root)
33
+ patterns := map[string]bool{}
34
+ for _, watcher := range watchers {
35
+ if watcher.GlobPattern.BaseURI != wantBase {
36
+ t.Fatalf(
37
+ "watcher base = %q, want %q",
38
+ watcher.GlobPattern.BaseURI,
39
+ wantBase,
40
+ )
41
+ }
42
+ patterns[watcher.GlobPattern.Pattern] = true
43
+ }
44
+ if !patterns["docs/missing/spec.md"] ||
45
+ !patterns["api/v1/**/*.json"] {
46
+ t.Fatalf("watcher patterns = %#v", patterns)
47
+ }
48
+ }