Skip to content

Commit 99939d9

Browse files
committed
Minimize number of watch globs
1 parent 869519d commit 99939d9

File tree

7 files changed

+313
-83
lines changed

7 files changed

+313
-83
lines changed

internal/project/project.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ type Project struct {
7474

7575
failedLookupsWatch *WatchedFiles[map[tspath.Path]string]
7676
affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]
77-
typingsFilesWatch *WatchedFiles[map[tspath.Path]string]
78-
typingsDirectoryWatch *WatchedFiles[map[tspath.Path]string]
77+
typingsWatch *WatchedFiles[map[tspath.Path]string]
7978

8079
checkerPool *checkerPool
8180

@@ -152,24 +151,19 @@ func NewProject(
152151
project.failedLookupsWatch = NewWatchedFiles(
153152
"failed lookups for "+configFileName,
154153
lsproto.WatchKindCreate,
155-
createResolutionLookupGlobMapper(project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
154+
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
156155
)
157156
project.affectingLocationsWatch = NewWatchedFiles(
158157
"affecting locations for "+configFileName,
159158
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
160-
createResolutionLookupGlobMapper(project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
159+
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
161160
)
162161
if builder.sessionOptions.TypingsLocation != "" {
163-
project.typingsFilesWatch = NewWatchedFiles(
162+
project.typingsWatch = NewWatchedFiles(
164163
"typings installer files",
165164
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
166165
globMapperForTypingsInstaller,
167166
)
168-
project.typingsDirectoryWatch = NewWatchedFiles(
169-
"typings installer directories",
170-
lsproto.WatchKindCreate|lsproto.WatchKindDelete,
171-
globMapperForTypingsInstaller,
172-
)
173167
}
174168
}
175169
return project
@@ -227,8 +221,7 @@ func (p *Project) Clone() *Project {
227221

228222
failedLookupsWatch: p.failedLookupsWatch,
229223
affectingLocationsWatch: p.affectingLocationsWatch,
230-
typingsFilesWatch: p.typingsFilesWatch,
231-
typingsDirectoryWatch: p.typingsDirectoryWatch,
224+
typingsWatch: p.typingsWatch,
232225

233226
checkerPool: p.checkerPool,
234227

internal/project/projectcollectionbuilder.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,14 +343,14 @@ func (b *projectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path]
343343
// the set of typings files is actually different.
344344
p.installedTypingsInfo = ataChange.TypingsInfo
345345
p.typingsFiles = ataChange.TypingsFiles
346-
fileWatchGlobs, directoryWatchGlobs := getTypingsLocationsGlobs(
346+
typingsWatchGlobs := getTypingsLocationsGlobs(
347347
ataChange.TypingsFilesToWatch,
348348
b.sessionOptions.TypingsLocation,
349+
b.sessionOptions.CurrentDirectory,
349350
p.currentDirectory,
350351
b.fs.fs.UseCaseSensitiveFileNames(),
351352
)
352-
p.typingsFilesWatch = p.typingsFilesWatch.Clone(fileWatchGlobs)
353-
p.typingsDirectoryWatch = p.typingsDirectoryWatch.Clone(directoryWatchGlobs)
353+
p.typingsWatch = p.typingsWatch.Clone(typingsWatchGlobs)
354354
p.dirty = true
355355
p.dirtyFilePath = ""
356356
},

internal/project/session.go

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ type Session struct {
108108
// after file watch changes and ATA updates.
109109
diagnosticsRefreshCancel context.CancelFunc
110110
diagnosticsRefreshMu sync.Mutex
111+
112+
// watches tracks the current watch globs and how many individual WatchedFiles
113+
// are using each glob.
114+
watches map[fileSystemWatcherKey]int
115+
watchesMu sync.Mutex
111116
}
112117

113118
func NewSession(init *SessionInit) *Session {
@@ -149,6 +154,7 @@ func NewSession(init *SessionInit) *Session {
149154
toPath,
150155
),
151156
pendingATAChanges: make(map[tspath.Path]*ATAStateChange),
157+
watches: make(map[fileSystemWatcherKey]int),
152158
}
153159

154160
if init.Options.TypingsLocation != "" && init.NpmExecutor != nil {
@@ -410,33 +416,57 @@ func (s *Session) WaitForBackgroundTasks() {
410416
s.backgroundQueue.Wait()
411417
}
412418

413-
func updateWatch[T any](ctx context.Context, client Client, logger logging.Logger, oldWatcher, newWatcher *WatchedFiles[T]) []error {
419+
func updateWatch[T any](ctx context.Context, session *Session, logger logging.Logger, oldWatcher, newWatcher *WatchedFiles[T]) []error {
414420
var errors []error
421+
session.watchesMu.Lock()
422+
defer session.watchesMu.Unlock()
415423
if newWatcher != nil {
416424
if id, watchers := newWatcher.Watchers(); len(watchers) > 0 {
417-
if err := client.WatchFiles(ctx, id, watchers); err != nil {
418-
errors = append(errors, err)
419-
}
420-
if logger != nil {
421-
if oldWatcher == nil {
422-
logger.Log(fmt.Sprintf("Added new watch: %s", id))
423-
} else {
424-
logger.Log(fmt.Sprintf("Updated watch: %s", id))
425+
var newWatchers []*lsproto.FileSystemWatcher
426+
for _, watcher := range watchers {
427+
key := toFileSystemWatcherKey(watcher)
428+
count := session.watches[key]
429+
session.watches[key] = count + 1
430+
if count == 0 {
431+
newWatchers = append(newWatchers, watcher)
425432
}
426-
for _, watcher := range watchers {
427-
logger.Log("\t" + *watcher.GlobPattern.Pattern)
433+
}
434+
if len(newWatchers) > 0 {
435+
if err := session.client.WatchFiles(ctx, id, newWatchers); err != nil {
436+
errors = append(errors, err)
437+
} else if logger != nil {
438+
if oldWatcher == nil {
439+
logger.Log(fmt.Sprintf("Added new watch: %s", id))
440+
} else {
441+
logger.Log(fmt.Sprintf("Updated watch: %s", id))
442+
}
443+
for _, watcher := range watchers {
444+
logger.Log("\t" + *watcher.GlobPattern.Pattern)
445+
}
446+
logger.Log("")
428447
}
429-
logger.Log("")
430448
}
431449
}
432450
}
433451
if oldWatcher != nil {
434452
if id, watchers := oldWatcher.Watchers(); len(watchers) > 0 {
435-
if err := client.UnwatchFiles(ctx, id); err != nil {
436-
errors = append(errors, err)
453+
var removedWatchers []WatcherID
454+
for _, watcher := range watchers {
455+
key := toFileSystemWatcherKey(watcher)
456+
count := session.watches[key]
457+
if count <= 1 {
458+
delete(session.watches, key)
459+
removedWatchers = append(removedWatchers, id)
460+
} else {
461+
session.watches[key] = count - 1
462+
}
437463
}
438-
if logger != nil && newWatcher == nil {
439-
logger.Log(fmt.Sprintf("Removed watch: %s", id))
464+
for _, id := range removedWatchers {
465+
if err := session.client.UnwatchFiles(ctx, id); err != nil {
466+
errors = append(errors, err)
467+
} else if logger != nil && newWatcher == nil {
468+
logger.Log(fmt.Sprintf("Removed watch: %s", id))
469+
}
440470
}
441471
}
442472
}
@@ -453,43 +483,38 @@ func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) er
453483
return a.rootFilesWatch.ID() == b.rootFilesWatch.ID()
454484
},
455485
func(_ tspath.Path, addedEntry *configFileEntry) {
456-
errors = append(errors, updateWatch(ctx, s.client, s.logger, nil, addedEntry.rootFilesWatch)...)
486+
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedEntry.rootFilesWatch)...)
457487
},
458488
func(_ tspath.Path, removedEntry *configFileEntry) {
459-
errors = append(errors, updateWatch(ctx, s.client, s.logger, removedEntry.rootFilesWatch, nil)...)
489+
errors = append(errors, updateWatch(ctx, s, s.logger, removedEntry.rootFilesWatch, nil)...)
460490
},
461491
func(_ tspath.Path, oldEntry, newEntry *configFileEntry) {
462-
errors = append(errors, updateWatch(ctx, s.client, s.logger, oldEntry.rootFilesWatch, newEntry.rootFilesWatch)...)
492+
errors = append(errors, updateWatch(ctx, s, s.logger, oldEntry.rootFilesWatch, newEntry.rootFilesWatch)...)
463493
},
464494
)
465495

466496
collections.DiffOrderedMaps(
467497
oldSnapshot.ProjectCollection.ProjectsByPath(),
468498
newSnapshot.ProjectCollection.ProjectsByPath(),
469499
func(_ tspath.Path, addedProject *Project) {
470-
errors = append(errors, updateWatch(ctx, s.client, s.logger, nil, addedProject.affectingLocationsWatch)...)
471-
errors = append(errors, updateWatch(ctx, s.client, s.logger, nil, addedProject.failedLookupsWatch)...)
472-
errors = append(errors, updateWatch(ctx, s.client, s.logger, nil, addedProject.typingsFilesWatch)...)
473-
errors = append(errors, updateWatch(ctx, s.client, s.logger, nil, addedProject.typingsDirectoryWatch)...)
500+
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.affectingLocationsWatch)...)
501+
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.failedLookupsWatch)...)
502+
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.typingsWatch)...)
474503
},
475504
func(_ tspath.Path, removedProject *Project) {
476-
errors = append(errors, updateWatch(ctx, s.client, s.logger, removedProject.affectingLocationsWatch, nil)...)
477-
errors = append(errors, updateWatch(ctx, s.client, s.logger, removedProject.failedLookupsWatch, nil)...)
478-
errors = append(errors, updateWatch(ctx, s.client, s.logger, removedProject.typingsFilesWatch, nil)...)
479-
errors = append(errors, updateWatch(ctx, s.client, s.logger, removedProject.typingsDirectoryWatch, nil)...)
505+
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.affectingLocationsWatch, nil)...)
506+
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.failedLookupsWatch, nil)...)
507+
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.typingsWatch, nil)...)
480508
},
481509
func(_ tspath.Path, oldProject, newProject *Project) {
482510
if oldProject.affectingLocationsWatch.ID() != newProject.affectingLocationsWatch.ID() {
483-
errors = append(errors, updateWatch(ctx, s.client, s.logger, oldProject.affectingLocationsWatch, newProject.affectingLocationsWatch)...)
511+
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.affectingLocationsWatch, newProject.affectingLocationsWatch)...)
484512
}
485513
if oldProject.failedLookupsWatch.ID() != newProject.failedLookupsWatch.ID() {
486-
errors = append(errors, updateWatch(ctx, s.client, s.logger, oldProject.failedLookupsWatch, newProject.failedLookupsWatch)...)
487-
}
488-
if oldProject.typingsFilesWatch.ID() != newProject.typingsFilesWatch.ID() {
489-
errors = append(errors, updateWatch(ctx, s.client, s.logger, oldProject.typingsFilesWatch, newProject.typingsFilesWatch)...)
514+
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.failedLookupsWatch, newProject.failedLookupsWatch)...)
490515
}
491-
if oldProject.typingsDirectoryWatch.ID() != newProject.typingsDirectoryWatch.ID() {
492-
errors = append(errors, updateWatch(ctx, s.client, s.logger, oldProject.typingsDirectoryWatch, newProject.typingsDirectoryWatch)...)
516+
if oldProject.typingsWatch.ID() != newProject.typingsWatch.ID() {
517+
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.typingsWatch, newProject.typingsWatch)...)
493518
}
494519
},
495520
)

