374 lines
8.4 KiB
Go
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
|
|
}
|