Skip to content

Commit 20ab1fd

Browse files
committed
Add non-root program file watch, fix watcher removal
1 parent 4f1094d commit 20ab1fd

File tree

7 files changed

+153
-84
lines changed

7 files changed

+153
-84
lines changed

internal/project/configfileregistrybuilder.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry
166166
}
167167

168168
var globs []string
169-
var externalFiles []string
169+
var externalDirectories []string
170170
var includeWorkspace bool
171171
var includeTsconfigDir bool
172172
canWatchWorkspace := canWatchDirectoryOrFile(tspath.GetPathComponents(c.sessionOptions.CurrentDirectory, ""))
@@ -183,7 +183,7 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry
183183
} else if canWatchTsconfigDir && tspath.ContainsPath(tsconfigDir, dir, comparePathsOptions) {
184184
includeTsconfigDir = true
185185
} else {
186-
externalFiles = append(externalFiles, dir)
186+
externalDirectories = append(externalDirectories, dir)
187187
}
188188
}
189189
for _, fileName := range entry.commandLine.LiteralFileNames() {
@@ -192,7 +192,7 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry
192192
} else if canWatchTsconfigDir && tspath.ContainsPath(tsconfigDir, fileName, comparePathsOptions) {
193193
includeTsconfigDir = true
194194
} else {
195-
externalFiles = append(externalFiles, fileName)
195+
externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(fileName))
196196
}
197197
}
198198

