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

691 lines
24 KiB
Go

package project
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"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/background"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/logging"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs"
)
type UpdateReason int
const (
UpdateReasonUnknown UpdateReason = iota
UpdateReasonDidOpenFile
UpdateReasonDidChangeCompilerOptionsForInferredProjects
UpdateReasonRequestedLanguageServicePendingChanges
UpdateReasonRequestedLanguageServiceProjectNotLoaded
UpdateReasonRequestedLanguageServiceProjectDirty
)
// SessionOptions are the immutable initialization options for a session.
// Snapshots may reference them as a pointer since they never change.
type SessionOptions struct {
CurrentDirectory string
DefaultLibraryPath string
TypingsLocation string
PositionEncoding lsproto.PositionEncodingKind
WatchEnabled bool
LoggingEnabled bool
DebounceDelay time.Duration
}
type SessionInit struct {
Options *SessionOptions
FS vfs.FS
Client Client
Logger logging.Logger
NpmExecutor ata.NpmExecutor
ParseCache *ParseCache
}
// Session manages the state of an LSP session. It receives textDocument
// events and requests for LanguageService objects from the LPS server
// and processes them into immutable snapshots as the data source for
// LanguageServices. When Session transitions from one snapshot to the
// next, it diffs them and updates file watchers and Automatic Type
// Acquisition (ATA) state accordingly.
type Session struct {
options *SessionOptions
toPath func(string) tspath.Path
client Client
logger logging.Logger
npmExecutor ata.NpmExecutor
fs *overlayFS
// parseCache is the ref-counted cache of source files used when
// creating programs during snapshot cloning.
parseCache *ParseCache
// extendedConfigCache is the ref-counted cache of tsconfig ASTs
// that are used in the "extends" of another tsconfig.
extendedConfigCache *extendedConfigCache
// programCounter counts how many snapshots reference a program.
// When a program is no longer referenced, its source files are
// released from the parseCache.
programCounter *programCounter
compilerOptionsForInferredProjects *core.CompilerOptions
typingsInstaller *ata.TypingsInstaller
backgroundQueue *background.Queue
// snapshotID is the counter for snapshot IDs. It does not necessarily
// equal the `snapshot.ID`. It is stored on Session instead of globally
// so IDs are predictable in tests.
snapshotID atomic.Uint64
// snapshot is the current immutable state of all projects.
snapshot *Snapshot
snapshotMu sync.RWMutex
// pendingFileChanges are accumulated from textDocument/* events delivered
// by the LSP server through DidOpenFile(), DidChangeFile(), etc. They are
// applied to the next snapshot update.
pendingFileChanges []FileChange
pendingFileChangesMu sync.Mutex
// pendingATAChanges are produced by Automatic Type Acquisition (ATA)
// installations and applied to the next snapshot update.
pendingATAChanges map[tspath.Path]*ATAStateChange
pendingATAChangesMu sync.Mutex
// diagnosticsRefreshCancel is the cancelation function for a scheduled
// diagnostics refresh. Diagnostics refreshes are scheduled and debounced
// after file watch changes and ATA updates.
diagnosticsRefreshCancel context.CancelFunc
diagnosticsRefreshMu sync.Mutex
// watches tracks the current watch globs and how many individual WatchedFiles
// are using each glob.
watches map[fileSystemWatcherKey]*fileSystemWatcherValue
watchesMu sync.Mutex
}
func NewSession(init *SessionInit) *Session {
currentDirectory := init.Options.CurrentDirectory
useCaseSensitiveFileNames := init.FS.UseCaseSensitiveFileNames()
toPath := func(fileName string) tspath.Path {
return tspath.ToPath(fileName, currentDirectory, useCaseSensitiveFileNames)
}
overlayFS := newOverlayFS(init.FS, make(map[tspath.Path]*overlay), init.Options.PositionEncoding, toPath)
parseCache := init.ParseCache
if parseCache == nil {
parseCache = &ParseCache{}
}
extendedConfigCache := &extendedConfigCache{}
session := &Session{
options: init.Options,
toPath: toPath,
client: init.Client,
logger: init.Logger,
npmExecutor: init.NpmExecutor,
fs: overlayFS,
parseCache: parseCache,
extendedConfigCache: extendedConfigCache,
programCounter: &programCounter{},
backgroundQueue: background.NewQueue(),
snapshotID: atomic.Uint64{},
snapshot: NewSnapshot(
uint64(0),
&snapshotFS{
toPath: toPath,
fs: init.FS,
},
init.Options,
parseCache,
extendedConfigCache,
&ConfigFileRegistry{},
nil,
toPath,
),
pendingATAChanges: make(map[tspath.Path]*ATAStateChange),
watches: make(map[fileSystemWatcherKey]*fileSystemWatcherValue),
}
if init.Options.TypingsLocation != "" && init.NpmExecutor != nil {
session.typingsInstaller = ata.NewTypingsInstaller(&ata.TypingsInstallerOptions{
TypingsLocation: init.Options.TypingsLocation,
ThrottleLimit: 5,
}, session)
}
return session
}
// FS implements module.ResolutionHost
func (s *Session) FS() vfs.FS {
return s.fs.fs
}
// GetCurrentDirectory implements module.ResolutionHost
func (s *Session) GetCurrentDirectory() string {
return s.options.CurrentDirectory
}
// Trace implements module.ResolutionHost
func (s *Session) Trace(msg string) {
panic("ATA module resolution should not use tracing")
}
func (s *Session) DidOpenFile(ctx context.Context, uri lsproto.DocumentUri, version int32, content string, languageKind lsproto.LanguageKind) {
s.cancelDiagnosticsRefresh()
s.pendingFileChangesMu.Lock()
s.pendingFileChanges = append(s.pendingFileChanges, FileChange{
Kind: FileChangeKindOpen,
URI: uri,
Version: version,
Content: content,
LanguageKind: languageKind,
})
changes, overlays := s.flushChangesLocked(ctx)
s.pendingFileChangesMu.Unlock()
s.UpdateSnapshot(ctx, overlays, SnapshotChange{
reason: UpdateReasonDidOpenFile,
fileChanges: changes,
requestedURIs: []lsproto.DocumentUri{uri},
})
}
func (s *Session) DidCloseFile(ctx context.Context, uri lsproto.DocumentUri) {
s.cancelDiagnosticsRefresh()
s.pendingFileChangesMu.Lock()
defer s.pendingFileChangesMu.Unlock()
s.pendingFileChanges = append(s.pendingFileChanges, FileChange{
Kind: FileChangeKindClose,
URI: uri,
Hash: s.fs.getFile(uri.FileName()).Hash(),
})
}
func (s *Session) DidChangeFile(ctx context.Context, uri lsproto.DocumentUri, version int32, changes []lsproto.TextDocumentContentChangePartialOrWholeDocument) {
s.cancelDiagnosticsRefresh()
s.pendingFileChangesMu.Lock()
defer s.pendingFileChangesMu.Unlock()
s.pendingFileChanges = append(s.pendingFileChanges, FileChange{
Kind: FileChangeKindChange,
URI: uri,
Version: version,
Changes: changes,
})
}
func (s *Session) DidSaveFile(ctx context.Context, uri lsproto.DocumentUri) {
s.cancelDiagnosticsRefresh()
s.pendingFileChangesMu.Lock()
defer s.pendingFileChangesMu.Unlock()
s.pendingFileChanges = append(s.pendingFileChanges, FileChange{
Kind: FileChangeKindSave,
URI: uri,
})
}
func (s *Session) DidChangeWatchedFiles(ctx context.Context, changes []*lsproto.FileEvent) {
fileChanges := make([]FileChange, 0, len(changes))
for _, change := range changes {
var kind FileChangeKind
switch change.Type {
case lsproto.FileChangeTypeCreated:
kind = FileChangeKindWatchCreate
case lsproto.FileChangeTypeChanged:
kind = FileChangeKindWatchChange
case lsproto.FileChangeTypeDeleted:
kind = FileChangeKindWatchDelete
default:
continue // Ignore unknown change types.
}
fileChanges = append(fileChanges, FileChange{
Kind: kind,
URI: change.Uri,
})
}
s.pendingFileChangesMu.Lock()
s.pendingFileChanges = append(s.pendingFileChanges, fileChanges...)
s.pendingFileChangesMu.Unlock()
// Schedule a debounced diagnostics refresh
s.ScheduleDiagnosticsRefresh()
}
func (s *Session) DidChangeCompilerOptionsForInferredProjects(ctx context.Context, options *core.CompilerOptions) {
s.compilerOptionsForInferredProjects = options
s.UpdateSnapshot(ctx, s.fs.Overlays(), SnapshotChange{
reason: UpdateReasonDidChangeCompilerOptionsForInferredProjects,
compilerOptionsForInferredProjects: options,
})
}
func (s *Session) ScheduleDiagnosticsRefresh() {
s.diagnosticsRefreshMu.Lock()
defer s.diagnosticsRefreshMu.Unlock()
// Cancel any existing scheduled diagnostics refresh
if s.diagnosticsRefreshCancel != nil {
s.diagnosticsRefreshCancel()
s.logger.Log("Delaying scheduled diagnostics refresh...")
} else {
s.logger.Log("Scheduling new diagnostics refresh...")
}
// Create a new cancellable context for the debounce task
debounceCtx, cancel := context.WithCancel(context.Background())
s.diagnosticsRefreshCancel = cancel
// Enqueue the debounced diagnostics refresh
s.backgroundQueue.Enqueue(debounceCtx, func(ctx context.Context) {
// Sleep for the debounce delay
select {
case <-time.After(s.options.DebounceDelay):
// Delay completed, proceed with refresh
case <-ctx.Done():
// Context was cancelled, newer events arrived
return
}
// Clear the cancel function since we're about to execute the refresh
s.diagnosticsRefreshMu.Lock()
s.diagnosticsRefreshCancel = nil
s.diagnosticsRefreshMu.Unlock()
if s.options.LoggingEnabled {
s.logger.Log("Running scheduled diagnostics refresh")
}
if err := s.client.RefreshDiagnostics(context.Background()); err != nil && s.options.LoggingEnabled {
s.logger.Logf("Error refreshing diagnostics: %v", err)
}
})
}
func (s *Session) cancelDiagnosticsRefresh() {
s.diagnosticsRefreshMu.Lock()
defer s.diagnosticsRefreshMu.Unlock()
if s.diagnosticsRefreshCancel != nil {
s.diagnosticsRefreshCancel()
s.logger.Log("Canceled scheduled diagnostics refresh")
s.diagnosticsRefreshCancel = nil
}
}
func (s *Session) Snapshot() (*Snapshot, func()) {
s.snapshotMu.RLock()
defer s.snapshotMu.RUnlock()
snapshot := s.snapshot
snapshot.Ref()
return snapshot, func() {
if snapshot.Deref() {
// The session itself accounts for one reference to the snapshot, and it derefs
// in UpdateSnapshot while holding the snapshotMu lock, so the only way to end
// up here is for an external caller to release the snapshot after the session
// has already dereferenced it and moved to a new snapshot. In other words, we
// can assume that `snapshot != s.snapshot`, and therefor there's no way for
// anyone else to acquire a reference to this snapshot again.
snapshot.dispose(s)
}
}
}
func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) {
var snapshot *Snapshot
fileChanges, overlays, ataChanges := s.flushChanges(ctx)
updateSnapshot := !fileChanges.IsEmpty() || len(ataChanges) > 0
if updateSnapshot {
// If there are pending file changes, we need to update the snapshot.
// Sending the requested URI ensures that the project for this URI is loaded.
snapshot = s.UpdateSnapshot(ctx, overlays, SnapshotChange{
reason: UpdateReasonRequestedLanguageServicePendingChanges,
fileChanges: fileChanges,
ataChanges: ataChanges,
requestedURIs: []lsproto.DocumentUri{uri},
})
} else {
// If there are no pending file changes, we can try to use the current snapshot.
s.snapshotMu.RLock()
snapshot = s.snapshot
s.snapshotMu.RUnlock()
}
project := snapshot.GetDefaultProject(uri)
if project == nil && !updateSnapshot || project != nil && project.dirty {
// The current snapshot does not have an up to date project for the URI,
// so we need to update the snapshot to ensure the project is loaded.
// !!! Allow multiple projects to update in parallel
snapshot = s.UpdateSnapshot(ctx, overlays, SnapshotChange{
reason: core.IfElse(project == nil, UpdateReasonRequestedLanguageServiceProjectNotLoaded, UpdateReasonRequestedLanguageServiceProjectDirty),
requestedURIs: []lsproto.DocumentUri{uri},
})
project = snapshot.GetDefaultProject(uri)
}
if project == nil {
return nil, fmt.Errorf("no project found for URI %s", uri)
}
return ls.NewLanguageService(project.GetProgram(), snapshot), nil
}
func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*overlay, change SnapshotChange) *Snapshot {
s.snapshotMu.Lock()
oldSnapshot := s.snapshot
newSnapshot := oldSnapshot.Clone(ctx, change, overlays, s)
s.snapshot = newSnapshot
s.snapshotMu.Unlock()
shouldDispose := newSnapshot != oldSnapshot && oldSnapshot.Deref()
if shouldDispose {
oldSnapshot.dispose(s)
}
// Enqueue ATA updates if needed
if s.typingsInstaller != nil {
s.triggerATAForUpdatedProjects(newSnapshot)
}
// Enqueue logging, watch updates, and diagnostic refresh tasks
s.backgroundQueue.Enqueue(context.Background(), func(ctx context.Context) {
if s.options.LoggingEnabled {
s.logger.Write(newSnapshot.builderLogs.String())
s.logProjectChanges(oldSnapshot, newSnapshot)
s.logger.Write("")
}
if s.options.WatchEnabled {
if err := s.updateWatches(oldSnapshot, newSnapshot); err != nil && s.options.LoggingEnabled {
s.logger.Log(err)
}
}
})
return newSnapshot
}
// WaitForBackgroundTasks waits for all background tasks to complete.
// This is intended to be used only for testing purposes.
func (s *Session) WaitForBackgroundTasks() {
s.backgroundQueue.Wait()
}
func updateWatch[T any](ctx context.Context, session *Session, logger logging.Logger, oldWatcher, newWatcher *WatchedFiles[T]) []error {
var errors []error
session.watchesMu.Lock()
defer session.watchesMu.Unlock()
if newWatcher != nil {
if id, watchers, ignored := newWatcher.Watchers(); len(watchers) > 0 {
var newWatchers collections.OrderedMap[WatcherID, *lsproto.FileSystemWatcher]
for i, watcher := range watchers {
key := toFileSystemWatcherKey(watcher)
value := session.watches[key]
globId := WatcherID(fmt.Sprintf("%s.%d", id, i))
if value == nil {
value = &fileSystemWatcherValue{id: globId}
session.watches[key] = value
}
value.count++
if value.count == 1 {
newWatchers.Set(globId, watcher)
}
}
for id, watcher := range newWatchers.Entries() {
if err := session.client.WatchFiles(ctx, id, []*lsproto.FileSystemWatcher{watcher}); err != nil {
errors = append(errors, err)
} else if logger != nil {
if oldWatcher == nil {
logger.Log(fmt.Sprintf("Added new watch: %s", id))
} else {
logger.Log(fmt.Sprintf("Updated watch: %s", id))
}
logger.Log("\t" + *watcher.GlobPattern.Pattern)
logger.Log("")
}
}
if len(ignored) > 0 {
logger.Logf("%d paths ineligible for watching", len(ignored))
if logger.IsVerbose() {
for path := range ignored {
logger.Log("\t" + path)
}
}
}
}
}
if oldWatcher != nil {
if _, watchers, _ := oldWatcher.Watchers(); len(watchers) > 0 {
var removedWatchers []WatcherID
for _, watcher := range watchers {
key := toFileSystemWatcherKey(watcher)
value := session.watches[key]
if value == nil {
continue
}
if value.count <= 1 {
delete(session.watches, key)
removedWatchers = append(removedWatchers, value.id)
} else {
value.count--
}
}
for _, id := range removedWatchers {
if err := session.client.UnwatchFiles(ctx, id); err != nil {
errors = append(errors, err)
} else if logger != nil && newWatcher == nil {
logger.Log(fmt.Sprintf("Removed watch: %s", id))
}
}
}
}
return errors
}
func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) error {
var errors []error
start := time.Now()
ctx := context.Background()
core.DiffMapsFunc(
oldSnapshot.ConfigFileRegistry.configs,
newSnapshot.ConfigFileRegistry.configs,
func(a, b *configFileEntry) bool {
return a.rootFilesWatch.ID() == b.rootFilesWatch.ID()
},
func(_ tspath.Path, addedEntry *configFileEntry) {
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedEntry.rootFilesWatch)...)
},
func(_ tspath.Path, removedEntry *configFileEntry) {
errors = append(errors, updateWatch(ctx, s, s.logger, removedEntry.rootFilesWatch, nil)...)
},
func(_ tspath.Path, oldEntry, newEntry *configFileEntry) {
errors = append(errors, updateWatch(ctx, s, s.logger, oldEntry.rootFilesWatch, newEntry.rootFilesWatch)...)
},
)
collections.DiffOrderedMaps(
oldSnapshot.ProjectCollection.ProjectsByPath(),
newSnapshot.ProjectCollection.ProjectsByPath(),
func(_ tspath.Path, addedProject *Project) {
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.programFilesWatch)...)
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.affectingLocationsWatch)...)
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.failedLookupsWatch)...)
errors = append(errors, updateWatch(ctx, s, s.logger, nil, addedProject.typingsWatch)...)
},
func(_ tspath.Path, removedProject *Project) {
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.programFilesWatch, nil)...)
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.affectingLocationsWatch, nil)...)
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.failedLookupsWatch, nil)...)
errors = append(errors, updateWatch(ctx, s, s.logger, removedProject.typingsWatch, nil)...)
},
func(_ tspath.Path, oldProject, newProject *Project) {
if oldProject.programFilesWatch.ID() != newProject.programFilesWatch.ID() {
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.programFilesWatch, newProject.programFilesWatch)...)
}
if oldProject.affectingLocationsWatch.ID() != newProject.affectingLocationsWatch.ID() {
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.affectingLocationsWatch, newProject.affectingLocationsWatch)...)
}
if oldProject.failedLookupsWatch.ID() != newProject.failedLookupsWatch.ID() {
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.failedLookupsWatch, newProject.failedLookupsWatch)...)
}
if oldProject.typingsWatch.ID() != newProject.typingsWatch.ID() {
errors = append(errors, updateWatch(ctx, s, s.logger, oldProject.typingsWatch, newProject.typingsWatch)...)
}
},
)
if len(errors) > 0 {
return fmt.Errorf("errors updating watches: %v", errors)
} else if s.options.LoggingEnabled {
s.logger.Log(fmt.Sprintf("Updated watches in %v", time.Since(start)))
}
return nil
}
func (s *Session) Close() {
// Cancel any pending diagnostics refresh
s.cancelDiagnosticsRefresh()
s.backgroundQueue.Close()
}
func (s *Session) flushChanges(ctx context.Context) (FileChangeSummary, map[tspath.Path]*overlay, map[tspath.Path]*ATAStateChange) {
s.pendingFileChangesMu.Lock()
defer s.pendingFileChangesMu.Unlock()
s.pendingATAChangesMu.Lock()
defer s.pendingATAChangesMu.Unlock()
pendingATAChanges := s.pendingATAChanges
s.pendingATAChanges = make(map[tspath.Path]*ATAStateChange)
fileChanges, overlays := s.flushChangesLocked(ctx)
return fileChanges, overlays, pendingATAChanges
}
// flushChangesLocked should only be called with s.pendingFileChangesMu held.
func (s *Session) flushChangesLocked(ctx context.Context) (FileChangeSummary, map[tspath.Path]*overlay) {
if len(s.pendingFileChanges) == 0 {
return FileChangeSummary{}, s.fs.Overlays()
}
start := time.Now()
changes, overlays := s.fs.processChanges(s.pendingFileChanges)
if s.options.LoggingEnabled {
s.logger.Log(fmt.Sprintf("Processed %d file changes in %v", len(s.pendingFileChanges), time.Since(start)))
}
s.pendingFileChanges = nil
return changes, overlays
}
// logProjectChanges logs information about projects that have changed between snapshots
func (s *Session) logProjectChanges(oldSnapshot *Snapshot, newSnapshot *Snapshot) {
var loggedProjectChanges bool
logProject := func(project *Project) {
var builder strings.Builder
project.print(s.logger.IsVerbose() /*writeFileNames*/, s.logger.IsVerbose() /*writeFileExplanation*/, &builder)
s.logger.Log(builder.String())
loggedProjectChanges = true
}
collections.DiffOrderedMaps(
oldSnapshot.ProjectCollection.ProjectsByPath(),
newSnapshot.ProjectCollection.ProjectsByPath(),
func(path tspath.Path, addedProject *Project) {
// New project added
logProject(addedProject)
},
func(path tspath.Path, removedProject *Project) {
// Project removed
s.logger.Logf("\nProject '%s' removed\n%s", removedProject.Name(), hr)
},
func(path tspath.Path, oldProject, newProject *Project) {
// Project updated
if newProject.ProgramUpdateKind == ProgramUpdateKindNewFiles {
logProject(newProject)
}
},
)
if loggedProjectChanges || s.logger.IsVerbose() {
s.logCacheStats(newSnapshot)
}
}
func (s *Session) logCacheStats(snapshot *Snapshot) {
var parseCacheSize int
var programCount int
var extendedConfigCount int
if s.logger.IsVerbose() {
s.parseCache.entries.Range(func(_ parseCacheKey, _ *parseCacheEntry) bool {
parseCacheSize++
return true
})
s.programCounter.refs.Range(func(_ *compiler.Program, _ *atomic.Int32) bool {
programCount++
return true
})
s.extendedConfigCache.entries.Range(func(_ tspath.Path, _ *extendedConfigCacheEntry) bool {
extendedConfigCount++
return true
})
}
s.logger.Write("\n======== Cache Statistics ========")
s.logger.Logf("Open file count: %6d", len(snapshot.fs.overlays))
s.logger.Logf("Cached disk files: %6d", len(snapshot.fs.diskFiles))
s.logger.Logf("Project count: %6d", len(snapshot.ProjectCollection.Projects()))
s.logger.Logf("Config count: %6d", len(snapshot.ConfigFileRegistry.configs))
if s.logger.IsVerbose() {
s.logger.Logf("Parse cache size: %6d", parseCacheSize)
s.logger.Logf("Program count: %6d", programCount)
s.logger.Logf("Extended config cache size: %6d", extendedConfigCount)
}
}
func (s *Session) NpmInstall(cwd string, npmInstallArgs []string) ([]byte, error) {
return s.npmExecutor.NpmInstall(cwd, npmInstallArgs)
}
func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) {
for _, project := range newSnapshot.ProjectCollection.Projects() {
if project.ShouldTriggerATA(newSnapshot.ID()) {
s.backgroundQueue.Enqueue(context.Background(), func(ctx context.Context) {
var logTree *logging.LogTree
if s.options.LoggingEnabled {
logTree = logging.NewLogTree("Triggering ATA for project " + project.Name())
}
typingsInfo := project.ComputeTypingsInfo()
request := &ata.TypingsInstallRequest{
ProjectID: project.configFilePath,
TypingsInfo: &typingsInfo,
FileNames: core.Map(project.Program.GetSourceFiles(), func(file *ast.SourceFile) string { return file.FileName() }),
ProjectRootPath: project.currentDirectory,
CompilerOptions: project.CommandLine.CompilerOptions(),
CurrentDirectory: s.options.CurrentDirectory,
GetScriptKind: core.GetScriptKindFromFileName,
FS: s.fs.fs,
Logger: logTree,
}
if result, err := s.typingsInstaller.InstallTypings(request); err != nil && logTree != nil {
s.logger.Log(fmt.Sprintf("ATA installation failed for project %s: %v", project.Name(), err))
s.logger.Log(logTree.String())
} else {
if !slices.Equal(result.TypingsFiles, project.typingsFiles) {
s.pendingATAChangesMu.Lock()
defer s.pendingATAChangesMu.Unlock()
s.pendingATAChanges[project.configFilePath] = &ATAStateChange{
TypingsInfo: &typingsInfo,
TypingsFiles: result.TypingsFiles,
TypingsFilesToWatch: result.FilesToWatch,
Logs: logTree,
}
s.ScheduleDiagnosticsRefresh()
}
}
})
}
}
}