@@ -2,9 +2,13 @@ package project
2
2
3
3
import (
4
4
"context"
5
+ "fmt"
5
6
"slices"
7
+ "strings"
8
+ "time"
6
9
7
10
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
11
+ "github.com/microsoft/typescript-go/internal/tspath"
8
12
)
9
13
10
14
const (
@@ -60,3 +64,263 @@ func (w *watchedFiles[T]) update(ctx context.Context, newData T) (updated bool,
60
64
w .watcherID = watcherID
61
65
return true , nil
62
66
}
67
+
68
+ func createGlobMapper (host ProjectHost ) func (data map [tspath.Path ]string ) []string {
69
+ rootDir := host .GetCurrentDirectory ()
70
+ rootPath := tspath .ToPath (rootDir , "" , host .FS ().UseCaseSensitiveFileNames ())
71
+ rootPathComponents := tspath .GetPathComponents (string (rootPath ), "" )
72
+ isRootWatchable := canWatchDirectoryOrFile (rootPathComponents )
73
+
74
+ return func (data map [tspath.Path ]string ) []string {
75
+ start := time .Now ()
76
+
77
+ // dir -> recursive
78
+ globSet := make (map [string ]bool )
79
+
80
+ for path , fileName := range data {
81
+ w := getDirectoryToWatchFailedLookupLocation (
82
+ fileName ,
83
+ path ,
84
+ rootDir ,
85
+ rootPath ,
86
+ rootPathComponents ,
87
+ isRootWatchable ,
88
+ rootDir ,
89
+ true ,
90
+ )
91
+ if w == nil {
92
+ continue
93
+ }
94
+ globSet [w .dir ] = globSet [w .dir ] || ! w .nonRecursive
95
+ }
96
+
97
+ globs := make ([]string , 0 , len (globSet ))
98
+ for dir , recursive := range globSet {
99
+ if recursive {
100
+ globs = append (globs , dir + "/" + recursiveFileGlobPattern )
101
+ } else {
102
+ globs = append (globs , dir + "/" + fileGlobPattern )
103
+ }
104
+ }
105
+ slices .Sort (globs )
106
+
107
+ timeTaken := time .Since (start )
108
+ host .Log (fmt .Sprintf ("createGlobMapper took %s to create %d globs for %d failed lookups" , timeTaken , len (globs ), len (data )))
109
+ return globs
110
+ }
111
+ }
112
+
113
+ type directoryOfFailedLookupWatch struct {
114
+ dir string
115
+ dirPath tspath.Path
116
+ nonRecursive bool
117
+ packageDir * string
118
+ packageDirPath * tspath.Path
119
+ }
120
+
121
+ func getDirectoryToWatchFailedLookupLocation (
122
+ failedLookupLocation string ,
123
+ failedLookupLocationPath tspath.Path ,
124
+ rootDir string ,
125
+ rootPath tspath.Path ,
126
+ rootPathComponents []string ,
127
+ isRootWatchable bool ,
128
+ currentDirectory string ,
129
+ preferNonRecursiveWatch bool ,
130
+ ) * directoryOfFailedLookupWatch {
131
+ failedLookupPathComponents := tspath .GetPathComponents (string (failedLookupLocationPath ), "" )
132
+ // Ensure failed look up is normalized path
133
+ // !!! needed?
134
+ if tspath .IsRootedDiskPath (failedLookupLocation ) {
135
+ failedLookupLocation = tspath .NormalizePath (failedLookupLocation )
136
+ } else {
137
+ failedLookupLocation = tspath .GetNormalizedAbsolutePath (failedLookupLocation , currentDirectory )
138
+ }
139
+ failedLookupComponents := tspath .GetPathComponents (failedLookupLocation , "" )
140
+ perceivedOsRootLength := perceivedOsRootLengthForWatching (failedLookupPathComponents , len (failedLookupPathComponents ))
141
+ if len (failedLookupPathComponents ) <= perceivedOsRootLength + 1 {
142
+ return nil
143
+ }
144
+ // If directory path contains node module, get the most parent node_modules directory for watching
145
+ nodeModulesIndex := slices .Index (failedLookupPathComponents , "node_modules" )
146
+ if nodeModulesIndex != - 1 && nodeModulesIndex + 1 <= perceivedOsRootLength + 1 {
147
+ return nil
148
+ }
149
+ lastNodeModulesIndex := lastIndex (failedLookupPathComponents , "node_modules" )
150
+ if isRootWatchable && isInDirectoryPath (rootPathComponents , failedLookupPathComponents ) {
151
+ if len (failedLookupPathComponents ) > len (rootPathComponents )+ 1 {
152
+ // Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution
153
+ return getDirectoryOfFailedLookupWatch (
154
+ failedLookupComponents ,
155
+ failedLookupPathComponents ,
156
+ max (len (rootPathComponents )+ 1 , perceivedOsRootLength + 1 ),
157
+ lastNodeModulesIndex ,
158
+ false ,
159
+ )
160
+ } else {
161
+ // Always watch root directory non recursively
162
+ return & directoryOfFailedLookupWatch {
163
+ dir : rootDir ,
164
+ dirPath : rootPath ,
165
+ nonRecursive : true ,
166
+ }
167
+ }
168
+ }
169
+
170
+ return getDirectoryToWatchFromFailedLookupLocationDirectory (
171
+ failedLookupComponents ,
172
+ failedLookupPathComponents ,
173
+ len (failedLookupPathComponents )- 1 ,
174
+ perceivedOsRootLength ,
175
+ nodeModulesIndex ,
176
+ rootPathComponents ,
177
+ lastNodeModulesIndex ,
178
+ preferNonRecursiveWatch ,
179
+ )
180
+ }
181
+
182
+ func getDirectoryToWatchFromFailedLookupLocationDirectory (
183
+ dirComponents []string ,
184
+ dirPathComponents []string ,
185
+ dirPathComponentsLength int ,
186
+ perceivedOsRootLength int ,
187
+ nodeModulesIndex int ,
188
+ rootPathComponents []string ,
189
+ lastNodeModulesIndex int ,
190
+ preferNonRecursiveWatch bool ,
191
+ ) * directoryOfFailedLookupWatch {
192
+ // If directory path contains node module, get the most parent node_modules directory for watching
193
+ if nodeModulesIndex != - 1 {
194
+ // If the directory is node_modules use it to watch, always watch it recursively
195
+ return getDirectoryOfFailedLookupWatch (
196
+ dirComponents ,
197
+ dirPathComponents ,
198
+ nodeModulesIndex + 1 ,
199
+ lastNodeModulesIndex ,
200
+ false ,
201
+ )
202
+ }
203
+
204
+ // Use some ancestor of the root directory
205
+ nonRecursive := true
206
+ length := dirPathComponentsLength
207
+ if ! preferNonRecursiveWatch {
208
+ for i := range dirPathComponentsLength {
209
+ if dirPathComponents [i ] != rootPathComponents [i ] {
210
+ nonRecursive = false
211
+ length = max (i + 1 , perceivedOsRootLength + 1 )
212
+ break
213
+ }
214
+ }
215
+ }
216
+ return getDirectoryOfFailedLookupWatch (
217
+ dirComponents ,
218
+ dirPathComponents ,
219
+ length ,
220
+ lastNodeModulesIndex ,
221
+ nonRecursive ,
222
+ )
223
+ }
224
+
225
+ func getDirectoryOfFailedLookupWatch (
226
+ dirComponents []string ,
227
+ dirPathComponents []string ,
228
+ length int ,
229
+ lastNodeModulesIndex int ,
230
+ nonRecursive bool ,
231
+ ) * directoryOfFailedLookupWatch {
232
+ packageDirLength := - 1
233
+ if lastNodeModulesIndex != - 1 && lastNodeModulesIndex + 1 >= length && lastNodeModulesIndex + 2 < len (dirPathComponents ) {
234
+ if ! strings .HasPrefix (dirPathComponents [lastNodeModulesIndex + 1 ], "@" ) {
235
+ packageDirLength = lastNodeModulesIndex + 2
236
+ } else if lastNodeModulesIndex + 3 < len (dirPathComponents ) {
237
+ packageDirLength = lastNodeModulesIndex + 3
238
+ }
239
+ }
240
+ var packageDir * string
241
+ var packageDirPath * tspath.Path
242
+ if packageDirLength != - 1 {
243
+ packageDir = ptrTo (tspath .GetPathFromPathComponents (dirPathComponents [:packageDirLength ]))
244
+ packageDirPath = ptrTo (tspath .Path (tspath .GetPathFromPathComponents (dirComponents [:packageDirLength ])))
245
+ }
246
+
247
+ return & directoryOfFailedLookupWatch {
248
+ dir : tspath .GetPathFromPathComponents (dirComponents [:length ]),
249
+ dirPath : tspath .Path (tspath .GetPathFromPathComponents (dirPathComponents [:length ])),
250
+ nonRecursive : nonRecursive ,
251
+ packageDir : packageDir ,
252
+ packageDirPath : packageDirPath ,
253
+ }
254
+ }
255
+
256
+ func perceivedOsRootLengthForWatching (pathComponents []string , length int ) int {
257
+ // Ignore "/", "c:/"
258
+ if length <= 1 {
259
+ return 1
260
+ }
261
+ indexAfterOsRoot := 1
262
+ firstComponent := pathComponents [0 ]
263
+ isDosStyle := len (firstComponent ) >= 2 && tspath .IsVolumeCharacter (firstComponent [0 ]) && firstComponent [1 ] == ':'
264
+ if firstComponent != "/" && ! isDosStyle && isDosStyleNextPart (pathComponents [1 ]) {
265
+ // ignore "//vda1cs4850/c$/folderAtRoot"
266
+ if length == 2 {
267
+ return 2
268
+ }
269
+ indexAfterOsRoot = 2
270
+ isDosStyle = true
271
+ }
272
+
273
+ afterOsRoot := pathComponents [indexAfterOsRoot ]
274
+ if isDosStyle && ! strings .EqualFold (afterOsRoot , "users" ) {
275
+ // Paths like c:/notUsers
276
+ return indexAfterOsRoot
277
+ }
278
+
279
+ if strings .EqualFold (afterOsRoot , "workspaces" ) {
280
+ // Paths like: /workspaces as codespaces hoist the repos in /workspaces so we have to exempt these from "2" level from root rule
281
+ return indexAfterOsRoot + 1
282
+ }
283
+
284
+ // Paths like: c:/users/username or /home/username
285
+ return indexAfterOsRoot + 2
286
+ }
287
+
288
+ func canWatchDirectoryOrFile (pathComponents []string ) bool {
289
+ length := len (pathComponents )
290
+ // Ignore "/", "c:/"
291
+ // ignore "/user", "c:/users" or "c:/folderAtRoot"
292
+ if length < 2 {
293
+ return false
294
+ }
295
+ perceivedOsRootLength := perceivedOsRootLengthForWatching (pathComponents , length )
296
+ return length > perceivedOsRootLength + 1
297
+ }
298
+
299
+ func isDosStyleNextPart (part string ) bool {
300
+ return len (part ) == 2 && tspath .IsVolumeCharacter (part [0 ]) && part [1 ] == '$'
301
+ }
302
+
303
+ func lastIndex [T comparable ](s []T , v T ) int {
304
+ for i := len (s ) - 1 ; i >= 0 ; i -- {
305
+ if s [i ] == v {
306
+ return i
307
+ }
308
+ }
309
+ return - 1
310
+ }
311
+
312
+ func isInDirectoryPath (dirComponents []string , fileOrDirComponents []string ) bool {
313
+ if len (fileOrDirComponents ) < len (dirComponents ) {
314
+ return false
315
+ }
316
+ for i := range dirComponents {
317
+ if dirComponents [i ] != fileOrDirComponents [i ] {
318
+ return false
319
+ }
320
+ }
321
+ return true
322
+ }
323
+
324
+ func ptrTo [T any ](v T ) * T {
325
+ return & v
326
+ }
0 commit comments