@@ -208,8 +208,8 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry
208208
}
209209
globs = append(globs, fileName)
210210
}
211-
if len(externalFiles) > 0 {
212-
for _, parent := range tspath.GetCommonParents(externalFiles, minWatchLocationDepth, comparePathsOptions) {
211+
if len(externalDirectories) > 0 {
212+
for _, parent := range tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, comparePathsOptions) {
213213
globs = append(globs, fmt.Sprintf("%s/%s", parent, recursiveFileGlobPattern))
214214
}
215215
}

internal/project/project.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type Project struct {
7272
// The ID of the snapshot that created the program stored in this project.
7373
ProgramLastUpdate uint64
7474

75+
programFilesWatch *WatchedFiles[[]string]
7576
failedLookupsWatch *WatchedFiles[map[tspath.Path]string]
7677
affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]
7778
typingsWatch *WatchedFiles[map[tspath.Path]string]
@@ -148,6 +149,11 @@ func NewProject(
148149

149150
project.configFilePath = tspath.ToPath(configFileName, currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames())
150151
if builder.sessionOptions.WatchEnabled {
152+
project.programFilesWatch = NewWatchedFiles(
153+
"non-root program files for "+configFileName,
154+
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
155+
core.Identity,
156+
)
151157
project.failedLookupsWatch = NewWatchedFiles(
152158
"failed lookups for "+configFileName,
153159
lsproto.WatchKindCreate,
@@ -219,6 +225,7 @@ func (p *Project) Clone() *Project {
219225
ProgramUpdateKind: ProgramUpdateKindNone,
220226
ProgramLastUpdate: p.ProgramLastUpdate,
221227

228+
programFilesWatch: p.programFilesWatch,
222229
failedLookupsWatch: p.failedLookupsWatch,
223230
affectingLocationsWatch: p.affectingLocationsWatch,
224231
typingsWatch: p.typingsWatch,
@@ -324,14 +331,19 @@ func (p *Project) CreateProgram() CreateProgramResult {
324331
}
325332
}
326333

327-
func (p *Project) CloneWatchers() (failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) {
334+
func (p *Project) CloneWatchers(workspaceDir string) (programFilesWatch *WatchedFiles[[]string], failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) {
328335
failedLookups := make(map[tspath.Path]string)
329336
affectingLocations := make(map[tspath.Path]string)
337+
programFiles := getNonRootFileGlobs(workspaceDir, p.Program.GetSourceFiles(), p.CommandLine.FileNamesByPath(), tspath.ComparePathsOptions{
338+
UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(),
339+
CurrentDirectory: p.currentDirectory,
340+
})
330341
extractLookups(p.toPath, failedLookups, affectingLocations, p.Program.GetResolvedModules())
331342
extractLookups(p.toPath, failedLookups, affectingLocations, p.Program.GetResolvedTypeReferenceDirectives())
343+
programFilesWatch = p.programFilesWatch.Clone(programFiles)
332344
failedLookupsWatch = p.failedLookupsWatch.Clone(failedLookups)
333345
affectingLocationsWatch = p.affectingLocationsWatch.Clone(affectingLocations)
334-
return failedLookupsWatch, affectingLocationsWatch
346+
return programFilesWatch, failedLookupsWatch, affectingLocationsWatch
335347
}
336348

337349
func (p *Project) log(msg string) {

internal/project/projectcollectionbuilder.go

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

internal/project/session.go

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ type Session struct {
111111

112112
// watches tracks the current watch globs and how many individual WatchedFiles
113113
// are using each glob.
114-
watches map[fileSystemWatcherKey]int
114+
watches map[fileSystemWatcherKey]*fileSystemWatcherValue
115115
watchesMu sync.Mutex
116116
}
117117

@@ -154,7 +154,7 @@ func NewSession(init *SessionInit) *Session {
154154
toPath,
155155
),
156156
pendingATAChanges: make(map[tspath.Path]*ATAStateChange),
157-
watches: make(map[fileSystemWatcherKey]int),
157+
watches: make(map[fileSystemWatcherKey]*fileSystemWatcherValue),
158158
}
159159

160160
if init.Options.TypingsLocation != "" && init.NpmExecutor != nil {
@@ -422,43 +422,49 @@ func updateWatch[T any](ctx context.Context, session *Session, logger logging.Lo
422422
defer session.watchesMu.Unlock()
423423
if newWatcher != nil {
424424
if id, watchers := newWatcher.Watchers(); len(watchers) > 0 {
425-
var newWatchers []*lsproto.FileSystemWatcher
426-
for _, watcher := range watchers {
425+
var newWatchers collections.OrderedMap[WatcherID, *lsproto.FileSystemWatcher]
426+
for i, watcher := range watchers {
427427
key := toFileSystemWatcherKey(watcher)
428-
count := session.watches[key]
429-
session.watches[key] = count + 1
430-
if count == 0 {
431-
newWatchers = append(newWatchers, watcher)
428+
value := session.watches[key]
429+
globId := WatcherID(fmt.Sprintf("%s.%d", id, i))
430+
if value == nil {
431+
value = &fileSystemWatcherValue{id: globId}
432+
session.watches[key] = value
433+
}
434+
value.count++
435+
if value.count == 1 {
436+
newWatchers.Set(globId, watcher)
432437
}
433438
}
434-
if len(newWatchers) > 0 {
435-
if err := session.client.WatchFiles(ctx, id, newWatchers); err != nil {
439+
for id, watcher := range newWatchers.Entries() {
440+
if err := session.client.WatchFiles(ctx, id, []*lsproto.FileSystemWatcher{watcher}); err != nil {
436441
errors = append(errors, err)
437442
} else if logger != nil {
438443
if oldWatcher == nil {
439444
logger.Log(fmt.Sprintf("Added new watch: %s", id))
440445
} else {
441446
logger.Log(fmt.Sprintf("Updated watch: %s", id))
442447
}
443-
for _, watcher := range watchers {
444-
logger.Log("\t" + *watcher.GlobPattern.Pattern)
445-
}
448+
logger.Log("\t" + *watcher.GlobPattern.Pattern)
446449
logger.Log("")
447450
}
448451
}
449452
}
450453
}
451454
if oldWatcher != nil {
452-
if id, watchers := oldWatcher.Watchers(); len(watchers) > 0 {
455+
if _, watchers := oldWatcher.Watchers(); len(watchers) > 0 {
453456
var removedWatchers []WatcherID
454457
for _, watcher := range watchers {
455458
key := toFileSystemWatcherKey(watcher)
456-
count := session.watches[key]
457-
if count <= 1 {
459+
value := session.watches[key]
460+
if value == nil {
461+
continue
462+
}
463+
if value.count <= 1 {
458464
delete(session.watches, key)
459-
removedWatchers = append(removedWatchers, id)
465+
removedWatchers = append(removedWatchers, value.id)
460466
} else {
461-
session.watches[key] = count - 1
467+
value.count--
462468
}
463469
}
464470
for _, id := range removedWatchers {
@@ -498,16 +504,21 @@ func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) er
498504
oldSnapshot.ProjectCollection.ProjectsByPath(),
499505
newSnapshot.ProjectCollection.ProjectsByPath(),
500506
func(_ tspath.Path, addedProject *Project) {
507+
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.programFilesWatch)...)
501508
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.affectingLocationsWatch)...)
502509
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.failedLookupsWatch)...)
503510
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.typingsWatch)...)
504511
},
505512
func(_ tspath.Path, removedProject *Project) {
513+
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.programFilesWatch, nil)...)
506514
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.affectingLocationsWatch, nil)...)
507515
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.failedLookupsWatch, nil)...)
508516
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.typingsWatch, nil)...)
509517
},
510518
func(_ tspath.Path, oldProject, newProject *Project) {
519+
if oldProject.programFilesWatch.ID() != newProject.programFilesWatch.ID() {
520+
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.programFilesWatch, newProject.programFilesWatch)...)
521+
}
511522
if oldProject.affectingLocationsWatch.ID() != newProject.affectingLocationsWatch.ID() {
512523
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.affectingLocationsWatch, newProject.affectingLocationsWatch)...)
513524
}

