2025-10-15 10:12:44 +03:00

374 lines
8.4 KiB
Go

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
}