vendor tsgo
This commit is contained in:
802
tools/tsgo/internal/project/snapshotfs.go
Normal file
802
tools/tsgo/internal/project/snapshotfs.go
Normal file
@@ -0,0 +1,802 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/project/dirty"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
|
||||
"github.com/zeebo/xxh3"
|
||||
)
|
||||
|
||||
type FileSource interface {
|
||||
FS() vfs.FS
|
||||
GetFile(fileName string) FileHandle
|
||||
GetFileByPath(fileName string, path tspath.Path) FileHandle
|
||||
FileExists(fileName string, path tspath.Path) bool
|
||||
GetAccessibleEntries(path string) vfs.Entries
|
||||
}
|
||||
|
||||
var (
|
||||
_ FileSource = (*snapshotFSBuilder)(nil)
|
||||
_ FileSource = (*SnapshotFS)(nil)
|
||||
)
|
||||
|
||||
// realpathAliasSet is a thread-safe set of symlink paths that alias a single realpath.
|
||||
// It implements dirty.Cloneable so it can be used as a value in dirty.SyncMap.
|
||||
type realpathAliasSet struct {
|
||||
mu sync.Mutex
|
||||
paths collections.Set[tspath.Path]
|
||||
}
|
||||
|
||||
func (s *realpathAliasSet) Add(path tspath.Path) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.paths.Add(path)
|
||||
}
|
||||
|
||||
func (s *realpathAliasSet) Clone() *realpathAliasSet {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
clone := &realpathAliasSet{}
|
||||
if s.paths.Len() > 0 {
|
||||
clone.paths = *s.paths.Clone()
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
type SnapshotFS struct {
|
||||
toPath func(fileName string) tspath.Path
|
||||
fs vfs.FS
|
||||
overlays map[tspath.Path]*Overlay
|
||||
overlayDirectories map[tspath.Path]map[tspath.Path]string
|
||||
diskFiles map[tspath.Path]*diskFile
|
||||
diskDirectories map[tspath.Path]dirty.CloneableMap[tspath.Path, string]
|
||||
readFiles collections.SyncMap[tspath.Path, memoizedDiskFile]
|
||||
// nodeModulesRealpathAliases maps realpath-based keys to sets of symlink-based keys,
|
||||
// for files inside node_modules that are accessed through directory symlinks.
|
||||
// This allows watch events (which use realpaths) to invalidate files cached under symlink paths.
|
||||
nodeModulesRealpathAliases map[tspath.Path]*realpathAliasSet
|
||||
}
|
||||
|
||||
type memoizedDiskFile func() FileHandle
|
||||
|
||||
func (s *SnapshotFS) FS() vfs.FS {
|
||||
return s.fs
|
||||
}
|
||||
|
||||
func (s *SnapshotFS) GetFile(fileName string) FileHandle {
|
||||
return s.GetFileByPath(fileName, s.toPath(fileName))
|
||||
}
|
||||
|
||||
func (s *SnapshotFS) FileExists(fileName string, path tspath.Path) bool {
|
||||
if _, ok := s.overlays[path]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := s.diskFiles[path]; ok {
|
||||
return true
|
||||
}
|
||||
return s.fs.FileExists(fileName)
|
||||
}
|
||||
|
||||
func (s *SnapshotFS) GetFileByPath(fileName string, path tspath.Path) FileHandle {
|
||||
if file, ok := s.overlays[path]; ok {
|
||||
return file
|
||||
}
|
||||
if file, ok := s.diskFiles[path]; ok {
|
||||
return file
|
||||
}
|
||||
newEntry := memoizedDiskFile(sync.OnceValue(func() FileHandle {
|
||||
if contents, ok := s.fs.ReadFile(fileName); ok {
|
||||
return newDiskFile(fileName, contents)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
entry, _ := s.readFiles.LoadOrStore(path, newEntry)
|
||||
return entry()
|
||||
}
|
||||
|
||||
func (s *SnapshotFS) GetAccessibleEntries(directoryName string) vfs.Entries {
|
||||
var entries vfs.Entries
|
||||
path := s.toPath(directoryName)
|
||||
if diskDirectories, ok := s.diskDirectories[path]; ok {
|
||||
readDirectoryIntoEntries(diskDirectories, s.isFile, &entries)
|
||||
}
|
||||
if overlayDirectories, ok := s.overlayDirectories[path]; ok {
|
||||
readDirectoryIntoEntries(overlayDirectories, s.isFile, &entries)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *SnapshotFS) isOpenFile(fileName string) bool {
|
||||
path := s.toPath(fileName)
|
||||
_, ok := s.overlays[path]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *SnapshotFS) isFile(path tspath.Path) bool {
|
||||
if _, ok := s.diskFiles[path]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := s.overlays[path]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type snapshotFSBuilder struct {
|
||||
fs vfs.FS
|
||||
prevOverlays map[tspath.Path]*Overlay
|
||||
overlays map[tspath.Path]*Overlay
|
||||
overlayDirectories map[tspath.Path]map[tspath.Path]string
|
||||
diskFiles *dirty.SyncMap[tspath.Path, *diskFile]
|
||||
diskDirectories *dirty.Map[tspath.Path, dirty.CloneableMap[tspath.Path, string]]
|
||||
nodeModulesRealpathAliases *dirty.SyncMap[tspath.Path, *realpathAliasSet]
|
||||
toPath func(string) tspath.Path
|
||||
accessibleEntries collections.SyncMap[tspath.Path, *vfs.Entries]
|
||||
}
|
||||
|
||||
func newSnapshotFSBuilder(
|
||||
fs vfs.FS,
|
||||
prevOverlays map[tspath.Path]*Overlay,
|
||||
overlays map[tspath.Path]*Overlay,
|
||||
diskFiles map[tspath.Path]*diskFile,
|
||||
diskDirectories map[tspath.Path]dirty.CloneableMap[tspath.Path, string],
|
||||
nodeModulesRealpathAliases map[tspath.Path]*realpathAliasSet,
|
||||
positionEncoding lsproto.PositionEncodingKind,
|
||||
toPath func(fileName string) tspath.Path,
|
||||
) *snapshotFSBuilder {
|
||||
cachedFS := cachedvfs.From(fs)
|
||||
cachedFS.Enable()
|
||||
|
||||
overlayDirectories := make(map[tspath.Path]map[tspath.Path]string)
|
||||
for path := range overlays {
|
||||
childPath := path
|
||||
child := overlays[path].FileName()
|
||||
for {
|
||||
parentPath := childPath.GetDirectoryPath()
|
||||
parent := tspath.GetDirectoryPath(child)
|
||||
if childPath == parentPath {
|
||||
break // reached root
|
||||
}
|
||||
baseName := tspath.GetBaseFileName(child)
|
||||
if dir, ok := overlayDirectories[parentPath]; ok {
|
||||
dir[childPath] = baseName
|
||||
} else {
|
||||
dir := make(map[tspath.Path]string)
|
||||
overlayDirectories[parentPath] = dir
|
||||
dir[childPath] = baseName
|
||||
}
|
||||
childPath = parentPath
|
||||
child = parent
|
||||
}
|
||||
}
|
||||
|
||||
return &snapshotFSBuilder{
|
||||
fs: cachedFS,
|
||||
prevOverlays: prevOverlays,
|
||||
overlays: overlays,
|
||||
overlayDirectories: overlayDirectories,
|
||||
diskFiles: dirty.NewSyncMap(diskFiles),
|
||||
diskDirectories: dirty.NewMap(diskDirectories),
|
||||
nodeModulesRealpathAliases: dirty.NewSyncMap(nodeModulesRealpathAliases),
|
||||
toPath: toPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) FS() vfs.FS {
|
||||
return s.fs
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) Finalize() (*SnapshotFS, bool) {
|
||||
// Synchronize directory structure based on added and deleted files (including overlays)
|
||||
var onDeletedFileOrDirectory func(path tspath.Path)
|
||||
var deleted map[tspath.Path]*diskFile
|
||||
|
||||
onAddedFile := func(path tspath.Path, fileName string) {
|
||||
childPath := path
|
||||
child := fileName
|
||||
for {
|
||||
parentPath := childPath.GetDirectoryPath()
|
||||
parent := tspath.GetDirectoryPath(child)
|
||||
if childPath == parentPath {
|
||||
break // reached root
|
||||
}
|
||||
baseName := tspath.GetBaseFileName(child)
|
||||
if dirEntry, ok := s.diskDirectories.Get(parentPath); ok {
|
||||
dirEntry.Change(func(dir dirty.CloneableMap[tspath.Path, string]) {
|
||||
dir[childPath] = baseName
|
||||
})
|
||||
break
|
||||
} else {
|
||||
dir := make(dirty.CloneableMap[tspath.Path, string])
|
||||
dir[childPath] = baseName
|
||||
s.diskDirectories.Add(parentPath, dir)
|
||||
}
|
||||
childPath = parentPath
|
||||
child = parent
|
||||
}
|
||||
}
|
||||
|
||||
onDeletedFileOrDirectory = func(path tspath.Path) {
|
||||
dirEntry, ok := s.diskDirectories.Get(path.GetDirectoryPath())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
dirEntry.Change(func(dir dirty.CloneableMap[tspath.Path, string]) {
|
||||
delete(dir, path)
|
||||
if len(dir) == 0 {
|
||||
dirEntry.Delete()
|
||||
onDeletedFileOrDirectory(dirEntry.Key())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
diskFiles, changed := s.diskFiles.FinalizeWith(dirty.FinalizationHooks[tspath.Path, *diskFile]{
|
||||
OnDelete: func(key tspath.Path, value *diskFile) {
|
||||
if deleted == nil {
|
||||
deleted = make(map[tspath.Path]*diskFile)
|
||||
}
|
||||
deleted[key] = value
|
||||
},
|
||||
OnAdd: func(key tspath.Path, value *diskFile) {
|
||||
onAddedFile(key, value.FileName())
|
||||
},
|
||||
})
|
||||
|
||||
for path := range deleted {
|
||||
onDeletedFileOrDirectory(path)
|
||||
}
|
||||
|
||||
// Prune deleted symlink paths from realpath alias sets before finalizing,
|
||||
// so that empty sets are dropped during finalization.
|
||||
for deletedPath, deletedFile := range deleted {
|
||||
if deletedFile.realpathPath == "" {
|
||||
continue
|
||||
}
|
||||
if entry, ok := s.nodeModulesRealpathAliases.Load(deletedFile.realpathPath); ok {
|
||||
entry.Locked(func(e dirty.Value[*realpathAliasSet]) {
|
||||
e.Change(func(aliasSet *realpathAliasSet) {
|
||||
aliasSet.paths.Delete(deletedPath)
|
||||
})
|
||||
if e.Value().paths.Len() == 0 {
|
||||
e.Delete()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nodeModulesRealpathAliases, aliasesChanged := s.nodeModulesRealpathAliases.Finalize()
|
||||
|
||||
return &SnapshotFS{
|
||||
fs: s.fs,
|
||||
overlays: s.overlays,
|
||||
overlayDirectories: s.overlayDirectories,
|
||||
diskFiles: diskFiles,
|
||||
diskDirectories: core.FirstResult(s.diskDirectories.Finalize()),
|
||||
nodeModulesRealpathAliases: nodeModulesRealpathAliases,
|
||||
toPath: s.toPath,
|
||||
}, changed || aliasesChanged
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) isOpenFile(path tspath.Path) bool {
|
||||
_, ok := s.overlays[path]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle {
|
||||
path := s.toPath(fileName)
|
||||
return s.GetFileByPath(fileName, path)
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) FileExists(fileName string, path tspath.Path) bool {
|
||||
if _, ok := s.overlays[path]; ok {
|
||||
return true
|
||||
}
|
||||
if entry, ok := s.diskFiles.Load(path); ok {
|
||||
val := entry.Value()
|
||||
if val == nil {
|
||||
return false
|
||||
}
|
||||
// Entry may be dirty - reload to check current state on disk.
|
||||
return s.reloadEntryIfNeeded(entry) != nil
|
||||
}
|
||||
// Path never loaded into diskFiles - use cached stat (no file read).
|
||||
return s.fs.FileExists(fileName)
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) FileHandle {
|
||||
if file, ok := s.overlays[path]; ok {
|
||||
return file
|
||||
}
|
||||
return s.getDiskFile(fileName, path, false)
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) GetAccessibleEntries(path string) vfs.Entries {
|
||||
entries := s.fs.GetAccessibleEntries(path)
|
||||
p := s.toPath(path)
|
||||
overlayDirectories, ok := s.overlayDirectories[p]
|
||||
if !ok {
|
||||
return entries
|
||||
}
|
||||
|
||||
if merged, ok := s.accessibleEntries.Load(p); ok {
|
||||
return *merged
|
||||
}
|
||||
merged := &vfs.Entries{
|
||||
Files: slices.Clip(entries.Files),
|
||||
Directories: slices.Clip(entries.Directories),
|
||||
Symlinks: entries.Symlinks,
|
||||
}
|
||||
readDirectoryIntoEntries(overlayDirectories, s.isOpenFile, merged)
|
||||
merged, _ = s.accessibleEntries.LoadOrStore(p, merged)
|
||||
return *merged
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) getDiskFile(fileName string, path tspath.Path, forceReload bool) FileHandle {
|
||||
entry, loaded := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true})
|
||||
if entry != nil {
|
||||
if !loaded && strings.Contains(string(path), "/node_modules/") {
|
||||
s.recordRealpathAlias(entry, fileName, path)
|
||||
}
|
||||
if forceReload {
|
||||
return s.reloadEntry(entry)
|
||||
}
|
||||
return s.reloadEntryIfNeeded(entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordRealpathAlias checks if fileName is accessed through a symlink and, if so,
|
||||
// records a mapping from the realpath-based key to the symlink-based key.
|
||||
// This is only called for files inside node_modules where symlinks are common.
|
||||
func (s *snapshotFSBuilder) recordRealpathAlias(diskFileEntry *dirty.SyncMapEntry[tspath.Path, *diskFile], symlinkFileName string, symlinkPath tspath.Path) {
|
||||
realpath := s.fs.Realpath(symlinkFileName)
|
||||
realpathPath := s.toPath(realpath)
|
||||
if realpathPath != symlinkPath {
|
||||
diskFileEntry.Change(func(file *diskFile) {
|
||||
file.realpathPath = realpathPath
|
||||
})
|
||||
entry, _ := s.nodeModulesRealpathAliases.LoadOrStore(realpathPath, &realpathAliasSet{})
|
||||
entry.Change(func(aliasSet *realpathAliasSet) {
|
||||
aliasSet.Add(symlinkPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) reloadEntry(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle {
|
||||
var fileName string
|
||||
entry.Locked(func(e dirty.Value[*diskFile]) {
|
||||
if e.Value() != nil {
|
||||
fileName = e.Value().fileName
|
||||
}
|
||||
})
|
||||
if fileName == "" {
|
||||
return nil
|
||||
}
|
||||
// Read file outside the lock to avoid blocking other goroutines.
|
||||
content, ok := s.fs.ReadFile(fileName)
|
||||
entry.Locked(func(e dirty.Value[*diskFile]) {
|
||||
if e.Value() == nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
e.Change(func(file *diskFile) {
|
||||
file.content = content
|
||||
file.hash = xxh3.HashString128(content)
|
||||
file.needsReload = false
|
||||
})
|
||||
} else {
|
||||
e.Delete()
|
||||
}
|
||||
})
|
||||
if entry.Value() == nil {
|
||||
return nil
|
||||
}
|
||||
return entry.Value()
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle {
|
||||
var fileName string
|
||||
entry.Locked(func(e dirty.Value[*diskFile]) {
|
||||
if e.Value() != nil && !e.Value().MatchesDiskText() {
|
||||
fileName = e.Value().fileName
|
||||
}
|
||||
})
|
||||
if fileName != "" {
|
||||
// Read file outside the lock to avoid blocking other goroutines.
|
||||
content, ok := s.fs.ReadFile(fileName)
|
||||
entry.Locked(func(e dirty.Value[*diskFile]) {
|
||||
if e.Value() == nil || e.Value().MatchesDiskText() {
|
||||
return // another goroutine already reloaded it
|
||||
}
|
||||
if ok {
|
||||
e.Change(func(file *diskFile) {
|
||||
file.content = content
|
||||
file.hash = xxh3.HashString128(content)
|
||||
file.needsReload = false
|
||||
})
|
||||
} else {
|
||||
e.Delete()
|
||||
}
|
||||
})
|
||||
}
|
||||
if entry.Value() == nil {
|
||||
return nil
|
||||
}
|
||||
return entry.Value()
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) watchChangesOverlapCache(change FileChangeSummary) bool {
|
||||
for uri := range change.Changed.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if _, ok := s.diskFiles.Load(path); ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := s.nodeModulesRealpathAliases.Load(path); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for uri := range change.Deleted.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if _, ok := s.diskFiles.Load(path); ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := s.nodeModulesRealpathAliases.Load(path); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) invalidateCache() {
|
||||
s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool {
|
||||
entry.Change(func(file *diskFile) {
|
||||
file.needsReload = true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) invalidateNodeModulesCache() {
|
||||
s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool {
|
||||
if strings.Contains(string(entry.Key()), "/node_modules/") {
|
||||
entry.Change(func(file *diskFile) {
|
||||
file.needsReload = true
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) {
|
||||
for uri := range change.Changed.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if entry, ok := s.diskFiles.Load(path); ok {
|
||||
entry.Change(func(file *diskFile) {
|
||||
file.needsReload = true
|
||||
})
|
||||
}
|
||||
}
|
||||
for uri := range change.Deleted.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if entry, ok := s.diskFiles.Load(path); ok {
|
||||
entry.Delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expandRealpathAliases adds synthetic URIs to the Changed and Deleted sets for
|
||||
// files that were accessed through node_modules symlinks. When a watch event arrives
|
||||
// using a realpath, this expands it to include the symlink-based path so that
|
||||
// downstream consumers (markDirtyFiles, markFilesChanged) can find cached entries.
|
||||
func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary) FileChangeSummary {
|
||||
if len(s.nodeModulesRealpathAliases) == 0 {
|
||||
return change
|
||||
}
|
||||
|
||||
var additionalChanged collections.Set[lsproto.DocumentUri]
|
||||
for uri := range change.Changed.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if aliases, ok := s.nodeModulesRealpathAliases[path]; ok {
|
||||
for aliasPath := range aliases.paths.Keys() {
|
||||
additionalChanged.Add(lsconv.FileNameToDocumentURI(string(aliasPath)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for uri := range additionalChanged.Keys() {
|
||||
change.Changed.Add(uri)
|
||||
}
|
||||
|
||||
var additionalDeleted collections.Set[lsproto.DocumentUri]
|
||||
for uri := range change.Deleted.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if aliases, ok := s.nodeModulesRealpathAliases[path]; ok {
|
||||
for aliasPath := range aliases.paths.Keys() {
|
||||
additionalDeleted.Add(lsconv.FileNameToDocumentURI(string(aliasPath)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for uri := range additionalDeleted.Keys() {
|
||||
change.Deleted.Add(uri)
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
// isRelevantFileName returns true if the given URI refers to a file that
|
||||
// could affect the project: it has a TypeScript-relevant extension, is a
|
||||
// dynamic (e.g. untitled) file, or is currently open as an overlay.
|
||||
func (s *snapshotFSBuilder) isRelevantFileName(uri lsproto.DocumentUri) bool {
|
||||
fileName := uri.FileName()
|
||||
if tspath.IsDynamicFileName(fileName) {
|
||||
return true
|
||||
}
|
||||
path := s.toPath(fileName)
|
||||
if _, ok := s.overlays[path]; ok {
|
||||
return true
|
||||
}
|
||||
i := strings.LastIndexByte(string(path), '.')
|
||||
if i < 0 {
|
||||
return false
|
||||
}
|
||||
switch string(path)[i:] {
|
||||
case ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// expandAndFilterWatchEvents expands directory deletion URIs into individual
|
||||
// file deletion URIs using the cached directory structure, and filters out
|
||||
// watch events for paths that are neither known directories nor have relevant
|
||||
// file extensions.
|
||||
func (s *snapshotFSBuilder) expandAndFilterWatchEvents(change FileChangeSummary) FileChangeSummary {
|
||||
if change.Deleted.Len() > 0 {
|
||||
var filteredDeleted collections.Set[lsproto.DocumentUri]
|
||||
for uri := range change.Deleted.Keys() {
|
||||
path := s.toPath(uri.FileName())
|
||||
if _, ok := s.diskDirectories.Get(path); ok {
|
||||
s.collectFilesRecursive(path, &filteredDeleted)
|
||||
} else if s.isRelevantFileName(uri) || isNodeModulesPath(path) {
|
||||
// node_modules deletions must always be preserved for auto-import registry change handlers.
|
||||
// They won't be in diskDirectories since the registry doesn't use the snapshotFSBuilder for
|
||||
// its file system, since we don't want to retain files read there.
|
||||
filteredDeleted.Add(uri)
|
||||
}
|
||||
}
|
||||
change.Deleted = filteredDeleted
|
||||
}
|
||||
|
||||
if change.Changed.Len() > 0 {
|
||||
var filteredChanged collections.Set[lsproto.DocumentUri]
|
||||
for uri := range change.Changed.Keys() {
|
||||
if s.isRelevantFileName(uri) {
|
||||
filteredChanged.Add(uri)
|
||||
}
|
||||
}
|
||||
change.Changed = filteredChanged
|
||||
}
|
||||
|
||||
// We can't filter created events because any created path could be a directory symlink
|
||||
// that includes relevant files. configFileRegistryBuilder will do check if these paths
|
||||
// are directories if they fall within a config's wildcard directories.
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
// isNodeModulesPath reports whether path is a node_modules directory itself or
|
||||
// lives inside one. Used to preserve node_modules watch deletions, whose package
|
||||
// files are read transiently and therefore never tracked in diskDirectories.
|
||||
func isNodeModulesPath(path tspath.Path) bool {
|
||||
s := string(path)
|
||||
return strings.HasSuffix(s, "/node_modules") || strings.Contains(s, "/node_modules/")
|
||||
}
|
||||
|
||||
// collectFilesRecursive recursively collects all cached file URIs under the
|
||||
// given directory path using the diskDirectories and diskFiles maps.
|
||||
func (s *snapshotFSBuilder) collectFilesRecursive(dirPath tspath.Path, files *collections.Set[lsproto.DocumentUri]) {
|
||||
dirEntry, ok := s.diskDirectories.Get(dirPath)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for childPath := range dirEntry.Value() {
|
||||
if entry, ok := s.diskFiles.Load(childPath); ok {
|
||||
if file := entry.Value(); file != nil {
|
||||
files.Add(lsconv.FileNameToDocumentURI(file.FileName()))
|
||||
}
|
||||
}
|
||||
s.collectFilesRecursive(childPath, files)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummary) FileChangeSummary {
|
||||
if change.Opened != "" && !tspath.IsDynamicFileName(change.Opened.FileName()) {
|
||||
path := s.toPath(change.Opened.FileName())
|
||||
if entry, ok := s.diskFiles.Load(path); !ok || entry.Original() == nil {
|
||||
change.Created.Add(change.Opened)
|
||||
} else if overlay, ok := s.overlays[path]; ok {
|
||||
// The file already exists in the program, but the overlay content from
|
||||
// didOpen may differ from what was originally read from disk (e.g. the
|
||||
// editor normalizes line endings, or the file changed on disk since the
|
||||
// project was loaded). Mark it as Changed so the project rebuilds.
|
||||
if diskFile := entry.Original(); diskFile != nil && overlay.Hash() != diskFile.Hash() {
|
||||
change.Changed.Add(change.Opened)
|
||||
}
|
||||
}
|
||||
}
|
||||
for uri := range change.Closed.Keys() {
|
||||
fileName := uri.FileName()
|
||||
if tspath.IsDynamicFileName(fileName) {
|
||||
continue
|
||||
}
|
||||
path := s.toPath(fileName)
|
||||
// We may have ignored watcher events while the file was open, so force a reload.
|
||||
if fh := s.getDiskFile(fileName, path, true /*forceReload*/); fh != nil {
|
||||
if fh.Hash() != s.prevOverlays[path].Hash() {
|
||||
change.Changed.Add(uri)
|
||||
}
|
||||
continue
|
||||
}
|
||||
change.Deleted.Add(uri)
|
||||
}
|
||||
return change
|
||||
}
|
||||
|
||||
// sourceFS is a vfs.FS that sources files from a FileSource and tracks seen files.
|
||||
type sourceFS struct {
|
||||
tracking bool
|
||||
toPath func(fileName string) tspath.Path
|
||||
missingDirectories *collections.SyncSet[tspath.Path]
|
||||
seenFiles *collections.SyncSet[tspath.Path]
|
||||
source FileSource
|
||||
}
|
||||
|
||||
func newSourceFS(tracking bool, source FileSource, toPath func(fileName string) tspath.Path) *sourceFS {
|
||||
fs := &sourceFS{
|
||||
tracking: tracking,
|
||||
toPath: toPath,
|
||||
source: source,
|
||||
}
|
||||
if tracking {
|
||||
fs.seenFiles = &collections.SyncSet[tspath.Path]{}
|
||||
fs.missingDirectories = &collections.SyncSet[tspath.Path]{}
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
var _ vfs.FS = (*sourceFS)(nil)
|
||||
|
||||
func (fs *sourceFS) DisableTracking() {
|
||||
fs.tracking = false
|
||||
}
|
||||
|
||||
func (fs *sourceFS) Track(fileName string) {
|
||||
if !fs.tracking {
|
||||
return
|
||||
}
|
||||
fs.seenFiles.Add(fs.toPath(fileName))
|
||||
}
|
||||
|
||||
func (fs *sourceFS) SeenFile(path tspath.Path) bool {
|
||||
if fs.seenFiles == nil {
|
||||
return false
|
||||
}
|
||||
return fs.seenFiles.Has(path)
|
||||
}
|
||||
|
||||
func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.Path) bool {
|
||||
if fs.seenFiles != nil && fs.seenFiles.Has(path) {
|
||||
return true
|
||||
}
|
||||
if fs.missingDirectories != nil && !fs.missingDirectories.IsEmpty() {
|
||||
for {
|
||||
if fs.missingDirectories.Has(path) {
|
||||
return true
|
||||
}
|
||||
|
||||
parent := path.GetDirectoryPath()
|
||||
if parent == path {
|
||||
break
|
||||
}
|
||||
path = parent
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (fs *sourceFS) GetFile(fileName string) FileHandle {
|
||||
fs.Track(fileName)
|
||||
return fs.source.GetFile(fileName)
|
||||
}
|
||||
|
||||
func (fs *sourceFS) GetFileByPath(fileName string, path tspath.Path) FileHandle {
|
||||
fs.Track(fileName)
|
||||
return fs.source.GetFileByPath(fileName, path)
|
||||
}
|
||||
|
||||
// DirectoryExists implements vfs.FS.
|
||||
func (fs *sourceFS) DirectoryExists(path string) bool {
|
||||
exists := fs.source.FS().DirectoryExists(path)
|
||||
if !exists && fs.tracking {
|
||||
fs.missingDirectories.Add(fs.toPath(path))
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
// FileExists implements vfs.FS.
|
||||
func (fs *sourceFS) FileExists(path string) bool {
|
||||
fs.Track(path)
|
||||
return fs.source.FileExists(path, fs.toPath(path))
|
||||
}
|
||||
|
||||
// GetAccessibleEntries implements vfs.FS.
|
||||
func (fs *sourceFS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
return fs.source.GetAccessibleEntries(path)
|
||||
}
|
||||
|
||||
// ReadFile implements vfs.FS.
|
||||
func (fs *sourceFS) ReadFile(path string) (contents string, ok bool) {
|
||||
if fh := fs.GetFile(path); fh != nil {
|
||||
return fh.Content(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Realpath implements vfs.FS.
|
||||
func (fs *sourceFS) Realpath(path string) string {
|
||||
return fs.source.FS().Realpath(path)
|
||||
}
|
||||
|
||||
// Stat implements vfs.FS.
|
||||
func (fs *sourceFS) Stat(path string) vfs.FileInfo {
|
||||
return fs.source.FS().Stat(path)
|
||||
}
|
||||
|
||||
// UseCaseSensitiveFileNames implements vfs.FS.
|
||||
func (fs *sourceFS) UseCaseSensitiveFileNames() bool {
|
||||
return fs.source.FS().UseCaseSensitiveFileNames()
|
||||
}
|
||||
|
||||
// WalkDir implements vfs.FS.
|
||||
func (fs *sourceFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
return fs.source.FS().WalkDir(root, walkFn)
|
||||
}
|
||||
|
||||
// WriteFile implements vfs.FS.
|
||||
func (fs *sourceFS) WriteFile(path string, data string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// AppendFile implements vfs.FS.
|
||||
func (fs *sourceFS) AppendFile(path string, data string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Remove implements vfs.FS.
|
||||
func (fs *sourceFS) Remove(path string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Chtimes implements vfs.FS.
|
||||
func (fs *sourceFS) Chtimes(path string, atime time.Time, mtime time.Time) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func readDirectoryIntoEntries[M ~map[tspath.Path]string](directories M, isFile func(tspath.Path) bool, entries *vfs.Entries) {
|
||||
for childPath, childName := range directories {
|
||||
if isFile(childPath) {
|
||||
entries.Files = append(entries.Files, childName)
|
||||
} else {
|
||||
entries.Directories = append(entries.Directories, childName)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user