Skip to content

Commit af91353

Browse files
committed
Bring back some minimal strategies to avoid super broad globs
1 parent a32714e commit af91353

File tree

7 files changed

+119
-31
lines changed

7 files changed

+119
-31
lines changed

internal/project/configfileregistrybuilder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry
208208
globs = append(globs, fileName)
209209
}
210210
if len(externalDirectories) > 0 {
211-
commonParents, ignoredExternalDirs := tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, comparePathsOptions)
211+
commonParents, ignoredExternalDirs := tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, getPathComponentsForWatching, comparePathsOptions)
212212
for _, parent := range commonParents {
213213
globs = append(globs, getRecursiveGlobPattern(parent))
214214
}

internal/project/project.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,12 @@ func NewProject(
157157
project.failedLookupsWatch = NewWatchedFiles(
158158
"failed lookups for "+configFileName,
159159
lsproto.WatchKindCreate,
160-
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
160+
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
161161
)
162162
project.affectingLocationsWatch = NewWatchedFiles(
163163
"affecting locations for "+configFileName,
164164
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
165-
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
165+
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
166166
)
167167
if builder.sessionOptions.TypingsLocation != "" {
168168
project.typingsWatch = NewWatchedFiles(
@@ -331,10 +331,10 @@ func (p *Project) CreateProgram() CreateProgramResult {
331331
}
332332
}
333333

334-
func (p *Project) CloneWatchers(workspaceDir string) (programFilesWatch *WatchedFiles[patternsAndIgnored], failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) {
334+
func (p *Project) CloneWatchers(workspaceDir string, libDir string) (programFilesWatch *WatchedFiles[patternsAndIgnored], failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) {
335335
failedLookups := make(map[tspath.Path]string)
336336
affectingLocations := make(map[tspath.Path]string)
337-
programFiles := getNonRootFileGlobs(workspaceDir, p.Program.GetSourceFiles(), p.CommandLine.FileNamesByPath(), tspath.ComparePathsOptions{
337+
programFiles := getNonRootFileGlobs(workspaceDir, libDir, p.Program.GetSourceFiles(), p.CommandLine.FileNamesByPath(), tspath.ComparePathsOptions{
338338
UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(),
339339
CurrentDirectory: p.currentDirectory,
340340
})

internal/project/projectcollectionbuilder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ func (b *projectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo
793793
if result.UpdateKind == ProgramUpdateKindNewFiles {
794794
filesChanged = true
795795
if b.sessionOptions.WatchEnabled {
796-
programFilesWatch, failedLookupsWatch, affectingLocationsWatch := project.CloneWatchers(b.sessionOptions.CurrentDirectory)
796+
programFilesWatch, failedLookupsWatch, affectingLocationsWatch := project.CloneWatchers(b.sessionOptions.CurrentDirectory, b.sessionOptions.DefaultLibraryPath)
797797
project.programFilesWatch = programFilesWatch
798798
project.failedLookupsWatch = failedLookupsWatch
799799
project.affectingLocationsWatch = affectingLocationsWatch

internal/project/watch.go

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"maps"
66
"slices"
7+
"strings"
78
"sync"
89
"sync/atomic"
910

@@ -128,7 +129,7 @@ func (w *WatchedFiles[T]) Clone(input T) *WatchedFiles[T] {
128129
}
129130
}
130131

131-
func createResolutionLookupGlobMapper(workspaceDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data map[tspath.Path]string) patternsAndIgnored {
132+
func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data map[tspath.Path]string) patternsAndIgnored {
132133
comparePathsOptions := tspath.ComparePathsOptions{
133134
CurrentDirectory: currentDirectory,
134135
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
@@ -137,8 +138,8 @@ func createResolutionLookupGlobMapper(workspaceDirectory string, currentDirector
137138
return func(data map[tspath.Path]string) patternsAndIgnored {
138139
var ignored map[string]struct{}
139140
var seenDirs collections.Set[string]
140-
var includeWorkspace, includeRoot bool
141-
var externalDirectories map[tspath.Path]string
141+
var includeWorkspace, includeRoot, includeLib bool
142+
var nodeModulesDirectories, externalDirectories map[tspath.Path]string
142143

143144
for path, fileName := range data {
144145
// Assuming all of the input paths are filenames, we can avoid
@@ -150,10 +151,16 @@ func createResolutionLookupGlobMapper(workspaceDirectory string, currentDirector
150151

151152
if tspath.ContainsPath(workspaceDirectory, fileName, comparePathsOptions) {
152153
includeWorkspace = true
153-
continue
154154
} else if tspath.ContainsPath(currentDirectory, fileName, comparePathsOptions) {
155155
includeRoot = true
156-
continue
156+
} else if tspath.ContainsPath(libDirectory, fileName, comparePathsOptions) {
157+
includeLib = true
158+
} else if idx := strings.Index(fileName, "/node_modules/"); idx != -1 {
159+
if nodeModulesDirectories == nil {
160+
nodeModulesDirectories = make(map[tspath.Path]string)
161+
}
162+
dir := fileName[:idx+len("/node_modules")]
163+
nodeModulesDirectories[tspath.ToPath(dir, currentDirectory, useCaseSensitiveFileNames)] = dir
157164
} else {
158165
if externalDirectories == nil {
159166
externalDirectories = make(map[tspath.Path]string)
@@ -169,8 +176,19 @@ func createResolutionLookupGlobMapper(workspaceDirectory string, currentDirector
169176
if includeRoot {
170177
globs = append(globs, getRecursiveGlobPattern(currentDirectory))
171178
}
179+
if includeLib {
180+
globs = append(globs, getRecursiveGlobPattern(libDirectory))
181+
}
182+
for _, dir := range nodeModulesDirectories {
183+
globs = append(globs, getRecursiveGlobPattern(dir))
184+
}
172185
if len(externalDirectories) > 0 {
173-
externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents(slices.Collect(maps.Values(externalDirectories)), minWatchLocationDepth, comparePathsOptions)
186+
externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents(
187+
slices.Collect(maps.Values(externalDirectories)),
188+
minWatchLocationDepth,
189+
getPathComponentsForWatching,
190+
comparePathsOptions,
191+
)
174192
slices.Sort(externalDirectoryParents)
175193
ignored = ignoredExternalDirs
176194
for _, dir := range externalDirectoryParents {
@@ -209,7 +227,12 @@ func getTypingsLocationsGlobs(
209227
includeWorkspace = true
210228
}
211229
}
212-
externalDirectoryParents, ignored := tspath.GetCommonParents(slices.Collect(maps.Values(externalDirectories)), minWatchLocationDepth, comparePathsOptions)
230+
externalDirectoryParents, ignored := tspath.GetCommonParents(
231+
slices.Collect(maps.Values(externalDirectories)),
232+
minWatchLocationDepth,
233+
getPathComponentsForWatching,
234+
comparePathsOptions,
235+
)
213236
slices.Sort(externalDirectoryParents)
214237
if includeWorkspace {
215238
globs[tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(workspaceDirectory)
@@ -226,6 +249,40 @@ func getTypingsLocationsGlobs(
226249
}
227250
}
228251

252+
func getPathComponentsForWatching(path string, currentDirectory string) []string {
253+
components := tspath.GetPathComponents(path, currentDirectory)
254+
rootLength := perceivedOsRootLengthForWatching(components)
255+
if rootLength <= 1 {
256+
return components
257+
}
258+
newRoot := tspath.CombinePaths(components[0], components[1:rootLength]...)
259+
return append([]string{newRoot}, components[rootLength:]...)
260+
}
261+
262+
func perceivedOsRootLengthForWatching(pathComponents []string) int {
263+
length := len(pathComponents)
264+
if length <= 1 {
265+
return length
266+
}
267+
if strings.HasPrefix(pathComponents[0], "//") {
268+
// Group UNC roots (//server/share) into a single component
269+
return 2
270+
}
271+
if len(pathComponents[0]) == 3 && tspath.IsVolumeCharacter(pathComponents[0][0]) && pathComponents[0][1] == ':' && pathComponents[0][2] == '/' {
272+
// Windows-style volume
273+
if strings.EqualFold(pathComponents[1], "users") {
274+
// Group C:/Users/username into a single component
275+
return min(3, length)
276+
}
277+
return 1
278+
}
279+
if pathComponents[1] == "home" {
280+
// Group /home/username into a single component
281+
return min(3, length)
282+
}
283+
return 1
284+
}
285+
229286
func ptrTo[T any](v T) *T {
230287
return &v
231288
}
@@ -258,26 +315,36 @@ func extractLookups[T resolutionWithLookupLocations](
258315
}
259316
}
260317

261-
func getNonRootFileGlobs(workspaceDir string, sourceFiles []*ast.SourceFile, rootFiles map[tspath.Path]string, comparePathsOptions tspath.ComparePathsOptions) patternsAndIgnored {
318+
func getNonRootFileGlobs(workspaceDir string, libDirectory string, sourceFiles []*ast.SourceFile, rootFiles map[tspath.Path]string, comparePathsOptions tspath.ComparePathsOptions) patternsAndIgnored {
262319
var globs []string
263-
var includeWorkspace bool
320+
var includeWorkspace, includeLib bool
264321
var ignored map[string]struct{}
265322
externalDirectories := make([]string, 0, max(0, len(sourceFiles)-len(rootFiles)))
266323
for _, sourceFile := range sourceFiles {
267324
if _, ok := rootFiles[sourceFile.Path()]; !ok {
268325
if tspath.ContainsPath(workspaceDir, sourceFile.FileName(), comparePathsOptions) {
269326
includeWorkspace = true
270-
continue
327+
} else if tspath.ContainsPath(libDirectory, sourceFile.FileName(), comparePathsOptions) {
328+
includeLib = true
329+
} else {
330+
externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(sourceFile.FileName()))
271331
}
272-
externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(sourceFile.FileName()))
273332
}
274333
}
275334

276335
if includeWorkspace {
277336
globs = append(globs, getRecursiveGlobPattern(workspaceDir))
278337
}
338+
if includeLib {
339+
globs = append(globs, getRecursiveGlobPattern(libDirectory))
340+
}
279341
if len(externalDirectories) > 0 {
280-
commonParents, ignoredDirs := tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, comparePathsOptions)
342+
commonParents, ignoredDirs := tspath.GetCommonParents(
343+
externalDirectories,
344+
minWatchLocationDepth,
345+
getPathComponentsForWatching,
346+
comparePathsOptions,
347+
)
281348
globs = append(globs, core.Map(commonParents, func(dir string) string {
282349
return getRecursiveGlobPattern(dir)
283350
})...)

internal/project/watch_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package project
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/assert"
7+
)
8+
9+
func TestGetPathComponentsForWatching(t *testing.T) {
10+
t.Parallel()
11+
12+
assert.DeepEqual(t, getPathComponentsForWatching("/project", ""), []string{"/", "project"})
13+
assert.DeepEqual(t, getPathComponentsForWatching("C:\\project", ""), []string{"C:/", "project"})
14+
assert.DeepEqual(t, getPathComponentsForWatching("//server/share/project/tsconfig.json", ""), []string{"//server/share", "project", "tsconfig.json"})
15+
assert.DeepEqual(t, getPathComponentsForWatching(`\\server\share\project\tsconfig.json`, ""), []string{"//server/share", "project", "tsconfig.json"})
16+
assert.DeepEqual(t, getPathComponentsForWatching("C:\\Users", ""), []string{"C:/Users"})
17+
assert.DeepEqual(t, getPathComponentsForWatching("C:\\Users\\andrew\\project", ""), []string{"C:/Users/andrew", "project"})
18+
assert.DeepEqual(t, getPathComponentsForWatching("/home", ""), []string{"/home"})
19+
assert.DeepEqual(t, getPathComponentsForWatching("/home/andrew/project", ""), []string{"/home/andrew", "project"})
20+
}

internal/tspath/path.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@ func SplitVolumePath(path string) (volume string, rest string, ok bool) {
10361036
func GetCommonParents(
10371037
paths []string,
10381038
minComponents int,
1039+
getPathComponents func(path string, currentDirectory string) []string,
10391040
options ComparePathsOptions,
10401041
) (parents []string, ignored map[string]struct{}) {
10411042
if minComponents < 1 {
@@ -1045,7 +1046,7 @@ func GetCommonParents(
10451046
return nil, nil
10461047
}
10471048
if len(paths) == 1 {
1048-
if len(reducePathComponents(GetPathComponents(paths[0], options.CurrentDirectory))) < minComponents {
1049+
if len(reducePathComponents(getPathComponents(paths[0], options.CurrentDirectory))) < minComponents {
10491050
return nil, map[string]struct{}{paths[0]: {}}
10501051
}
10511052
return paths, nil
@@ -1054,7 +1055,7 @@ func GetCommonParents(
10541055
ignored = make(map[string]struct{})
10551056
pathComponents := make([][]string, 0, len(paths))
10561057
for _, path := range paths {
1057-
components := reducePathComponents(GetPathComponents(path, options.CurrentDirectory))
1058+
components := reducePathComponents(getPathComponents(path, options.CurrentDirectory))
10581059
if len(components) < minComponents {
10591060
ignored[path] = struct{}{}
10601061
} else {

internal/tspath/path_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -713,15 +713,15 @@ func TestGetCommonParents(t *testing.T) {
713713
t.Run("empty input", func(t *testing.T) {
714714
t.Parallel()
715715
var paths []string
716-
got, ignored := GetCommonParents(paths, 1, opts)
716+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
717717
assert.Equal(t, len(ignored), 0)
718718
assert.DeepEqual(t, got, ([]string)(nil))
719719
})
720720

721721
t.Run("single path returns itself", func(t *testing.T) {
722722
t.Parallel()
723723
paths := []string{"/a/b/c/d"}
724-
got, ignored := GetCommonParents(paths, 1, opts)
724+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
725725
assert.Equal(t, len(ignored), 0)
726726
expected := []string{paths[0]}
727727
assert.DeepEqual(t, got, expected)
@@ -730,7 +730,7 @@ func TestGetCommonParents(t *testing.T) {
730730
t.Run("paths shorter than minComponents are ignored", func(t *testing.T) {
731731
t.Parallel()
732732
paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g", "/x/y"}
733-
got, ignored := GetCommonParents(paths, 4, opts)
733+
got, ignored := GetCommonParents(paths, 4, GetPathComponents, opts)
734734
assert.DeepEqual(t, ignored, map[string]struct{}{"/x/y": {}})
735735
expected := []string{"/a/b/c", "/a/b/f/g"}
736736
assert.DeepEqual(t, got, expected)
@@ -739,7 +739,7 @@ func TestGetCommonParents(t *testing.T) {
739739
t.Run("three paths share /a/b", func(t *testing.T) {
740740
t.Parallel()
741741
paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g"}
742-
got, ignored := GetCommonParents(paths, 1, opts)
742+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
743743
assert.Equal(t, len(ignored), 0)
744744
expected := []string{"/a/b"}
745745
assert.DeepEqual(t, got, expected)
@@ -748,7 +748,7 @@ func TestGetCommonParents(t *testing.T) {
748748
t.Run("mixed with short path collapses to root when minComponents=1", func(t *testing.T) {
749749
t.Parallel()
750750
paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g", "/x/y/z"}
751-
got, ignored := GetCommonParents(paths, 1, opts)
751+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
752752
assert.Equal(t, len(ignored), 0)
753753
expected := []string{"/"}
754754
assert.DeepEqual(t, got, expected)
@@ -757,7 +757,7 @@ func TestGetCommonParents(t *testing.T) {
757757
t.Run("mixed with short path preserves both when minComponents=3", func(t *testing.T) {
758758
t.Parallel()
759759
paths := []string{"/a/b/c/d", "/a/b/c/e", "/a/b/f/g", "/x/y/z"}
760-
got, ignored := GetCommonParents(paths, 3, opts)
760+
got, ignored := GetCommonParents(paths, 3, GetPathComponents, opts)
761761
assert.Equal(t, len(ignored), 0)
762762
expected := []string{"/a/b", "/x/y/z"}
763763
assert.DeepEqual(t, got, expected)
@@ -766,7 +766,7 @@ func TestGetCommonParents(t *testing.T) {
766766
t.Run("different volumes are returned individually", func(t *testing.T) {
767767
t.Parallel()
768768
paths := []string{"c:/a/b/c/d", "d:/a/b/c/d"}
769-
got, ignored := GetCommonParents(paths, 1, opts)
769+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
770770
assert.Equal(t, len(ignored), 0)
771771
expected := []string{paths[0], paths[1]}
772772
assert.DeepEqual(t, got, expected)
@@ -775,7 +775,7 @@ func TestGetCommonParents(t *testing.T) {
775775
t.Run("duplicate paths deduplicate result", func(t *testing.T) {
776776
t.Parallel()
777777
paths := []string{"/a/b/c/d", "/a/b/c/d"}
778-
got, ignored := GetCommonParents(paths, 1, opts)
778+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
779779
assert.Equal(t, len(ignored), 0)
780780
expected := []string{paths[0]}
781781
assert.DeepEqual(t, got, expected)
@@ -784,7 +784,7 @@ func TestGetCommonParents(t *testing.T) {
784784
t.Run("paths with few components are returned as-is when minComponents met", func(t *testing.T) {
785785
t.Parallel()
786786
paths := []string{"/a/b/c/d", "/x/y"}
787-
got, ignored := GetCommonParents(paths, 2, opts)
787+
got, ignored := GetCommonParents(paths, 2, GetPathComponents, opts)
788788
assert.Equal(t, len(ignored), 0)
789789
expected := []string{"/a/b/c/d", "/x/y"}
790790
assert.DeepEqual(t, got, expected)
@@ -793,7 +793,7 @@ func TestGetCommonParents(t *testing.T) {
793793
t.Run("minComponents=2", func(t *testing.T) {
794794
t.Parallel()
795795
paths := []string{"/a/b/c/d", "/a/z/c/e", "/a/aaa/f/g", "/x/y/z"}
796-
got, ignored := GetCommonParents(paths, 2, opts)
796+
got, ignored := GetCommonParents(paths, 2, GetPathComponents, opts)
797797
assert.Equal(t, len(ignored), 0)
798798
expected := []string{"/a", "/x/y/z"}
799799
assert.DeepEqual(t, got, expected)
@@ -802,7 +802,7 @@ func TestGetCommonParents(t *testing.T) {
802802
t.Run("trailing separators are handled", func(t *testing.T) {
803803
t.Parallel()
804804
paths := []string{"/a/b/", "/a/b/c"}
805-
got, ignored := GetCommonParents(paths, 1, opts)
805+
got, ignored := GetCommonParents(paths, 1, GetPathComponents, opts)
806806
assert.Equal(t, len(ignored), 0)
807807
expected := []string{"/a/b"}
808808
assert.DeepEqual(t, got, expected)

0 commit comments

Comments
 (0)