803 lines
24 KiB
Go
803 lines
24 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|