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.
- 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/go.mod +1 -1
- 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_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -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_glob_path_other.go +16 -0
- package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
- package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
- package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -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 +1012 -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_directories_separate_glob_territory_test.go +181 -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 +31 -0
- package/lib/internal/projectInputPathIdentity.js +276 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
- package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +126 -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 +468 -47
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +61 -7
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
- package/lib/launcher/internal/runtimeHooks.js +7 -15
- package/lib/launcher/internal/runtimeHooks.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 +288 -2
- package/lib/launcher/internal/watchTopology.js +1921 -54
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
- package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
- package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +34 -8
- 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 +12 -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 +112 -3
- package/src/internal/projectInputPathIdentity.ts +377 -0
- package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
- package/src/launcher/internal/runTtsc.ts +148 -117
- package/src/launcher/internal/runTtscserver.ts +633 -56
- package/src/launcher/internal/runTtsx.ts +87 -10
- package/src/launcher/internal/runtimeHooks.ts +16 -16
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2494 -74
- package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
- package/src/plugin/internal/loadProjectPlugins.ts +49 -8
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
- package/tools/shim_audit/baseline.json +0 -9
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"runtime"
|
|
11
|
+
"sort"
|
|
12
|
+
"strings"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// LSPProjectInputSnapshot is the normalized external filesystem topology
|
|
16
|
+
// published by project-rule contributors.
|
|
17
|
+
type LSPProjectInputSnapshot struct {
|
|
18
|
+
Root string `json:"root"`
|
|
19
|
+
Files []string `json:"files"`
|
|
20
|
+
Globs []string `json:"globs"`
|
|
21
|
+
ReloadFiles []string `json:"reloadFiles,omitempty"`
|
|
22
|
+
ReloadDirectories []string `json:"reloadDirectories,omitempty"`
|
|
23
|
+
ReloadFileDigests map[string]string `json:"reloadFileDigests,omitempty"`
|
|
24
|
+
ReloadDirectoryDigests map[string]string `json:"reloadDirectoryDigests,omitempty"`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type projectInputRecord struct {
|
|
28
|
+
generation uint64
|
|
29
|
+
snapshot LSPProjectInputSnapshot
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ProjectInputs returns a stable copy of the current merged dependency
|
|
33
|
+
// snapshot.
|
|
34
|
+
func (s *NativePluginSource) ProjectInputs() LSPProjectInputSnapshot {
|
|
35
|
+
if s == nil {
|
|
36
|
+
return LSPProjectInputSnapshot{}
|
|
37
|
+
}
|
|
38
|
+
s.projectInputsMu.RLock()
|
|
39
|
+
defer s.projectInputsMu.RUnlock()
|
|
40
|
+
return copyProjectInputSnapshot(s.projectInputs)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ProjectInputReloadFingerprintsAreCurrent reports whether the retained
|
|
44
|
+
// selection-time baseline still matches the filesystem. The proxy checks this
|
|
45
|
+
// after the client confirms dynamic watcher registration, closing the interval
|
|
46
|
+
// between construction-time validation and active event delivery.
|
|
47
|
+
func (s *NativePluginSource) ProjectInputReloadFingerprintsAreCurrent() bool {
|
|
48
|
+
if s == nil {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
s.projectInputsMu.RLock()
|
|
52
|
+
snapshot := copyProjectInputSnapshot(s.projectInputs)
|
|
53
|
+
s.projectInputsMu.RUnlock()
|
|
54
|
+
return projectInputReloadFingerprintsAreCurrent(snapshot)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ProjectInputMatchesURI reports whether a watched-file URI belongs to a
|
|
58
|
+
// declared exact dependency or glob population.
|
|
59
|
+
func (s *NativePluginSource) ProjectInputMatchesURI(uri string) bool {
|
|
60
|
+
if s == nil {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
location, ok := filePathFromURI(uri)
|
|
64
|
+
if !ok {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
s.projectInputsMu.RLock()
|
|
68
|
+
defer s.projectInputsMu.RUnlock()
|
|
69
|
+
return projectInputSnapshotMatchesCandidate(s.projectInputs, location)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ProjectInputReloadMatchesURI reports whether uri names an exact reload file,
|
|
73
|
+
// a reload directory, or one of that directory's immediate entries. Callers
|
|
74
|
+
// with an LSP change event should use ProjectInputReloadMatchesChange so an
|
|
75
|
+
// ordinary content edit inside a topology directory does not force a restart.
|
|
76
|
+
func (s *NativePluginSource) ProjectInputReloadMatchesURI(uri string) bool {
|
|
77
|
+
if s == nil {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
location, ok := filePathFromURI(uri)
|
|
81
|
+
if !ok {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
candidate := realProjectInputPath(location)
|
|
85
|
+
candidateEntry := realProjectInputEntryPath(location)
|
|
86
|
+
candidateKey := projectInputPathKey(candidate)
|
|
87
|
+
candidateEntryKey := projectInputPathKey(candidateEntry)
|
|
88
|
+
s.projectInputsMu.RLock()
|
|
89
|
+
defer s.projectInputsMu.RUnlock()
|
|
90
|
+
for _, file := range s.projectInputs.ReloadFiles {
|
|
91
|
+
// The candidate is resolved physically, so the declaration has to be too;
|
|
92
|
+
// a Windows short component or a symlinked ancestor otherwise never matches.
|
|
93
|
+
fileKey := projectInputPathKey(realProjectInputPath(file))
|
|
94
|
+
if fileKey == candidateKey || fileKey == candidateEntryKey {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for _, directory := range s.projectInputs.ReloadDirectories {
|
|
99
|
+
if projectInputPathKey(realProjectInputEntryPath(directory)) ==
|
|
100
|
+
candidateEntryKey {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
if projectInputDirectoryContainsImmediate(directory, candidateEntry) {
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ProjectInputReloadMatchesChange reports whether an LSP filesystem change
|
|
111
|
+
// invalidates executable plugin selection. Exact reload files are content
|
|
112
|
+
// inputs. Reload directories are topology inputs, so only a change to the
|
|
113
|
+
// directory itself always qualifies. An immediate entry qualifies only when
|
|
114
|
+
// the current name/type/symlink-target digest differs from the snapshot and
|
|
115
|
+
// the entry is not data territory under a declared glob's literal root.
|
|
116
|
+
func (s *NativePluginSource) ProjectInputReloadMatchesChange(
|
|
117
|
+
uri string,
|
|
118
|
+
changeType *int,
|
|
119
|
+
) bool {
|
|
120
|
+
if s == nil {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
location, ok := filePathFromURI(uri)
|
|
124
|
+
if !ok {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
candidate := realProjectInputPath(location)
|
|
128
|
+
candidateEntry := realProjectInputEntryPath(location)
|
|
129
|
+
candidateKey := projectInputPathKey(candidate)
|
|
130
|
+
candidateEntryKey := projectInputPathKey(candidateEntry)
|
|
131
|
+
s.projectInputsMu.RLock()
|
|
132
|
+
snapshot := copyProjectInputSnapshot(s.projectInputs)
|
|
133
|
+
s.projectInputsMu.RUnlock()
|
|
134
|
+
for _, file := range snapshot.ReloadFiles {
|
|
135
|
+
// The candidate is resolved physically, so the declaration has to be too;
|
|
136
|
+
// a Windows short component or a symlinked ancestor otherwise never matches.
|
|
137
|
+
fileKey := projectInputPathKey(realProjectInputPath(file))
|
|
138
|
+
if fileKey == candidateKey || fileKey == candidateEntryKey {
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
matched := false
|
|
143
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
144
|
+
if projectInputPathKey(realProjectInputEntryPath(directory)) ==
|
|
145
|
+
candidateEntryKey {
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
if !projectInputDirectoryContainsImmediate(directory, candidateEntry) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
key := projectInputPathKey(directory)
|
|
152
|
+
// The digest map is keyed by the declared spelling, so that lookup keeps
|
|
153
|
+
// it. The identity comparison is against a physically resolved candidate
|
|
154
|
+
// and has to resolve too, or a directory recreated with identical topology
|
|
155
|
+
// under an aliased spelling would fall through to the digest compare and
|
|
156
|
+
// report no change at all.
|
|
157
|
+
if projectInputPathKey(realProjectInputPath(directory)) ==
|
|
158
|
+
candidateEntryKey {
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
// A glob root appearing immediately inside a resolution directory is a
|
|
162
|
+
// data-population transition, not a plugin-selection transition. The data
|
|
163
|
+
// watcher already reports it and invalidates the Program in the resident
|
|
164
|
+
// process. Only a root strictly below this directory can explain away its
|
|
165
|
+
// digest delta; a glob rooted on or above the directory cannot.
|
|
166
|
+
if projectInputGlobExemptsReloadEntry(
|
|
167
|
+
snapshot,
|
|
168
|
+
directory,
|
|
169
|
+
candidate,
|
|
170
|
+
) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if projectInputReloadDirectoryDigest(directory) !=
|
|
174
|
+
snapshot.ReloadDirectoryDigests[key] {
|
|
175
|
+
matched = true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return matched
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ProjectInputOwnersForURI returns the stable plugin keys whose latest
|
|
182
|
+
// successful snapshots match uri. Ownership is retained past the flattened
|
|
183
|
+
// client registration so an external edit refreshes only the contributors that
|
|
184
|
+
// declared it.
|
|
185
|
+
func (s *NativePluginSource) ProjectInputOwnersForURI(uri string) []string {
|
|
186
|
+
if s == nil {
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
location, ok := filePathFromURI(uri)
|
|
190
|
+
if !ok {
|
|
191
|
+
return nil
|
|
192
|
+
}
|
|
193
|
+
s.projectInputsMu.RLock()
|
|
194
|
+
defer s.projectInputsMu.RUnlock()
|
|
195
|
+
owners := []string{}
|
|
196
|
+
for _, plugin := range selectPluginTransports(
|
|
197
|
+
s.plugins,
|
|
198
|
+
nil,
|
|
199
|
+
s.projectContextJSON,
|
|
200
|
+
) {
|
|
201
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
202
|
+
record, ok := s.pluginProjectInputs[key]
|
|
203
|
+
if !ok || !projectInputSnapshotMatchesCandidate(record.snapshot, location) {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
owners = append(owners, key)
|
|
207
|
+
}
|
|
208
|
+
return owners
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// projectInputSnapshotMatchesCandidate compares each declaration and candidate
|
|
212
|
+
// in one filesystem identity domain.
|
|
213
|
+
//
|
|
214
|
+
// An editor URI may use a Windows short component or a symlinked ancestor such
|
|
215
|
+
// as macOS `/var`. Exact files resolve both spellings physically. Glob matching
|
|
216
|
+
// additionally retains the candidate path so Windows can apply the semantics
|
|
217
|
+
// of the directory that owns each matched segment.
|
|
218
|
+
func projectInputSnapshotMatchesCandidate(
|
|
219
|
+
snapshot LSPProjectInputSnapshot,
|
|
220
|
+
candidate string,
|
|
221
|
+
) bool {
|
|
222
|
+
candidateKey := projectInputPathKey(realProjectInputPath(candidate))
|
|
223
|
+
for _, file := range snapshot.Files {
|
|
224
|
+
if projectInputPathKey(realProjectInputPath(file)) == candidateKey {
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for _, pattern := range snapshot.Globs {
|
|
229
|
+
if projectInputGlobMatchesCandidate(pattern, candidate) {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// RefreshProjectInputs schedules a coalesced dependency rediscovery after a
|
|
237
|
+
// configuration input changes.
|
|
238
|
+
func (s *NativePluginSource) RefreshProjectInputs() {
|
|
239
|
+
if s == nil {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
s.projectInputsRefresh.schedule(s.discoverProjectInputs)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// SetProjectInputsObserver registers the proxy callback that replaces the
|
|
246
|
+
// client's dynamic watched-file registration after a successful topology
|
|
247
|
+
// change.
|
|
248
|
+
func (s *NativePluginSource) SetProjectInputsObserver(observer func()) {
|
|
249
|
+
if s == nil {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
s.projectInputsMu.Lock()
|
|
253
|
+
s.projectInputsObserver = observer
|
|
254
|
+
s.projectInputsMu.Unlock()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func (s *NativePluginSource) discoverProjectInputs(generation uint64) {
|
|
258
|
+
changed := false
|
|
259
|
+
for _, plugin := range selectPluginTransports(
|
|
260
|
+
s.plugins,
|
|
261
|
+
func(plugin NativeLSPPluginEntry) bool {
|
|
262
|
+
return plugin.ProjectInputs
|
|
263
|
+
},
|
|
264
|
+
s.projectContextJSON,
|
|
265
|
+
) {
|
|
266
|
+
body, err := s.run(plugin, "project-inputs")
|
|
267
|
+
if err != nil {
|
|
268
|
+
s.log("%v", err)
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
var snapshot LSPProjectInputSnapshot
|
|
272
|
+
if err := json.Unmarshal(body, &snapshot); err != nil {
|
|
273
|
+
s.log(
|
|
274
|
+
"ttscserver: %s project-inputs returned invalid JSON: %v",
|
|
275
|
+
pluginLabel(plugin),
|
|
276
|
+
err,
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
snapshot, err = normalizeLSPProjectInputSnapshot(snapshot, s.cwd)
|
|
281
|
+
if err != nil {
|
|
282
|
+
s.log(
|
|
283
|
+
"ttscserver: %s project-inputs returned an invalid snapshot: %v",
|
|
284
|
+
pluginLabel(plugin),
|
|
285
|
+
err,
|
|
286
|
+
)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
if s.storeProjectInputs(plugin, generation, snapshot) {
|
|
290
|
+
changed = true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if changed {
|
|
294
|
+
s.projectInputsMu.RLock()
|
|
295
|
+
observer := s.projectInputsObserver
|
|
296
|
+
s.projectInputsMu.RUnlock()
|
|
297
|
+
if observer != nil {
|
|
298
|
+
observer()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func (s *NativePluginSource) storeProjectInputs(
|
|
304
|
+
plugin NativeLSPPluginEntry,
|
|
305
|
+
generation uint64,
|
|
306
|
+
snapshot LSPProjectInputSnapshot,
|
|
307
|
+
) bool {
|
|
308
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
309
|
+
s.projectInputsMu.Lock()
|
|
310
|
+
defer s.projectInputsMu.Unlock()
|
|
311
|
+
if existing, ok := s.pluginProjectInputs[key]; ok &&
|
|
312
|
+
generation < existing.generation {
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
if s.pluginProjectInputs == nil {
|
|
316
|
+
s.pluginProjectInputs = map[string]projectInputRecord{}
|
|
317
|
+
}
|
|
318
|
+
existing, existed := s.pluginProjectInputs[key]
|
|
319
|
+
if existed {
|
|
320
|
+
preserveProjectInputReloadFingerprints(existing.snapshot, &snapshot)
|
|
321
|
+
}
|
|
322
|
+
s.pluginProjectInputs[key] = projectInputRecord{
|
|
323
|
+
generation: generation,
|
|
324
|
+
snapshot: snapshot,
|
|
325
|
+
}
|
|
326
|
+
if existed && projectInputSnapshotsEqual(existing.snapshot, snapshot) {
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
s.projectInputs = s.flattenProjectInputsLocked()
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func (s *NativePluginSource) flattenProjectInputsLocked() LSPProjectInputSnapshot {
|
|
334
|
+
files := map[string]string{}
|
|
335
|
+
globs := map[string]string{}
|
|
336
|
+
reloadFiles := map[string]string{}
|
|
337
|
+
reloadDirectories := map[string]string{}
|
|
338
|
+
reloadFileDigests := map[string]string{}
|
|
339
|
+
reloadDirectoryDigests := map[string]string{}
|
|
340
|
+
root := ""
|
|
341
|
+
for _, plugin := range selectPluginTransports(
|
|
342
|
+
s.plugins,
|
|
343
|
+
nil,
|
|
344
|
+
s.projectContextJSON,
|
|
345
|
+
) {
|
|
346
|
+
key := pluginKey(plugin, s.projectContextJSON)
|
|
347
|
+
snapshot := s.pluginProjectInputs[key].snapshot
|
|
348
|
+
if root == "" && snapshot.Root != "" {
|
|
349
|
+
root = snapshot.Root
|
|
350
|
+
}
|
|
351
|
+
for _, file := range snapshot.Files {
|
|
352
|
+
files[projectInputPathKey(file)] = file
|
|
353
|
+
}
|
|
354
|
+
for _, pattern := range snapshot.Globs {
|
|
355
|
+
globs[projectInputPathKey(pattern)] = pattern
|
|
356
|
+
}
|
|
357
|
+
for _, file := range snapshot.ReloadFiles {
|
|
358
|
+
fileKey := projectInputPathKey(file)
|
|
359
|
+
reloadFiles[fileKey] = file
|
|
360
|
+
reloadFileDigests[fileKey] = snapshot.ReloadFileDigests[fileKey]
|
|
361
|
+
}
|
|
362
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
363
|
+
directoryKey := projectInputPathKey(directory)
|
|
364
|
+
reloadDirectories[directoryKey] = directory
|
|
365
|
+
reloadDirectoryDigests[directoryKey] =
|
|
366
|
+
snapshot.ReloadDirectoryDigests[directoryKey]
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
out := LSPProjectInputSnapshot{
|
|
370
|
+
Root: root,
|
|
371
|
+
ReloadFileDigests: reloadFileDigests,
|
|
372
|
+
ReloadDirectoryDigests: reloadDirectoryDigests,
|
|
373
|
+
}
|
|
374
|
+
for _, file := range files {
|
|
375
|
+
out.Files = append(out.Files, file)
|
|
376
|
+
}
|
|
377
|
+
for _, pattern := range globs {
|
|
378
|
+
out.Globs = append(out.Globs, pattern)
|
|
379
|
+
}
|
|
380
|
+
for _, file := range reloadFiles {
|
|
381
|
+
out.ReloadFiles = append(out.ReloadFiles, file)
|
|
382
|
+
}
|
|
383
|
+
for _, directory := range reloadDirectories {
|
|
384
|
+
out.ReloadDirectories = append(out.ReloadDirectories, directory)
|
|
385
|
+
}
|
|
386
|
+
sort.Strings(out.Files)
|
|
387
|
+
sort.Strings(out.Globs)
|
|
388
|
+
sort.Strings(out.ReloadFiles)
|
|
389
|
+
sort.Strings(out.ReloadDirectories)
|
|
390
|
+
return out
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
func normalizeLSPProjectInputSnapshot(
|
|
394
|
+
snapshot LSPProjectInputSnapshot,
|
|
395
|
+
expectedRoot string,
|
|
396
|
+
) (LSPProjectInputSnapshot, error) {
|
|
397
|
+
if strings.TrimSpace(snapshot.Root) == "" ||
|
|
398
|
+
!isAbsoluteLocalLSPProjectInputPath(snapshot.Root, runtime.GOOS) {
|
|
399
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
400
|
+
"root %q is not an absolute local path",
|
|
401
|
+
snapshot.Root,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
root := filepath.ToSlash(realProjectInputPath(snapshot.Root))
|
|
405
|
+
if strings.TrimSpace(expectedRoot) != "" &&
|
|
406
|
+
projectInputPathKey(root) !=
|
|
407
|
+
projectInputPathKey(realProjectInputPath(expectedRoot)) {
|
|
408
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
409
|
+
"root %q differs from selected project root %q",
|
|
410
|
+
root,
|
|
411
|
+
filepath.ToSlash(realProjectInputPath(expectedRoot)),
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
files := map[string]string{}
|
|
415
|
+
for _, file := range snapshot.Files {
|
|
416
|
+
if strings.TrimSpace(file) == "" ||
|
|
417
|
+
!isAbsoluteLocalLSPProjectInputPath(file, runtime.GOOS) {
|
|
418
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
419
|
+
"file %q is not an absolute local path",
|
|
420
|
+
file,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
normalized := filepath.ToSlash(realProjectInputPath(file))
|
|
424
|
+
files[projectInputPathKey(normalized)] = normalized
|
|
425
|
+
}
|
|
426
|
+
reloadFiles := map[string]string{}
|
|
427
|
+
for _, file := range snapshot.ReloadFiles {
|
|
428
|
+
if strings.TrimSpace(file) == "" ||
|
|
429
|
+
!isAbsoluteLocalLSPProjectInputPath(file, runtime.GOOS) {
|
|
430
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
431
|
+
"reload file %q is not an absolute local path",
|
|
432
|
+
file,
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
normalized := filepath.ToSlash(realProjectInputEntryPath(file))
|
|
436
|
+
reloadFiles[projectInputPathKey(normalized)] = normalized
|
|
437
|
+
}
|
|
438
|
+
reloadDirectories := map[string]string{}
|
|
439
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
440
|
+
if strings.TrimSpace(directory) == "" ||
|
|
441
|
+
!isAbsoluteLocalLSPProjectInputPath(directory, runtime.GOOS) {
|
|
442
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
443
|
+
"reload directory %q is not an absolute local path",
|
|
444
|
+
directory,
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
for _, normalized := range []string{
|
|
448
|
+
filepath.ToSlash(realProjectInputPath(directory)),
|
|
449
|
+
filepath.ToSlash(realProjectInputEntryPath(directory)),
|
|
450
|
+
} {
|
|
451
|
+
reloadDirectories[projectInputPathKey(normalized)] = normalized
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
globs := map[string]string{}
|
|
455
|
+
for _, pattern := range snapshot.Globs {
|
|
456
|
+
if strings.TrimSpace(pattern) == "" ||
|
|
457
|
+
!isAbsoluteLocalLSPProjectInputPath(pattern, runtime.GOOS) {
|
|
458
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
459
|
+
"glob %q is not an absolute local path",
|
|
460
|
+
pattern,
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
// Every other lane in this function resolves its declaration physically,
|
|
464
|
+
// and a glob has to as well or its population is compared against events
|
|
465
|
+
// that were. A pattern has no filesystem object of its own, so resolution
|
|
466
|
+
// takes its longest existing prefix and rejoins the wildcards, which leaves
|
|
467
|
+
// the pattern's shape untouched while its root gains one identity.
|
|
468
|
+
normalized := filepath.ToSlash(realProjectInputPath(pattern))
|
|
469
|
+
globs[projectInputPathKey(normalized)] = normalized
|
|
470
|
+
}
|
|
471
|
+
out := LSPProjectInputSnapshot{
|
|
472
|
+
Root: root,
|
|
473
|
+
ReloadFileDigests: map[string]string{},
|
|
474
|
+
ReloadDirectoryDigests: map[string]string{},
|
|
475
|
+
}
|
|
476
|
+
for _, file := range files {
|
|
477
|
+
out.Files = append(out.Files, file)
|
|
478
|
+
}
|
|
479
|
+
for _, pattern := range globs {
|
|
480
|
+
out.Globs = append(out.Globs, pattern)
|
|
481
|
+
}
|
|
482
|
+
for _, file := range reloadFiles {
|
|
483
|
+
out.ReloadFiles = append(out.ReloadFiles, file)
|
|
484
|
+
key := projectInputPathKey(file)
|
|
485
|
+
digest, ok := projectInputSnapshotDigest(snapshot.ReloadFileDigests, file)
|
|
486
|
+
if !ok {
|
|
487
|
+
if snapshot.ReloadFileDigests != nil {
|
|
488
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
489
|
+
"reload file %q is missing its selection fingerprint",
|
|
490
|
+
file,
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
digest = projectInputReloadFileDigest(file)
|
|
494
|
+
}
|
|
495
|
+
if !validProjectInputFingerprint(digest) {
|
|
496
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
497
|
+
"reload file %q has an invalid fingerprint",
|
|
498
|
+
file,
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
out.ReloadFileDigests[key] = digest
|
|
502
|
+
}
|
|
503
|
+
for _, directory := range reloadDirectories {
|
|
504
|
+
out.ReloadDirectories = append(out.ReloadDirectories, directory)
|
|
505
|
+
key := projectInputPathKey(directory)
|
|
506
|
+
digest, ok := projectInputSnapshotDigest(
|
|
507
|
+
snapshot.ReloadDirectoryDigests,
|
|
508
|
+
directory,
|
|
509
|
+
)
|
|
510
|
+
if !ok {
|
|
511
|
+
if snapshot.ReloadDirectoryDigests != nil {
|
|
512
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
513
|
+
"reload directory %q is missing its selection fingerprint",
|
|
514
|
+
directory,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
digest = projectInputReloadDirectoryDigest(directory)
|
|
518
|
+
}
|
|
519
|
+
if !validProjectInputFingerprint(digest) {
|
|
520
|
+
return LSPProjectInputSnapshot{}, fmt.Errorf(
|
|
521
|
+
"reload directory %q has an invalid fingerprint",
|
|
522
|
+
directory,
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
out.ReloadDirectoryDigests[key] = digest
|
|
526
|
+
}
|
|
527
|
+
sort.Strings(out.Files)
|
|
528
|
+
sort.Strings(out.Globs)
|
|
529
|
+
sort.Strings(out.ReloadFiles)
|
|
530
|
+
sort.Strings(out.ReloadDirectories)
|
|
531
|
+
return out, nil
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
func isAbsoluteLocalLSPProjectInputPath(
|
|
535
|
+
location string,
|
|
536
|
+
goos string,
|
|
537
|
+
) bool {
|
|
538
|
+
if strings.ContainsRune(location, '\x00') {
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
if goos != "windows" {
|
|
542
|
+
return path.IsAbs(location)
|
|
543
|
+
}
|
|
544
|
+
normalized := strings.ReplaceAll(location, "/", `\`)
|
|
545
|
+
if strings.HasPrefix(normalized, `\\?\`) {
|
|
546
|
+
extended := strings.TrimPrefix(normalized, `\\?\`)
|
|
547
|
+
if isWindowsDrivePath(extended) {
|
|
548
|
+
return true
|
|
549
|
+
}
|
|
550
|
+
if strings.HasPrefix(strings.ToLower(extended), `unc\`) {
|
|
551
|
+
return isWindowsUNCPath(extended[4:])
|
|
552
|
+
}
|
|
553
|
+
return false
|
|
554
|
+
}
|
|
555
|
+
if strings.HasPrefix(normalized, `\\.\`) {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
return isWindowsDrivePath(normalized) ||
|
|
559
|
+
(strings.HasPrefix(normalized, `\\`) &&
|
|
560
|
+
isWindowsUNCPath(strings.TrimPrefix(normalized, `\\`)))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
func isWindowsDrivePath(location string) bool {
|
|
564
|
+
return len(location) >= 3 &&
|
|
565
|
+
((location[0] >= 'A' && location[0] <= 'Z') ||
|
|
566
|
+
(location[0] >= 'a' && location[0] <= 'z')) &&
|
|
567
|
+
location[1] == ':' &&
|
|
568
|
+
location[2] == '\\'
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
func isWindowsUNCPath(location string) bool {
|
|
572
|
+
components := strings.Split(location, `\`)
|
|
573
|
+
return len(components) >= 2 &&
|
|
574
|
+
isWindowsUNCVolumeSegment(components[0]) &&
|
|
575
|
+
isWindowsUNCVolumeSegment(components[1])
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
func isWindowsUNCVolumeSegment(segment string) bool {
|
|
579
|
+
return segment != "" &&
|
|
580
|
+
segment != "." &&
|
|
581
|
+
segment != ".." &&
|
|
582
|
+
!strings.ContainsAny(segment, "\x00<>:\"/\\|?*")
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
func copyProjectInputSnapshot(
|
|
586
|
+
snapshot LSPProjectInputSnapshot,
|
|
587
|
+
) LSPProjectInputSnapshot {
|
|
588
|
+
copied := LSPProjectInputSnapshot{
|
|
589
|
+
Root: snapshot.Root,
|
|
590
|
+
Files: append([]string(nil), snapshot.Files...),
|
|
591
|
+
Globs: append([]string(nil), snapshot.Globs...),
|
|
592
|
+
ReloadFiles: append([]string(nil), snapshot.ReloadFiles...),
|
|
593
|
+
ReloadDirectories: append([]string(nil), snapshot.ReloadDirectories...),
|
|
594
|
+
}
|
|
595
|
+
if snapshot.ReloadFileDigests != nil {
|
|
596
|
+
copied.ReloadFileDigests = make(
|
|
597
|
+
map[string]string,
|
|
598
|
+
len(snapshot.ReloadFileDigests),
|
|
599
|
+
)
|
|
600
|
+
for key, digest := range snapshot.ReloadFileDigests {
|
|
601
|
+
copied.ReloadFileDigests[key] = digest
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if snapshot.ReloadDirectoryDigests != nil {
|
|
605
|
+
copied.ReloadDirectoryDigests = make(
|
|
606
|
+
map[string]string,
|
|
607
|
+
len(snapshot.ReloadDirectoryDigests),
|
|
608
|
+
)
|
|
609
|
+
for key, digest := range snapshot.ReloadDirectoryDigests {
|
|
610
|
+
copied.ReloadDirectoryDigests[key] = digest
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return copied
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
func projectInputSnapshotsEqual(
|
|
617
|
+
left LSPProjectInputSnapshot,
|
|
618
|
+
right LSPProjectInputSnapshot,
|
|
619
|
+
) bool {
|
|
620
|
+
if projectInputPathKey(left.Root) != projectInputPathKey(right.Root) ||
|
|
621
|
+
len(left.Files) != len(right.Files) ||
|
|
622
|
+
len(left.Globs) != len(right.Globs) ||
|
|
623
|
+
len(left.ReloadFiles) != len(right.ReloadFiles) ||
|
|
624
|
+
len(left.ReloadDirectories) != len(right.ReloadDirectories) {
|
|
625
|
+
return false
|
|
626
|
+
}
|
|
627
|
+
for index := range left.Files {
|
|
628
|
+
if projectInputPathKey(left.Files[index]) !=
|
|
629
|
+
projectInputPathKey(right.Files[index]) {
|
|
630
|
+
return false
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for index := range left.Globs {
|
|
634
|
+
if projectInputPathKey(left.Globs[index]) !=
|
|
635
|
+
projectInputPathKey(right.Globs[index]) {
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for index := range left.ReloadFiles {
|
|
640
|
+
if projectInputPathKey(left.ReloadFiles[index]) !=
|
|
641
|
+
projectInputPathKey(right.ReloadFiles[index]) {
|
|
642
|
+
return false
|
|
643
|
+
}
|
|
644
|
+
key := projectInputPathKey(left.ReloadFiles[index])
|
|
645
|
+
if left.ReloadFileDigests[key] != right.ReloadFileDigests[key] {
|
|
646
|
+
return false
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for index := range left.ReloadDirectories {
|
|
650
|
+
if projectInputPathKey(left.ReloadDirectories[index]) !=
|
|
651
|
+
projectInputPathKey(right.ReloadDirectories[index]) {
|
|
652
|
+
return false
|
|
653
|
+
}
|
|
654
|
+
key := projectInputPathKey(left.ReloadDirectories[index])
|
|
655
|
+
if left.ReloadDirectoryDigests[key] != right.ReloadDirectoryDigests[key] {
|
|
656
|
+
return false
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return true
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
func projectInputDirectoryContainsImmediate(
|
|
663
|
+
directory string,
|
|
664
|
+
candidate string,
|
|
665
|
+
) bool {
|
|
666
|
+
// Both operands are resolved physically before they are compared. Callers
|
|
667
|
+
// pass a candidate that already went through that resolution, so leaving the
|
|
668
|
+
// declared directory lexical would never match it under a Windows short
|
|
669
|
+
// component or a symlinked ancestor.
|
|
670
|
+
resolvedDirectory := realProjectInputPath(directory)
|
|
671
|
+
relative, within := projectInputPathIdentityRelative(
|
|
672
|
+
resolvedDirectory,
|
|
673
|
+
candidate,
|
|
674
|
+
)
|
|
675
|
+
return within && !strings.Contains(relative, "/")
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
func projectInputGlobExemptsReloadEntry(
|
|
679
|
+
snapshot LSPProjectInputSnapshot,
|
|
680
|
+
directory string,
|
|
681
|
+
candidate string,
|
|
682
|
+
) bool {
|
|
683
|
+
resolvedDirectory := realProjectInputPath(directory)
|
|
684
|
+
directoryKey := projectInputPathKey(resolvedDirectory)
|
|
685
|
+
for _, pattern := range snapshot.Globs {
|
|
686
|
+
globRoot, _ := projectInputGlobRelativePattern(pattern)
|
|
687
|
+
resolvedGlobRoot := realProjectInputPath(globRoot)
|
|
688
|
+
if projectInputPathKey(resolvedGlobRoot) == directoryKey {
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
691
|
+
if projectInputPathContains(resolvedDirectory, resolvedGlobRoot) &&
|
|
692
|
+
projectInputPathContains(resolvedGlobRoot, candidate) {
|
|
693
|
+
return true
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return false
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
func projectInputPathContains(
|
|
700
|
+
directory string,
|
|
701
|
+
candidate string,
|
|
702
|
+
) bool {
|
|
703
|
+
_, within := projectInputPathIdentityRelative(directory, candidate)
|
|
704
|
+
return within
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
func projectInputPathIdentityRelative(
|
|
708
|
+
directory string,
|
|
709
|
+
candidate string,
|
|
710
|
+
) (string, bool) {
|
|
711
|
+
root := strings.TrimRight(projectInputPathKey(directory), "/")
|
|
712
|
+
location := strings.TrimRight(projectInputPathKey(candidate), "/")
|
|
713
|
+
if root == location {
|
|
714
|
+
return "", true
|
|
715
|
+
}
|
|
716
|
+
prefix := root + "/"
|
|
717
|
+
if !strings.HasPrefix(location, prefix) {
|
|
718
|
+
return "", false
|
|
719
|
+
}
|
|
720
|
+
return strings.TrimPrefix(location, prefix), true
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
func preserveProjectInputReloadFingerprints(
|
|
724
|
+
baseline LSPProjectInputSnapshot,
|
|
725
|
+
current *LSPProjectInputSnapshot,
|
|
726
|
+
) {
|
|
727
|
+
if current == nil {
|
|
728
|
+
return
|
|
729
|
+
}
|
|
730
|
+
for _, file := range current.ReloadFiles {
|
|
731
|
+
key := projectInputPathKey(file)
|
|
732
|
+
if digest := baseline.ReloadFileDigests[key]; digest != "" {
|
|
733
|
+
current.ReloadFileDigests[key] = digest
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for _, directory := range current.ReloadDirectories {
|
|
737
|
+
key := projectInputPathKey(directory)
|
|
738
|
+
if digest := baseline.ReloadDirectoryDigests[key]; digest != "" {
|
|
739
|
+
current.ReloadDirectoryDigests[key] = digest
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
func projectInputReloadFingerprintsAreCurrent(
|
|
745
|
+
snapshot LSPProjectInputSnapshot,
|
|
746
|
+
) bool {
|
|
747
|
+
for _, file := range snapshot.ReloadFiles {
|
|
748
|
+
key := projectInputPathKey(file)
|
|
749
|
+
if snapshot.ReloadFileDigests[key] != projectInputReloadFileDigest(file) {
|
|
750
|
+
return false
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
for _, directory := range snapshot.ReloadDirectories {
|
|
754
|
+
key := projectInputPathKey(directory)
|
|
755
|
+
if snapshot.ReloadDirectoryDigests[key] !=
|
|
756
|
+
projectInputReloadDirectoryDigest(directory) {
|
|
757
|
+
return false
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return true
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
func projectInputSnapshotDigest(
|
|
764
|
+
fingerprints map[string]string,
|
|
765
|
+
location string,
|
|
766
|
+
) (string, bool) {
|
|
767
|
+
if fingerprints == nil {
|
|
768
|
+
return "", false
|
|
769
|
+
}
|
|
770
|
+
if digest, ok := fingerprints[location]; ok {
|
|
771
|
+
return digest, true
|
|
772
|
+
}
|
|
773
|
+
wanted := projectInputPathKey(location)
|
|
774
|
+
for candidate, digest := range fingerprints {
|
|
775
|
+
if projectInputPathKey(realProjectInputPath(candidate)) == wanted ||
|
|
776
|
+
projectInputPathKey(realProjectInputEntryPath(candidate)) == wanted {
|
|
777
|
+
return digest, true
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return "", false
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
func validProjectInputFingerprint(digest string) bool {
|
|
784
|
+
if len(digest) != sha256.Size*2 || strings.ToLower(digest) != digest {
|
|
785
|
+
return false
|
|
786
|
+
}
|
|
787
|
+
for _, char := range digest {
|
|
788
|
+
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
|
789
|
+
return false
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return true
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
func projectInputReloadDirectoryDigest(directory string) string {
|
|
796
|
+
topology := projectInputReloadDirectoryTopologyDigest(directory)
|
|
797
|
+
digest := sha256.New()
|
|
798
|
+
digest.Write([]byte("directory\x00"))
|
|
799
|
+
digest.Write([]byte(projectInputPhysicalPathKey(directory)))
|
|
800
|
+
digest.Write([]byte{0})
|
|
801
|
+
digest.Write([]byte(topology))
|
|
802
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
func projectInputReloadDirectoryTopologyDigest(directory string) string {
|
|
806
|
+
entries, err := os.ReadDir(projectInputFilesystemPath(directory))
|
|
807
|
+
if err != nil {
|
|
808
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
809
|
+
return fmt.Sprintf("%x", missing[:])
|
|
810
|
+
}
|
|
811
|
+
digest := sha256.New()
|
|
812
|
+
for index, entry := range entries {
|
|
813
|
+
kind := "other"
|
|
814
|
+
info, err := entry.Info()
|
|
815
|
+
if err != nil {
|
|
816
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
817
|
+
return fmt.Sprintf("%x", missing[:])
|
|
818
|
+
}
|
|
819
|
+
switch {
|
|
820
|
+
case info.Mode()&os.ModeSymlink != 0:
|
|
821
|
+
kind = "symlink"
|
|
822
|
+
case info.IsDir():
|
|
823
|
+
kind = "directory"
|
|
824
|
+
case info.Mode().IsRegular():
|
|
825
|
+
kind = "file"
|
|
826
|
+
}
|
|
827
|
+
target := ""
|
|
828
|
+
if kind == "symlink" {
|
|
829
|
+
target, err = os.Readlink(
|
|
830
|
+
filepath.Join(projectInputFilesystemPath(directory), entry.Name()),
|
|
831
|
+
)
|
|
832
|
+
if err != nil {
|
|
833
|
+
target = "<unreadable>"
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
digest.Write([]byte(entry.Name()))
|
|
837
|
+
digest.Write([]byte{0})
|
|
838
|
+
digest.Write([]byte(kind))
|
|
839
|
+
digest.Write([]byte{0})
|
|
840
|
+
digest.Write([]byte(target))
|
|
841
|
+
if index+1 != len(entries) {
|
|
842
|
+
digest.Write([]byte{0})
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
func projectInputReloadFileDigest(location string) string {
|
|
849
|
+
native := projectInputFilesystemPath(location)
|
|
850
|
+
info, err := os.Lstat(native)
|
|
851
|
+
if err != nil {
|
|
852
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
853
|
+
return fmt.Sprintf("%x", missing[:])
|
|
854
|
+
}
|
|
855
|
+
if info.Mode()&os.ModeSymlink != 0 {
|
|
856
|
+
target, err := os.Readlink(native)
|
|
857
|
+
if err != nil {
|
|
858
|
+
target = "<unreadable>"
|
|
859
|
+
}
|
|
860
|
+
content := []byte("missing\x00")
|
|
861
|
+
if body, readErr := os.ReadFile(native); readErr == nil {
|
|
862
|
+
content = append([]byte("file\x00"), body...)
|
|
863
|
+
}
|
|
864
|
+
digest := sha256.New()
|
|
865
|
+
digest.Write([]byte("symlink\x00"))
|
|
866
|
+
digest.Write([]byte(target))
|
|
867
|
+
digest.Write([]byte{0})
|
|
868
|
+
digest.Write(content)
|
|
869
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
870
|
+
}
|
|
871
|
+
if info.Mode().IsRegular() {
|
|
872
|
+
body, err := os.ReadFile(native)
|
|
873
|
+
if err != nil {
|
|
874
|
+
missing := sha256.Sum256([]byte("missing\x00"))
|
|
875
|
+
return fmt.Sprintf("%x", missing[:])
|
|
876
|
+
}
|
|
877
|
+
digest := sha256.New()
|
|
878
|
+
digest.Write([]byte("file\x00"))
|
|
879
|
+
digest.Write(body)
|
|
880
|
+
return fmt.Sprintf("%x", digest.Sum(nil))
|
|
881
|
+
}
|
|
882
|
+
other := sha256.Sum256([]byte("other\x00"))
|
|
883
|
+
return fmt.Sprintf("%x", other[:])
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
func realProjectInputPath(location string) string {
|
|
887
|
+
absolute, err := filepath.Abs(projectInputFilesystemPath(location))
|
|
888
|
+
if err != nil {
|
|
889
|
+
return filepath.Clean(filepath.FromSlash(location))
|
|
890
|
+
}
|
|
891
|
+
probe := absolute
|
|
892
|
+
suffix := []string{}
|
|
893
|
+
for {
|
|
894
|
+
resolved, err := physicalProjectInputPath(probe)
|
|
895
|
+
if err == nil {
|
|
896
|
+
for index := len(suffix) - 1; index >= 0; index-- {
|
|
897
|
+
resolved = filepath.Join(resolved, suffix[index])
|
|
898
|
+
}
|
|
899
|
+
return filepath.Clean(resolved)
|
|
900
|
+
}
|
|
901
|
+
parent := filepath.Dir(probe)
|
|
902
|
+
if parent == probe {
|
|
903
|
+
return filepath.Clean(absolute)
|
|
904
|
+
}
|
|
905
|
+
suffix = append(suffix, filepath.Base(probe))
|
|
906
|
+
probe = parent
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
func realProjectInputEntryPath(location string) string {
|
|
911
|
+
native := projectInputFilesystemPath(location)
|
|
912
|
+
return filepath.Join(
|
|
913
|
+
realProjectInputPath(filepath.Dir(native)),
|
|
914
|
+
filepath.Base(native),
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
func projectInputFilesystemPath(location string) string {
|
|
919
|
+
if runtime.GOOS != "windows" {
|
|
920
|
+
return filepath.FromSlash(location)
|
|
921
|
+
}
|
|
922
|
+
normalized := strings.ReplaceAll(location, "/", `\`)
|
|
923
|
+
if strings.HasPrefix(strings.ToLower(normalized), `\\?\unc\`) {
|
|
924
|
+
return `\\` + normalized[8:]
|
|
925
|
+
}
|
|
926
|
+
if strings.HasPrefix(normalized, `\\?\`) &&
|
|
927
|
+
isWindowsDrivePath(normalized[4:]) {
|
|
928
|
+
return normalized[4:]
|
|
929
|
+
}
|
|
930
|
+
return normalized
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
func matchProjectInputGlob(
|
|
934
|
+
pattern []string,
|
|
935
|
+
candidate []string,
|
|
936
|
+
candidateCaseSensitive []bool,
|
|
937
|
+
) bool {
|
|
938
|
+
type position struct {
|
|
939
|
+
pattern int
|
|
940
|
+
candidate int
|
|
941
|
+
}
|
|
942
|
+
memo := map[position]bool{}
|
|
943
|
+
visited := map[position]bool{}
|
|
944
|
+
var visit func(int, int) bool
|
|
945
|
+
visit = func(patternIndex int, candidateIndex int) bool {
|
|
946
|
+
key := position{pattern: patternIndex, candidate: candidateIndex}
|
|
947
|
+
if visited[key] {
|
|
948
|
+
return memo[key]
|
|
949
|
+
}
|
|
950
|
+
visited[key] = true
|
|
951
|
+
var matched bool
|
|
952
|
+
switch {
|
|
953
|
+
case patternIndex == len(pattern):
|
|
954
|
+
matched = candidateIndex == len(candidate)
|
|
955
|
+
case pattern[patternIndex] == "**":
|
|
956
|
+
matched = visit(patternIndex+1, candidateIndex) ||
|
|
957
|
+
(candidateIndex != len(candidate) &&
|
|
958
|
+
visit(patternIndex, candidateIndex+1))
|
|
959
|
+
case candidateIndex != len(candidate):
|
|
960
|
+
sensitive :=
|
|
961
|
+
candidateIndex >= len(candidateCaseSensitive) ||
|
|
962
|
+
candidateCaseSensitive[candidateIndex]
|
|
963
|
+
matched = matchProjectInputGlobSegment(
|
|
964
|
+
pattern[patternIndex],
|
|
965
|
+
candidate[candidateIndex],
|
|
966
|
+
sensitive,
|
|
967
|
+
) && visit(patternIndex+1, candidateIndex+1)
|
|
968
|
+
}
|
|
969
|
+
memo[key] = matched
|
|
970
|
+
return matched
|
|
971
|
+
}
|
|
972
|
+
return visit(0, 0)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
func matchProjectInputGlobSegment(
|
|
976
|
+
pattern string,
|
|
977
|
+
candidate string,
|
|
978
|
+
caseSensitive bool,
|
|
979
|
+
) bool {
|
|
980
|
+
if !caseSensitive {
|
|
981
|
+
pattern = strings.ToLower(pattern)
|
|
982
|
+
candidate = strings.ToLower(candidate)
|
|
983
|
+
}
|
|
984
|
+
expression := []rune(pattern)
|
|
985
|
+
input := []rune(candidate)
|
|
986
|
+
matches := make([][]bool, len(expression)+1)
|
|
987
|
+
for index := range matches {
|
|
988
|
+
matches[index] = make([]bool, len(input)+1)
|
|
989
|
+
}
|
|
990
|
+
matches[0][0] = true
|
|
991
|
+
for patternIndex, char := range expression {
|
|
992
|
+
if char == '*' {
|
|
993
|
+
matches[patternIndex+1][0] = matches[patternIndex][0]
|
|
994
|
+
}
|
|
995
|
+
for inputIndex := range input {
|
|
996
|
+
switch char {
|
|
997
|
+
case '*':
|
|
998
|
+
matches[patternIndex+1][inputIndex+1] =
|
|
999
|
+
matches[patternIndex][inputIndex+1] ||
|
|
1000
|
+
matches[patternIndex+1][inputIndex]
|
|
1001
|
+
case '?':
|
|
1002
|
+
matches[patternIndex+1][inputIndex+1] =
|
|
1003
|
+
matches[patternIndex][inputIndex]
|
|
1004
|
+
default:
|
|
1005
|
+
matches[patternIndex+1][inputIndex+1] =
|
|
1006
|
+
char == input[inputIndex] &&
|
|
1007
|
+
matches[patternIndex][inputIndex]
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return matches[len(expression)][len(input)]
|
|
1012
|
+
}
|