package project import ( "fmt" "maps" "slices" "strings" "sync" "sync/atomic" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/tspath" ) const ( minWatchLocationDepth = 2 ) type fileSystemWatcherKey struct { pattern string kind lsproto.WatchKind } type fileSystemWatcherValue struct { count int id WatcherID } // watchRegistry tracks the current watch globs and how many individual // WatchedFiles reference each glob. It provides ref-count helpers so callers // don't manipulate the map directly. // // All methods are safe for concurrent use; locking is handled internally. type watchRegistry struct { mu sync.Mutex entries map[fileSystemWatcherKey]*fileSystemWatcherValue pending map[WatcherID]struct{} } func newWatchRegistry() *watchRegistry { return &watchRegistry{ entries: make(map[fileSystemWatcherKey]*fileSystemWatcherValue), pending: make(map[WatcherID]struct{}), } } // Acquire increments the ref count for a watcher. If this is the first // reference (count goes from 0 to 1), it returns true so the caller knows // to register the watcher with the client. func (r *watchRegistry) Acquire(watcher *lsproto.FileSystemWatcher, id WatcherID) (isNew bool) { r.mu.Lock() defer r.mu.Unlock() key := toFileSystemWatcherKey(watcher) value := r.entries[key] if value == nil { value = &fileSystemWatcherValue{id: id} r.entries[key] = value } value.count++ return value.count == 1 } // Release decrements the ref count for a watcher. If no references remain, // the entry is removed and the function returns the WatcherID and true so // the caller knows to unregister the watcher from the client. func (r *watchRegistry) Release(watcher *lsproto.FileSystemWatcher) (id WatcherID, removed bool) { r.mu.Lock() defer r.mu.Unlock() key := toFileSystemWatcherKey(watcher) value := r.entries[key] if value == nil { return "", false } if value.count <= 1 { delete(r.entries, key) return value.id, true } value.count-- return "", false } // MarkPending records that a watcher's registration failed and needs retry. func (r *watchRegistry) MarkPending(id WatcherID) { r.mu.Lock() defer r.mu.Unlock() r.pending[id] = struct{}{} } // ClearPending removes a watcher from the pending set after successful registration. func (r *watchRegistry) ClearPending(id WatcherID) { r.mu.Lock() defer r.mu.Unlock() delete(r.pending, id) } // IsPending returns true if the watcher needs retry due to a previous failure. func (r *watchRegistry) IsPending(id WatcherID) bool { r.mu.Lock() defer r.mu.Unlock() _, ok := r.pending[id] return ok } type PatternsAndIgnored struct { directoriesOutsideWorkspace []string patternsInsideWorkspace []string ignored map[string]struct{} } // toFileSystemWatcherKey produces a deduplication key for a file system watcher. // Note: this key is a simple string concatenation of the base and pattern, so // structurally different watchers (Pattern vs RelativePattern, URI vs WorkspaceFolder) // could theoretically collide. In practice, workspace watchers use plain Pattern // with filesystem paths while outside-workspace watchers use RelativePattern with // file:// URIs, so collisions don't occur. func toFileSystemWatcherKey(w *lsproto.FileSystemWatcher) fileSystemWatcherKey { kind := w.Kind if kind == nil { kind = new(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete) } var pattern string if w.GlobPattern.Pattern != nil { pattern = *w.GlobPattern.Pattern } else if w.GlobPattern.RelativePattern != nil { var base string if w.GlobPattern.RelativePattern.BaseUri.URI != nil { base = string(*w.GlobPattern.RelativePattern.BaseUri.URI) } else if w.GlobPattern.RelativePattern.BaseUri.WorkspaceFolder != nil { panic("workspace folder-based relative patterns not implemented") } pattern = base + "/" + w.GlobPattern.RelativePattern.Pattern } return fileSystemWatcherKey{pattern: pattern, kind: *kind} } func fileSystemWatcherGlobString(w *lsproto.FileSystemWatcher) string { if w.GlobPattern.Pattern != nil { return *w.GlobPattern.Pattern } if w.GlobPattern.RelativePattern != nil { var base string if w.GlobPattern.RelativePattern.BaseUri.URI != nil { base = string(*w.GlobPattern.RelativePattern.BaseUri.URI) } else if w.GlobPattern.RelativePattern.BaseUri.WorkspaceFolder != nil { panic("workspace folder-based relative patterns not implemented") } return base + "/" + w.GlobPattern.RelativePattern.Pattern } return "" } type WatcherID string var watcherID atomic.Uint64 type WatchedFiles[T any] struct { name string watchKind lsproto.WatchKind hasRelativePatternCapability bool computeGlobPatterns func(input T) PatternsAndIgnored mu sync.RWMutex input T computeWatchersOnce sync.Once workspaceWatchers []*lsproto.FileSystemWatcher outsideWorkspaceWatchers []*lsproto.FileSystemWatcher ignored map[string]struct{} id uint64 } func NewWatchedFiles[T any](name string, watchKind lsproto.WatchKind, hasRelativePatternCapability bool, computeGlobPatterns func(input T) PatternsAndIgnored) *WatchedFiles[T] { return &WatchedFiles[T]{ id: watcherID.Add(1), name: name, watchKind: watchKind, hasRelativePatternCapability: hasRelativePatternCapability, computeGlobPatterns: computeGlobPatterns, } } type Watchers struct { WatcherID WatcherID WorkspaceWatchers []*lsproto.FileSystemWatcher OutsideWorkspaceWatchers []*lsproto.FileSystemWatcher IgnoredPaths map[string]struct{} } func (w *WatchedFiles[T]) Watchers() Watchers { w.computeWatchersOnce.Do(func() { w.mu.Lock() defer w.mu.Unlock() result := w.computeGlobPatterns(w.input) globs := slices.Compact(slices.Sorted(slices.Values(result.patternsInsideWorkspace))) ignored := result.ignored // ignored is only used for logging and doesn't affect watcher identity w.ignored = ignored changed := false if !slices.EqualFunc(w.workspaceWatchers, globs, func(a *lsproto.FileSystemWatcher, b string) bool { return *a.GlobPattern.Pattern == b }) { w.workspaceWatchers = core.Map(globs, func(glob string) *lsproto.FileSystemWatcher { return &lsproto.FileSystemWatcher{ GlobPattern: lsproto.PatternOrRelativePattern{ Pattern: &glob, }, Kind: &w.watchKind, } }) changed = true } dirsOutside := slices.Compact(slices.Sorted(slices.Values(result.directoriesOutsideWorkspace))) if !slices.EqualFunc(w.outsideWorkspaceWatchers, dirsOutside, func(a *lsproto.FileSystemWatcher, b string) bool { return fileSystemWatcherGlobString(a) == recursiveDirectoryGlobPattern(b, w.hasRelativePatternCapability) }) { w.outsideWorkspaceWatchers = core.Map(dirsOutside, func(dir string) *lsproto.FileSystemWatcher { return newRecursiveDirectoryWatcher(dir, w.watchKind, w.hasRelativePatternCapability) }) changed = true } if changed { w.id = watcherID.Add(1) } }) w.mu.RLock() defer w.mu.RUnlock() return Watchers{ WatcherID: WatcherID(fmt.Sprintf("%s watcher %d", w.name, w.id)), WorkspaceWatchers: w.workspaceWatchers, OutsideWorkspaceWatchers: w.outsideWorkspaceWatchers, IgnoredPaths: w.ignored, } } func (w *WatchedFiles[T]) ID() WatcherID { if w == nil { return "" } return w.Watchers().WatcherID } func (w *WatchedFiles[T]) Name() string { return w.name } func (w *WatchedFiles[T]) WatchKind() lsproto.WatchKind { return w.watchKind } func (w *WatchedFiles[T]) Clone(input T) *WatchedFiles[T] { if w == nil { return nil } w.mu.RLock() defer w.mu.RUnlock() return &WatchedFiles[T]{ name: w.name, watchKind: w.watchKind, hasRelativePatternCapability: w.hasRelativePatternCapability, computeGlobPatterns: w.computeGlobPatterns, workspaceWatchers: w.workspaceWatchers, outsideWorkspaceWatchers: w.outsideWorkspaceWatchers, input: input, } } func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data *collections.SyncSet[tspath.Path]) PatternsAndIgnored { workspaceDirectoryPath := tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames) currentDirectoryPath := tspath.ToPath(currentDirectory, currentDirectory, useCaseSensitiveFileNames) libDirectoryPath := tspath.ToPath(libDirectory, currentDirectory, useCaseSensitiveFileNames) return func(data *collections.SyncSet[tspath.Path]) PatternsAndIgnored { var ignored map[string]struct{} var seenDirs collections.Set[tspath.Path] var includeWorkspace, includeRoot, includeLib bool var nodeModulesDirectories collections.Set[tspath.Path] var externalDirectories collections.Set[tspath.Path] if data != nil { data.Range(func(path tspath.Path) bool { if tspath.IsDynamicFileName(string(path)) { return true } // Assuming all of the input paths are file paths, we can avoid // duplicate work by only taking one file per dir, since their outputs // will always be the same. if !seenDirs.AddIfAbsent(path.GetDirectoryPath()) { return true } if workspaceDirectoryPath.ContainsPath(path) { includeWorkspace = true } else if currentDirectoryPath.ContainsPath(path) { includeRoot = true } else if libDirectoryPath.ContainsPath(path) { includeLib = true } else if idx := strings.Index(string(path), "/node_modules/"); idx != -1 { nodeModulesDirectories.Add(path[:idx+len("/node_modules")]) } else { externalDirectories.Add(path.GetDirectoryPath()) } return true }) } var globs []string if includeWorkspace { globs = append(globs, getRecursiveGlobPattern(string(workspaceDirectoryPath))) } if includeRoot { globs = append(globs, getRecursiveGlobPattern(string(currentDirectoryPath))) } if includeLib { globs = append(globs, getRecursiveGlobPattern(string(libDirectoryPath))) } if nodeModulesDirectories.Len() > 0 { nodeModulesGlobs := make([]string, 0, nodeModulesDirectories.Len()) for dir := range nodeModulesDirectories.Keys() { nodeModulesGlobs = append(nodeModulesGlobs, getRecursiveGlobPattern(string(dir))) } slices.Sort(nodeModulesGlobs) globs = append(globs, nodeModulesGlobs...) } var outsideDirs []string if externalDirectories.Len() > 0 { externalDirStrings := make([]string, 0, externalDirectories.Len()) for dir := range externalDirectories.Keys() { externalDirStrings = append(externalDirStrings, string(dir)) } externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents( externalDirStrings, minWatchLocationDepth, getPathComponentsForWatching, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}, // Already using tspath.Path ) slices.Sort(externalDirectoryParents) ignored = ignoredExternalDirs outsideDirs = externalDirectoryParents } return PatternsAndIgnored{ directoriesOutsideWorkspace: outsideDirs, patternsInsideWorkspace: globs, ignored: ignored, } } } func getTypingsLocationsGlobs( typingsFiles []string, typingsLocation string, workspaceDirectory string, currentDirectory string, useCaseSensitiveFileNames bool, ) PatternsAndIgnored { var includeTypingsLocation, includeWorkspace bool externalDirectories := make(map[tspath.Path]string) globs := make(map[tspath.Path]string) comparePathsOptions := tspath.ComparePathsOptions{ CurrentDirectory: currentDirectory, UseCaseSensitiveFileNames: useCaseSensitiveFileNames, } for _, file := range typingsFiles { if tspath.ContainsPath(typingsLocation, file, comparePathsOptions) { includeTypingsLocation = true } else if !tspath.ContainsPath(workspaceDirectory, file, comparePathsOptions) { directory := tspath.GetDirectoryPath(file) externalDirectories[tspath.ToPath(directory, currentDirectory, useCaseSensitiveFileNames)] = directory } else { includeWorkspace = true } } externalDirectoryParents, ignored := tspath.GetCommonParents( slices.Collect(maps.Values(externalDirectories)), minWatchLocationDepth, getPathComponentsForWatching, comparePathsOptions, ) slices.Sort(externalDirectoryParents) if includeWorkspace { globs[tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(workspaceDirectory) } if includeTypingsLocation { globs[tspath.ToPath(typingsLocation, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(typingsLocation) } return PatternsAndIgnored{ directoriesOutsideWorkspace: externalDirectoryParents, patternsInsideWorkspace: slices.Collect(maps.Values(globs)), ignored: ignored, } } func getPathComponentsForWatching(path string, currentDirectory string) []string { components := tspath.GetPathComponents(path, currentDirectory) rootLength := perceivedOsRootLengthForWatching(components) if rootLength <= 1 { return components } newRoot := tspath.CombinePaths(components[0], components[1:rootLength]...) return append([]string{newRoot}, components[rootLength:]...) } func perceivedOsRootLengthForWatching(pathComponents []string) int { length := len(pathComponents) if length <= 1 { return length } if strings.HasPrefix(pathComponents[0], "//") { // Group UNC roots (//server/share) into a single component return 2 } if len(pathComponents[0]) == 3 && tspath.IsVolumeCharacter(pathComponents[0][0]) && pathComponents[0][1] == ':' && pathComponents[0][2] == '/' { // Windows-style volume if strings.EqualFold(pathComponents[1], "users") { // Group C:/Users/username into a single component return min(3, length) } return 1 } if pathComponents[1] == "home" { // Group /home/username into a single component return min(3, length) } return 1 } func getRecursiveGlobPattern(directory string) string { return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory), "**/*") } // recursiveDirectoryGlobPattern returns the string form of a recursive watcher // for the given directory that would be produced by newRecursiveDirectoryWatcher. func recursiveDirectoryGlobPattern(directory string, useRelativePattern bool) string { if useRelativePattern { return string(lsconv.FileNameToDocumentURI(directory)) + "/**/*" } return getRecursiveGlobPattern(directory) } // newRecursiveDirectoryWatcher creates a FileSystemWatcher for recursively // watching a directory. When useRelativePattern is true, a RelativePattern with // a file:// base URI is used; otherwise a plain glob Pattern is used. func newRecursiveDirectoryWatcher(directory string, kind lsproto.WatchKind, useRelativePattern bool) *lsproto.FileSystemWatcher { if useRelativePattern { baseUri := lsproto.URI(lsconv.FileNameToDocumentURI(directory)) return &lsproto.FileSystemWatcher{ GlobPattern: lsproto.PatternOrRelativePattern{ RelativePattern: &lsproto.RelativePattern{ BaseUri: lsproto.WorkspaceFolderOrURI{ URI: &baseUri, }, Pattern: "**/*", }, }, Kind: &kind, } } glob := getRecursiveGlobPattern(directory) return &lsproto.FileSystemWatcher{ GlobPattern: lsproto.PatternOrRelativePattern{ Pattern: &glob, }, Kind: &kind, } }