package project import ( "maps" "sync" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ls" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" "github.com/zeebo/xxh3" ) type FileContent interface { Content() string Hash() xxh3.Uint128 } type FileHandle interface { FileContent FileName() string Version() int32 MatchesDiskText() bool IsOverlay() bool LSPLineMap() *ls.LSPLineMap ECMALineInfo() *sourcemap.ECMALineInfo Kind() core.ScriptKind } type fileBase struct { fileName string content string hash xxh3.Uint128 lineMapOnce sync.Once lineMap *ls.LSPLineMap lineInfoOnce sync.Once lineInfo *sourcemap.ECMALineInfo } func (f *fileBase) FileName() string { return f.fileName } func (f *fileBase) Hash() xxh3.Uint128 { return f.hash } func (f *fileBase) Content() string { return f.content } func (f *fileBase) LSPLineMap() *ls.LSPLineMap { f.lineMapOnce.Do(func() { f.lineMap = ls.ComputeLSPLineStarts(f.content) }) return f.lineMap } func (f *fileBase) ECMALineInfo() *sourcemap.ECMALineInfo { f.lineInfoOnce.Do(func() { lineStarts := core.ComputeECMALineStarts(f.content) f.lineInfo = sourcemap.CreateECMALineInfo(f.content, lineStarts) }) return f.lineInfo } type diskFile struct { fileBase needsReload bool } func newDiskFile(fileName string, content string) *diskFile { return &diskFile{ fileBase: fileBase{ fileName: fileName, content: content, hash: xxh3.Hash128([]byte(content)), }, } } var _ FileHandle = (*diskFile)(nil) func (f *diskFile) Version() int32 { return 0 } func (f *diskFile) MatchesDiskText() bool { return !f.needsReload } func (f *diskFile) IsOverlay() bool { return false } func (f *diskFile) Kind() core.ScriptKind { return core.GetScriptKindFromFileName(f.fileName) } func (f *diskFile) Clone() *diskFile { return &diskFile{ fileBase: fileBase{ fileName: f.fileName, content: f.content, hash: f.hash, }, } } var _ FileHandle = (*overlay)(nil) type overlay struct { fileBase version int32 kind core.ScriptKind matchesDiskText bool } func newOverlay(fileName string, content string, version int32, kind core.ScriptKind) *overlay { return &overlay{ fileBase: fileBase{ fileName: fileName, content: content, hash: xxh3.Hash128([]byte(content)), }, version: version, kind: kind, } } func (o *overlay) Version() int32 { return o.version } func (o *overlay) Text() string { return o.content } // MatchesDiskText may return false negatives, but never false positives. func (o *overlay) MatchesDiskText() bool { return o.matchesDiskText } // !!! optimization: incorporate mtime func (o *overlay) computeMatchesDiskText(fs vfs.FS) bool { if isDynamicFileName(o.fileName) { return false } diskContent, ok := fs.ReadFile(o.fileName) if !ok { return false } return xxh3.Hash128([]byte(diskContent)) == o.hash } func (o *overlay) IsOverlay() bool { return true } func (o *overlay) Kind() core.ScriptKind { return o.kind } type overlayFS struct { toPath func(string) tspath.Path fs vfs.FS positionEncoding lsproto.PositionEncodingKind mu sync.RWMutex overlays map[tspath.Path]*overlay } func newOverlayFS(fs vfs.FS, overlays map[tspath.Path]*overlay, positionEncoding lsproto.PositionEncodingKind, toPath func(string) tspath.Path) *overlayFS { return &overlayFS{ fs: fs, positionEncoding: positionEncoding, overlays: overlays, toPath: toPath, } } func (fs *overlayFS) Overlays() map[tspath.Path]*overlay { fs.mu.RLock() defer fs.mu.RUnlock() return fs.overlays } func (fs *overlayFS) getFile(fileName string) FileHandle { fs.mu.RLock() overlays := fs.overlays fs.mu.RUnlock() path := fs.toPath(fileName) if overlay, ok := overlays[path]; ok { return overlay } content, ok := fs.fs.ReadFile(fileName) if !ok { return nil } return newDiskFile(fileName, content) } func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, map[tspath.Path]*overlay) { fs.mu.Lock() defer fs.mu.Unlock() var result FileChangeSummary newOverlays := maps.Clone(fs.overlays) // Reduced collection of changes that occurred on a single file type fileEvents struct { openChange *FileChange closeChange *FileChange watchChanged bool changes []*FileChange saved bool created bool deleted bool } fileEventMap := make(map[lsproto.DocumentUri]*fileEvents) for _, change := range changes { uri := change.URI events, exists := fileEventMap[uri] if exists { if events.openChange != nil { panic("should see no changes after open") } } else { events = &fileEvents{} fileEventMap[uri] = events } switch change.Kind { case FileChangeKindOpen: events.openChange = &change events.closeChange = nil events.watchChanged = false events.changes = nil events.saved = false events.created = false events.deleted = false case FileChangeKindClose: events.closeChange = &change events.changes = nil events.saved = false events.watchChanged = false case FileChangeKindChange: if events.closeChange != nil { panic("should see no changes after close") } events.changes = append(events.changes, &change) events.saved = false events.watchChanged = false case FileChangeKindSave: events.saved = true case FileChangeKindWatchCreate: if events.deleted { // Delete followed by create becomes a change events.deleted = false events.watchChanged = true } else { events.created = true } case FileChangeKindWatchChange: if !events.created { events.watchChanged = true events.saved = false } case FileChangeKindWatchDelete: events.watchChanged = false events.saved = false // Delete after create cancels out if events.created { events.created = false } else { events.deleted = true } } } // Process deduplicated events per file for uri, events := range fileEventMap { path := uri.Path(fs.fs.UseCaseSensitiveFileNames()) o := newOverlays[path] if events.openChange != nil { if result.Opened != "" { panic("can only process one file open event at a time") } result.Opened = uri newOverlays[path] = newOverlay( uri.FileName(), events.openChange.Content, events.openChange.Version, ls.LanguageKindToScriptKind(events.openChange.LanguageKind), ) continue } if events.closeChange != nil { if result.Closed == nil { result.Closed = make(map[lsproto.DocumentUri]xxh3.Uint128) } result.Closed[uri] = events.closeChange.Hash delete(newOverlays, path) } if events.watchChanged { if o == nil { result.Changed.Add(uri) } else if o != nil && !events.saved { if matchesDiskText := o.computeMatchesDiskText(fs.fs); matchesDiskText != o.MatchesDiskText() { o = newOverlay(o.FileName(), o.Content(), o.Version(), o.kind) o.matchesDiskText = matchesDiskText newOverlays[path] = o } } } if len(events.changes) > 0 { result.Changed.Add(uri) if o == nil { panic("overlay not found for changed file: " + uri) } for _, change := range events.changes { converters := ls.NewConverters(fs.positionEncoding, func(fileName string) *ls.LSPLineMap { return o.LSPLineMap() }) for _, textChange := range change.Changes { if partialChange := textChange.Partial; partialChange != nil { newContent := converters.FromLSPTextChange(o, partialChange).ApplyTo(o.content) o = newOverlay(o.fileName, newContent, change.Version, o.kind) } else if wholeChange := textChange.WholeDocument; wholeChange != nil { o = newOverlay(o.fileName, wholeChange.Text, change.Version, o.kind) } } if len(change.Changes) > 0 { o.version = change.Version o.hash = xxh3.Hash128([]byte(o.content)) o.matchesDiskText = false newOverlays[path] = o } } } if events.saved { if o == nil { panic("overlay not found for saved file: " + uri) } o = newOverlay(o.FileName(), o.Content(), o.Version(), o.kind) o.matchesDiskText = true newOverlays[path] = o } if events.created && o == nil { result.Created.Add(uri) } if events.deleted && o == nil { result.Deleted.Add(uri) } } fs.overlays = newOverlays return result, newOverlays }