internal/project/session_test.go

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package project_test
33
import (
44
"context"
55
"maps"
6+
"strings"
67
"testing"
78

89
"github.com/microsoft/typescript-go/internal/bundled"
910
"github.com/microsoft/typescript-go/internal/core"
1011
"github.com/microsoft/typescript-go/internal/glob"
1112
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
13+
"github.com/microsoft/typescript-go/internal/project"
1214
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
1315
"github.com/microsoft/typescript-go/internal/tspath"
1416
"gotest.tools/v3/assert"
@@ -552,51 +554,63 @@ func TestSession(t *testing.T) {
552554

553555
t.Run("change program file not in tsconfig root files", func(t *testing.T) {
554556
t.Parallel()
555-
files := map[string]any{
556-
"/home/projects/TS/p1/tsconfig.json": `{
557-
"compilerOptions": {
558-
"noLib": true,
559-
"module": "nodenext",
560-
"strict": true
561-
},
562-
"files": ["src/index.ts"]
563-
}`,
564-
"/home/projects/TS/p1/src/index.ts": `import { x } from "../../x";`,
565-
"/home/projects/TS/x.ts": `export const x = 1;`,
566-
}
567-
568-
session, utils := projecttestutil.Setup(files)
569-
session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
570-
lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
571-
assert.NilError(t, err)
572-
programBefore := lsBefore.GetProgram()
573-
session.WaitForBackgroundTasks()
557+
for _, workspaceDir := range []string{"/", "/home/projects/TS/p1", "/somewhere/else/entirely"} {
558+
t.Run("workspaceDir="+strings.ReplaceAll(workspaceDir, "/", "_"), func(t *testing.T) {
559+
t.Parallel()
560+
files := map[string]any{
561+
"/home/projects/TS/p1/tsconfig.json": `{
562+
"compilerOptions": {
563+
"noLib": true,
564+
"module": "nodenext",
565+
"strict": true
566+
},
567+
"files": ["src/index.ts"]
568+
}`,
569+
"/home/projects/TS/p1/src/index.ts": `import { x } from "../../x";`,
570+
"/home/projects/TS/x.ts": `export const x = 1;`,
571+
}
574572

575-
var xWatched bool
576-
outer:
577-
for _, call := range utils.Client().WatchFilesCalls() {
578-
for _, watcher := range call.Watchers {
579-
if core.Must(glob.Parse(*watcher.GlobPattern.Pattern)).Match("/home/projects/TS/x.ts") {
580-
xWatched = true
581-
break outer
573+
session, utils := projecttestutil.SetupWithOptions(files, &project.SessionOptions{
574+
CurrentDirectory: workspaceDir,
575+
DefaultLibraryPath: bundled.LibPath(),
576+
TypingsLocation: projecttestutil.TestTypingsLocation,
577+
PositionEncoding: lsproto.PositionEncodingKindUTF8,
578+
WatchEnabled: true,
579+
LoggingEnabled: true,
580+
})
581+
session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
582+
lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
583+
assert.NilError(t, err)
584+
programBefore := lsBefore.GetProgram()
585+
session.WaitForBackgroundTasks()
586+
587+
var xWatched bool
588+
outer:
589+
for _, call := range utils.Client().WatchFilesCalls() {
590+
for _, watcher := range call.Watchers {
591+
if core.Must(glob.Parse(*watcher.GlobPattern.Pattern)).Match("/home/projects/TS/x.ts") {
592+
xWatched = true
593+
break outer
594+
}
595+
}
582596
}
583-
}
584-
}
585-
assert.Check(t, xWatched)
597+
assert.Check(t, xWatched)
586598

587-
err = utils.FS().WriteFile("/home/projects/TS/x.ts", `export const x = 2;`, false)
588-
assert.NilError(t, err)
599+
err = utils.FS().WriteFile("/home/projects/TS/x.ts", `export const x = 2;`, false)
600+
assert.NilError(t, err)
589601

590-
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
591-
{
592-
Type: lsproto.FileChangeTypeChanged,
593-
Uri: "file:///home/projects/TS/x.ts",
594-
},
595-
})
602+
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
603+
{
604+
Type: lsproto.FileChangeTypeChanged,
605+
Uri: "file:///home/projects/TS/x.ts",
606+
},
607+
})
596608

597-
lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
598-
assert.NilError(t, err)
599-
assert.Check(t, lsAfter.GetProgram() != programBefore)
609+
lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts")
610+
assert.NilError(t, err)
611+
assert.Check(t, lsAfter.GetProgram() != programBefore)
612+
})
613+
}
600614
})
601615

602616
t.Run("change config file", func(t *testing.T) {

internal/project/util.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,8 @@ package project
22

33
import (
44
"strings"
5-
6-
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
75
)
86

97
func isDynamicFileName(fileName string) bool {
108
return strings.HasPrefix(fileName, "^")
119
}
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-
}

0 commit comments

Comments
 (0)