ttsc 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cmd/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
|
@@ -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
|
+
}
|