internal/project/util.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
package project
22

3-
import "strings"
3+
import (
4+
"strings"
5+
6+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
7+
)
48

59
func isDynamicFileName(fileName string) bool {
610
return strings.HasPrefix(fileName, "^")
711
}
12+
13+
type fileSystemWatcherKey struct {
14+
pattern string
15+
kind lsproto.WatchKind
16+
}
17+
18+
func toFileSystemWatcherKey(w *lsproto.FileSystemWatcher) fileSystemWatcherKey {
19+
if w.GlobPattern.RelativePattern != nil {
20+
panic("relative globs not implemented")
21+
}
22+
kind := w.Kind
23+
if kind == nil {
24+
kind = ptrTo(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete)
25+
}
26+
return fileSystemWatcherKey{pattern: *w.GlobPattern.Pattern, kind: *kind}
27+
}

internal/project/watch.go

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
const (
2020
fileGlobPattern = "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"
2121
recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"
22+
minWatchLocationDepth = 2
2223
)
2324

2425
type WatcherID string
@@ -112,15 +113,21 @@ func globMapperForTypingsInstaller(data map[tspath.Path]string) []string {
112113
return slices.AppendSeq(make([]string, 0, len(data)), maps.Values(data))
113114
}
114115

115-
func createResolutionLookupGlobMapper(currentDirectory string, useCaseSensitiveFileNames bool) func(data map[tspath.Path]string) []string {
116+
func createResolutionLookupGlobMapper(workspaceDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data map[tspath.Path]string) []string {
117+
isWorkspaceWatchable := canWatchDirectoryOrFile(tspath.GetPathComponents(workspaceDirectory, ""))
116118
rootPath := tspath.ToPath(currentDirectory, "", useCaseSensitiveFileNames)
117119
rootPathComponents := tspath.GetPathComponents(string(rootPath), "")
118120
isRootWatchable := canWatchDirectoryOrFile(rootPathComponents)
121+
comparePathsOptions := tspath.ComparePathsOptions{
122+
CurrentDirectory: currentDirectory,
123+
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
124+
}
119125

120126
return func(data map[tspath.Path]string) []string {
121127
// dir -> recursive
122128
globSet := make(map[string]bool)
123129
var seenDirs collections.Set[string]
130+
var includeWorkspace bool
124131

125132
for path, fileName := range data {
126133
// Assuming all of the input paths are filenames, we can avoid
@@ -130,6 +137,11 @@ func createResolutionLookupGlobMapper(currentDirectory string, useCaseSensitiveF
130137
continue
131138
}
132139

140+
if isWorkspaceWatchable && tspath.ContainsPath(workspaceDirectory, fileName, comparePathsOptions) {
141+
includeWorkspace = true
142+
continue
143+
}
144+
133145
w := getDirectoryToWatchFailedLookupLocation(
134146
fileName,
135147
path,
@@ -146,6 +158,9 @@ func createResolutionLookupGlobMapper(currentDirectory string, useCaseSensitiveF
146158
}
147159

148160
globs := make([]string, 0, len(globSet))
161+
if includeWorkspace {
162+
globs = append(globs, workspaceDirectory+"/"+recursiveFileGlobPattern)
163+
}
149164
for dir, recursive := range globSet {
150165
if recursive {
151166
globs = append(globs, dir+"/"+recursiveFileGlobPattern)
@@ -159,45 +174,43 @@ func createResolutionLookupGlobMapper(currentDirectory string, useCaseSensitiveF
159174
}
160175
}
161176

162-
func getTypingsLocationsGlobs(typingsFiles []string, typingsLocation string, currentDirectory string, useCaseSensitiveFileNames bool) (fileGlobs map[tspath.Path]string, directoryGlobs map[tspath.Path]string) {
177+
func getTypingsLocationsGlobs(
178+
typingsFiles []string,
179+
typingsLocation string,
180+
workspaceDirectory string,
181+
currentDirectory string,
182+
useCaseSensitiveFileNames bool,
183+
) map[tspath.Path]string {
184+
var includeTypingsLocation, includeWorkspace bool
185+
externalDirectories := make(map[tspath.Path]string)
186+
isWorkspaceWatchable := canWatchDirectoryOrFile(tspath.GetPathComponents(workspaceDirectory, ""))
187+
globs := make(map[tspath.Path]string)
163188
comparePathsOptions := tspath.ComparePathsOptions{
164189
CurrentDirectory: currentDirectory,
165190
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
166191
}
167192
for _, file := range typingsFiles {
168-
basename := tspath.GetBaseFileName(file)
169-
if basename == "package.json" || basename == "bower.json" {
170-
// package.json or bower.json exists, watch the file to detect changes and update typings
171-
if fileGlobs == nil {
172-
fileGlobs = map[tspath.Path]string{}
173-
}
174-
fileGlobs[tspath.ToPath(file, currentDirectory, useCaseSensitiveFileNames)] = file
193+
if tspath.ContainsPath(typingsLocation, file, comparePathsOptions) {
194+
includeTypingsLocation = true
195+
} else if !isWorkspaceWatchable || !tspath.ContainsPath(workspaceDirectory, file, comparePathsOptions) {
196+
directory := tspath.GetDirectoryPath(file)
197+
externalDirectories[tspath.ToPath(directory, currentDirectory, useCaseSensitiveFileNames)] = directory
175198
} else {
176-
var globLocation string
177-
// path in projectRoot, watch project root
178-
if tspath.ContainsPath(currentDirectory, file, comparePathsOptions) {
179-
currentDirectoryLen := len(currentDirectory) + 1
180-
subDirectory := strings.IndexRune(file[currentDirectoryLen:], tspath.DirectorySeparator)
181-
if subDirectory != -1 {
182-
// Watch subDirectory
183-
globLocation = file[0 : currentDirectoryLen+subDirectory]
184-
} else {
185-
// Watch the directory itself
186-
globLocation = file
187-
}
188-
} else {
189-
// path in global cache, watch global cache
190-
// else watch node_modules or bower_components
191-
globLocation = core.IfElse(tspath.ContainsPath(typingsLocation, file, comparePathsOptions), typingsLocation, file)
192-
}
193-
// package.json or bower.json exists, watch the file to detect changes and update typings
194-
if directoryGlobs == nil {
195-
directoryGlobs = map[tspath.Path]string{}
196-
}
197-
directoryGlobs[tspath.ToPath(globLocation, currentDirectory, useCaseSensitiveFileNames)] = fmt.Sprintf("%s/%s", globLocation, recursiveFileGlobPattern)
199+
includeWorkspace = true
198200
}
199201
}
200-
return fileGlobs, directoryGlobs
202+
externalDirectoryParents := tspath.GetCommonParents(slices.Collect(maps.Values(externalDirectories)), minWatchLocationDepth, comparePathsOptions)
203+
slices.Sort(externalDirectoryParents)
204+
if includeWorkspace {
205+
globs[tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames)] = workspaceDirectory + "/" + recursiveFileGlobPattern
206+
}
207+
if includeTypingsLocation {
208+
globs[tspath.ToPath(typingsLocation, currentDirectory, useCaseSensitiveFileNames)] = typingsLocation + "/" + recursiveFileGlobPattern
209+
}
210+
for _, dir := range externalDirectoryParents {
211+
globs[tspath.ToPath(dir, currentDirectory, useCaseSensitiveFileNames)] = dir + "/" + recursiveFileGlobPattern
212+
}
213+
return globs
201214
}
202215

203216
type directoryOfFailedLookupWatch struct {
@@ -371,7 +384,7 @@ func canWatchDirectoryOrFile(pathComponents []string) bool {
371384
length := len(pathComponents)
372385
// Ignore "/", "c:/"
373386
// ignore "/user", "c:/users" or "c:/folderAtRoot"
374-
if length < 2 {
387+
if length < minWatchLocationDepth {
375388
return false
376389
}
377390
perceivedOsRootLength := perceivedOsRootLengthForWatching(pathComponents, length)

0 commit comments

Comments
 (0)