325 lines
11 KiB
Go
325 lines
11 KiB
Go
package project
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
|
|
"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/project/ata"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/dirty"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/logging"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
)
|
|
|
|
type Snapshot struct {
|
|
id uint64
|
|
parentId uint64
|
|
refCount atomic.Int32
|
|
|
|
// Session options are immutable for the server lifetime,
|
|
// so can be a pointer.
|
|
sessionOptions *SessionOptions
|
|
toPath func(fileName string) tspath.Path
|
|
converters *ls.Converters
|
|
|
|
// Immutable state, cloned between snapshots
|
|
fs *snapshotFS
|
|
ProjectCollection *ProjectCollection
|
|
ConfigFileRegistry *ConfigFileRegistry
|
|
compilerOptionsForInferredProjects *core.CompilerOptions
|
|
|
|
builderLogs *logging.LogTree
|
|
apiError error
|
|
}
|
|
|
|
// NewSnapshot
|
|
func NewSnapshot(
|
|
id uint64,
|
|
fs *snapshotFS,
|
|
sessionOptions *SessionOptions,
|
|
parseCache *ParseCache,
|
|
extendedConfigCache *extendedConfigCache,
|
|
configFileRegistry *ConfigFileRegistry,
|
|
compilerOptionsForInferredProjects *core.CompilerOptions,
|
|
toPath func(fileName string) tspath.Path,
|
|
) *Snapshot {
|
|
s := &Snapshot{
|
|
id: id,
|
|
|
|
sessionOptions: sessionOptions,
|
|
toPath: toPath,
|
|
|
|
fs: fs,
|
|
ConfigFileRegistry: configFileRegistry,
|
|
ProjectCollection: &ProjectCollection{toPath: toPath},
|
|
compilerOptionsForInferredProjects: compilerOptionsForInferredProjects,
|
|
}
|
|
s.converters = ls.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap)
|
|
s.refCount.Store(1)
|
|
return s
|
|
}
|
|
|
|
func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project {
|
|
fileName := uri.FileName()
|
|
path := s.toPath(fileName)
|
|
return s.ProjectCollection.GetDefaultProject(fileName, path)
|
|
}
|
|
|
|
func (s *Snapshot) GetFile(fileName string) FileHandle {
|
|
return s.fs.GetFile(fileName)
|
|
}
|
|
|
|
func (s *Snapshot) LSPLineMap(fileName string) *ls.LSPLineMap {
|
|
if file := s.fs.GetFile(fileName); file != nil {
|
|
return file.LSPLineMap()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Snapshot) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo {
|
|
if file := s.fs.GetFile(fileName); file != nil {
|
|
return file.ECMALineInfo()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Snapshot) Converters() *ls.Converters {
|
|
return s.converters
|
|
}
|
|
|
|
func (s *Snapshot) ID() uint64 {
|
|
return s.id
|
|
}
|
|
|
|
func (s *Snapshot) UseCaseSensitiveFileNames() bool {
|
|
return s.fs.fs.UseCaseSensitiveFileNames()
|
|
}
|
|
|
|
func (s *Snapshot) ReadFile(fileName string) (string, bool) {
|
|
handle := s.GetFile(fileName)
|
|
if handle == nil {
|
|
return "", false
|
|
}
|
|
return handle.Content(), true
|
|
}
|
|
|
|
type APISnapshotRequest struct {
|
|
OpenProjects *collections.Set[string]
|
|
CloseProjects *collections.Set[tspath.Path]
|
|
UpdateProjects *collections.Set[tspath.Path]
|
|
}
|
|
|
|
type SnapshotChange struct {
|
|
reason UpdateReason
|
|
// fileChanges are the changes that have occurred since the last snapshot.
|
|
fileChanges FileChangeSummary
|
|
// requestedURIs are URIs that were requested by the client.
|
|
// The new snapshot should ensure projects for these URIs have loaded programs.
|
|
requestedURIs []lsproto.DocumentUri
|
|
// compilerOptionsForInferredProjects is the compiler options to use for inferred projects.
|
|
// It should only be set the value in the next snapshot should be changed. If nil, the
|
|
// value from the previous snapshot will be copied to the new snapshot.
|
|
compilerOptionsForInferredProjects *core.CompilerOptions
|
|
// ataChanges contains ATA-related changes to apply to projects in the new snapshot.
|
|
ataChanges map[tspath.Path]*ATAStateChange
|
|
apiRequest *APISnapshotRequest
|
|
}
|
|
|
|
// ATAStateChange represents a change to a project's ATA state.
|
|
type ATAStateChange struct {
|
|
ProjectID tspath.Path
|
|
// TypingsInfo is the new typings info for the project.
|
|
TypingsInfo *ata.TypingsInfo
|
|
// TypingsFiles is the new list of typing files for the project.
|
|
TypingsFiles []string
|
|
// TypingsFilesToWatch is the new list of typing files to watch for changes.
|
|
TypingsFilesToWatch []string
|
|
Logs *logging.LogTree
|
|
}
|
|
|
|
func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays map[tspath.Path]*overlay, session *Session) *Snapshot {
|
|
var logger *logging.LogTree
|
|
|
|
// Print in-progress logs immediately if cloning fails
|
|
if session.options.LoggingEnabled {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
session.logger.Write(logger.String())
|
|
panic(r)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if session.options.LoggingEnabled {
|
|
logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d", s.id))
|
|
switch change.reason {
|
|
case UpdateReasonDidOpenFile:
|
|
logger.Logf("Reason: DidOpenFile - %s", change.fileChanges.Opened)
|
|
case UpdateReasonDidChangeCompilerOptionsForInferredProjects:
|
|
logger.Logf("Reason: DidChangeCompilerOptionsForInferredProjects")
|
|
case UpdateReasonRequestedLanguageServicePendingChanges:
|
|
logger.Logf("Reason: RequestedLanguageService (pending file changes) - %v", change.requestedURIs)
|
|
case UpdateReasonRequestedLanguageServiceProjectNotLoaded:
|
|
logger.Logf("Reason: RequestedLanguageService (project not loaded) - %v", change.requestedURIs)
|
|
case UpdateReasonRequestedLanguageServiceProjectDirty:
|
|
logger.Logf("Reason: RequestedLanguageService (project dirty) - %v", change.requestedURIs)
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
fs := newSnapshotFSBuilder(session.fs.fs, overlays, s.fs.diskFiles, session.options.PositionEncoding, s.toPath)
|
|
fs.markDirtyFiles(change.fileChanges)
|
|
|
|
compilerOptionsForInferredProjects := s.compilerOptionsForInferredProjects
|
|
if change.compilerOptionsForInferredProjects != nil {
|
|
// !!! mark inferred projects as dirty?
|
|
compilerOptionsForInferredProjects = change.compilerOptionsForInferredProjects
|
|
}
|
|
|
|
newSnapshotID := session.snapshotID.Add(1)
|
|
projectCollectionBuilder := newProjectCollectionBuilder(
|
|
ctx,
|
|
newSnapshotID,
|
|
fs,
|
|
s.ProjectCollection,
|
|
s.ConfigFileRegistry,
|
|
s.ProjectCollection.apiOpenedProjects,
|
|
compilerOptionsForInferredProjects,
|
|
s.sessionOptions,
|
|
session.parseCache,
|
|
session.extendedConfigCache,
|
|
)
|
|
|
|
var apiError error
|
|
if change.apiRequest != nil {
|
|
apiError = projectCollectionBuilder.HandleAPIRequest(change.apiRequest, logger.Fork("HandleAPIRequest"))
|
|
}
|
|
|
|
if len(change.ataChanges) != 0 {
|
|
projectCollectionBuilder.DidUpdateATAState(change.ataChanges, logger.Fork("DidUpdateATAState"))
|
|
}
|
|
|
|
if !change.fileChanges.IsEmpty() {
|
|
projectCollectionBuilder.DidChangeFiles(change.fileChanges, logger.Fork("DidChangeFiles"))
|
|
}
|
|
|
|
for _, uri := range change.requestedURIs {
|
|
projectCollectionBuilder.DidRequestFile(uri, logger.Fork("DidRequestFile"))
|
|
}
|
|
|
|
projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger)
|
|
|
|
// Clean cached disk files not touched by any open project. It's not important that we do this on
|
|
// file open specifically, but we don't need to do it on every snapshot clone.
|
|
if len(change.fileChanges.Opened) != 0 {
|
|
var changedFiles bool
|
|
for _, project := range projectCollection.Projects() {
|
|
if project.ProgramLastUpdate == newSnapshotID && project.ProgramUpdateKind != ProgramUpdateKindCloned {
|
|
changedFiles = true
|
|
break
|
|
}
|
|
}
|
|
// The set of seen files can change only if a program was constructed (not cloned) during this snapshot.
|
|
if changedFiles {
|
|
cleanFilesStart := time.Now()
|
|
removedFiles := 0
|
|
fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool {
|
|
for _, project := range projectCollection.Projects() {
|
|
if project.host.seenFiles.Has(entry.Key()) {
|
|
return true
|
|
}
|
|
}
|
|
entry.Delete()
|
|
removedFiles++
|
|
return true
|
|
})
|
|
if session.options.LoggingEnabled {
|
|
logger.Logf("Removed %d cached files in %v", removedFiles, time.Since(cleanFilesStart))
|
|
}
|
|
}
|
|
}
|
|
|
|
snapshotFS, _ := fs.Finalize()
|
|
newSnapshot := NewSnapshot(
|
|
newSnapshotID,
|
|
snapshotFS,
|
|
s.sessionOptions,
|
|
session.parseCache,
|
|
session.extendedConfigCache,
|
|
nil,
|
|
compilerOptionsForInferredProjects,
|
|
s.toPath,
|
|
)
|
|
newSnapshot.parentId = s.id
|
|
newSnapshot.ProjectCollection = projectCollection
|
|
newSnapshot.ConfigFileRegistry = configFileRegistry
|
|
newSnapshot.builderLogs = logger
|
|
newSnapshot.apiError = apiError
|
|
|
|
for _, project := range newSnapshot.ProjectCollection.Projects() {
|
|
session.programCounter.Ref(project.Program)
|
|
if project.ProgramLastUpdate == newSnapshotID {
|
|
// If the program was updated during this clone, the project and its host are new
|
|
// and still retain references to the builder. Freezing clears the builder reference
|
|
// so it's GC'd and to ensure the project can't access any data not already in the
|
|
// snapshot during use. This is pretty kludgy, but it's an artifact of Program design:
|
|
// Program has a single host, which is expected to implement a full vfs.FS, among
|
|
// other things. That host is *mostly* only used during program *construction*, but a
|
|
// few methods may get exercised during program *use*. So, our compiler host is allowed
|
|
// to access caches and perform mutating effects (like acquire referenced project
|
|
// config files) during snapshot building, and then we call `freeze` to ensure those
|
|
// mutations don't happen afterwards. In the future, we might improve things by
|
|
// separating what it takes to build a program from what it takes to use a program,
|
|
// and only pass the former into NewProgram instead of retaining it indefinitely.
|
|
project.host.freeze(snapshotFS, newSnapshot.ConfigFileRegistry)
|
|
}
|
|
}
|
|
for path, config := range newSnapshot.ConfigFileRegistry.configs {
|
|
if config.commandLine != nil && config.commandLine.ConfigFile != nil {
|
|
if prevConfig, ok := s.ConfigFileRegistry.configs[path]; ok {
|
|
if prevConfig.commandLine != nil && config.commandLine.ConfigFile == prevConfig.commandLine.ConfigFile {
|
|
for _, file := range prevConfig.commandLine.ExtendedSourceFiles() {
|
|
// Ref count extended configs that were already loaded in the previous snapshot.
|
|
// New/changed ones were handled during config file registry building.
|
|
session.extendedConfigCache.Ref(s.toPath(file))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start))
|
|
return newSnapshot
|
|
}
|
|
|
|
func (s *Snapshot) Ref() {
|
|
s.refCount.Add(1)
|
|
}
|
|
|
|
func (s *Snapshot) Deref() bool {
|
|
return s.refCount.Add(-1) == 0
|
|
}
|
|
|
|
func (s *Snapshot) dispose(session *Session) {
|
|
for _, project := range s.ProjectCollection.Projects() {
|
|
if project.Program != nil && session.programCounter.Deref(project.Program) {
|
|
for _, file := range project.Program.SourceFiles() {
|
|
session.parseCache.Deref(file)
|
|
}
|
|
}
|
|
}
|
|
for _, config := range s.ConfigFileRegistry.configs {
|
|
if config.commandLine != nil {
|
|
for _, file := range config.commandLine.ExtendedSourceFiles() {
|
|
session.extendedConfigCache.Deref(session.toPath(file))
|
|
}
|
|
}
|
|
}
|
|
}
|