package project import ( "context" "fmt" "maps" "slices" "time" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" "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/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" ) type projectLoadKind int const ( // Project is not created or updated, only looked up in cache projectLoadKindFind projectLoadKind = iota // Project is created and then its graph is updated projectLoadKindCreate ) type projectCollectionBuilder struct { sessionOptions *SessionOptions parseCache *ParseCache extendedConfigCache *extendedConfigCache ctx context.Context fs *snapshotFSBuilder base *ProjectCollection compilerOptionsForInferredProjects *core.CompilerOptions configFileRegistryBuilder *configFileRegistryBuilder newSnapshotID uint64 programStructureChanged bool fileDefaultProjects map[tspath.Path]tspath.Path configuredProjects *dirty.SyncMap[tspath.Path, *Project] inferredProject *dirty.Box[*Project] apiOpenedProjects map[tspath.Path]struct{} } func newProjectCollectionBuilder( ctx context.Context, newSnapshotID uint64, fs *snapshotFSBuilder, oldProjectCollection *ProjectCollection, oldConfigFileRegistry *ConfigFileRegistry, oldAPIOpenedProjects map[tspath.Path]struct{}, compilerOptionsForInferredProjects *core.CompilerOptions, sessionOptions *SessionOptions, parseCache *ParseCache, extendedConfigCache *extendedConfigCache, ) *projectCollectionBuilder { return &projectCollectionBuilder{ ctx: ctx, fs: fs, compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, sessionOptions: sessionOptions, parseCache: parseCache, extendedConfigCache: extendedConfigCache, base: oldProjectCollection, configFileRegistryBuilder: newConfigFileRegistryBuilder(fs, oldConfigFileRegistry, extendedConfigCache, sessionOptions, nil), newSnapshotID: newSnapshotID, configuredProjects: dirty.NewSyncMap(oldProjectCollection.configuredProjects, nil), inferredProject: dirty.NewBox(oldProjectCollection.inferredProject), apiOpenedProjects: maps.Clone(oldAPIOpenedProjects), } } func (b *projectCollectionBuilder) Finalize(logger *logging.LogTree) (*ProjectCollection, *ConfigFileRegistry) { var changed bool newProjectCollection := b.base ensureCloned := func() { if !changed { newProjectCollection = newProjectCollection.clone() changed = true } } if configuredProjects, configuredProjectsChanged := b.configuredProjects.Finalize(); configuredProjectsChanged { ensureCloned() newProjectCollection.configuredProjects = configuredProjects } if !changed && !maps.Equal(b.fileDefaultProjects, b.base.fileDefaultProjects) { ensureCloned() newProjectCollection.fileDefaultProjects = b.fileDefaultProjects } if newInferredProject, inferredProjectChanged := b.inferredProject.Finalize(); inferredProjectChanged { ensureCloned() newProjectCollection.inferredProject = newInferredProject } configFileRegistry := b.configFileRegistryBuilder.Finalize() newProjectCollection.configFileRegistry = configFileRegistry return newProjectCollection, configFileRegistry } func (b *projectCollectionBuilder) forEachProject(fn func(entry dirty.Value[*Project]) bool) { keepGoing := true b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { keepGoing = fn(entry) return keepGoing }) if !keepGoing { return } if b.inferredProject.Value() != nil { fn(b.inferredProject) } } func (b *projectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotRequest, logger *logging.LogTree) error { var projectsToClose map[tspath.Path]struct{} if apiRequest.CloseProjects != nil { projectsToClose = maps.Clone(apiRequest.CloseProjects.M) for projectPath := range apiRequest.CloseProjects.Keys() { delete(b.apiOpenedProjects, projectPath) } } if apiRequest.OpenProjects != nil { for configFileName := range apiRequest.OpenProjects.Keys() { configPath := b.toPath(configFileName) if entry := b.findOrCreateProject(configFileName, configPath, projectLoadKindCreate, logger); entry != nil { if b.apiOpenedProjects == nil { b.apiOpenedProjects = make(map[tspath.Path]struct{}) } b.apiOpenedProjects[configPath] = struct{}{} b.updateProgram(entry, logger) } else { return fmt.Errorf("project not found for open: %s", configFileName) } } } if apiRequest.UpdateProjects != nil { for configPath := range apiRequest.UpdateProjects.Keys() { if entry, ok := b.configuredProjects.Load(configPath); ok { b.updateProgram(entry, logger) } else { return fmt.Errorf("project not found for update: %s", configPath) } } } for _, overlay := range b.fs.overlays { if entry := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); entry != nil { delete(projectsToClose, entry.Value().configFilePath) } } for projectPath := range projectsToClose { if entry, ok := b.configuredProjects.Load(projectPath); ok { b.deleteConfiguredProject(entry, logger) } } return nil } func (b *projectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, logger *logging.LogTree) { changedFiles := make([]tspath.Path, 0, len(summary.Closed)+summary.Changed.Len()) for uri, hash := range summary.Closed { fileName := uri.FileName() path := b.toPath(fileName) if fh := b.fs.GetFileByPath(fileName, path); fh == nil || fh.Hash() != hash { changedFiles = append(changedFiles, path) } } for uri := range summary.Changed.Keys() { fileName := uri.FileName() path := b.toPath(fileName) changedFiles = append(changedFiles, path) } configChangeLogger := logger.Fork("Checking for changes affecting config files") configChangeResult := b.configFileRegistryBuilder.DidChangeFiles(summary, configChangeLogger) logChangeFileResult(configChangeResult, configChangeLogger) b.forEachProject(func(entry dirty.Value[*Project]) bool { // Handle closed and changed files b.markFilesChanged(entry, changedFiles, lsproto.FileChangeTypeChanged, logger) if entry.Value().Kind == KindInferred && len(summary.Closed) > 0 { rootFilesMap := entry.Value().CommandLine.FileNamesByPath() newRootFiles := entry.Value().CommandLine.FileNames() for uri := range summary.Closed { fileName := uri.FileName() path := b.toPath(fileName) if _, ok := rootFilesMap[path]; ok { newRootFiles = slices.Delete(newRootFiles, slices.Index(newRootFiles, fileName), slices.Index(newRootFiles, fileName)+1) } } b.updateInferredProjectRoots(newRootFiles, logger) } // Handle deleted files if summary.Deleted.Len() > 0 { deletedPaths := make([]tspath.Path, 0, summary.Deleted.Len()) for uri := range summary.Deleted.Keys() { fileName := uri.FileName() path := b.toPath(fileName) deletedPaths = append(deletedPaths, path) } b.markFilesChanged(entry, deletedPaths, lsproto.FileChangeTypeDeleted, logger) } // Handle created files if summary.Created.Len() > 0 { createdPaths := make([]tspath.Path, 0, summary.Created.Len()) for uri := range summary.Created.Keys() { fileName := uri.FileName() path := b.toPath(fileName) createdPaths = append(createdPaths, path) } b.markFilesChanged(entry, createdPaths, lsproto.FileChangeTypeCreated, logger) } return true }) // Handle opened file if summary.Opened != "" { fileName := summary.Opened.FileName() path := b.toPath(fileName) var toRemoveProjects collections.Set[tspath.Path] openFileResult := b.ensureConfiguredProjectAndAncestorsForOpenFile(fileName, path, logger) b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { toRemoveProjects.Add(entry.Value().configFilePath) b.updateProgram(entry, logger) return true }) var inferredProjectFiles []string for _, overlay := range b.fs.overlays { if p := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); p != nil { toRemoveProjects.Delete(p.Value().configFilePath) } else { inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) } } for projectPath := range toRemoveProjects.Keys() { if openFileResult.retain.Has(projectPath) { continue } if _, ok := b.apiOpenedProjects[projectPath]; ok { continue } if p, ok := b.configuredProjects.Load(projectPath); ok { b.deleteConfiguredProject(p, logger) } } slices.Sort(inferredProjectFiles) b.updateInferredProjectRoots(inferredProjectFiles, logger) b.configFileRegistryBuilder.Cleanup() } b.programStructureChanged = b.markProjectsAffectedByConfigChanges(configChangeResult, logger) } func logChangeFileResult(result changeFileResult, logger *logging.LogTree) { if len(result.affectedProjects) > 0 { logger.Logf("Config file change affected projects: %v", slices.Collect(maps.Keys(result.affectedProjects))) } if len(result.affectedFiles) > 0 { logger.Logf("Config file change affected config file lookups for %d files", len(result.affectedFiles)) } } func (b *projectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, logger *logging.LogTree) { startTime := time.Now() fileName := uri.FileName() hasChanges := b.programStructureChanged // See if we can find a default project without updating a bunch of stuff. path := b.toPath(fileName) if result := b.findDefaultProject(fileName, path); result != nil { hasChanges = b.updateProgram(result, logger) || hasChanges if result.Value() != nil { return } } // Make sure all projects we know about are up to date... b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { hasChanges = b.updateProgram(entry, logger) || hasChanges return true }) if hasChanges { // If the structure of other projects changed, we might need to move files // in/out of the inferred project. var inferredProjectFiles []string for path, overlay := range b.fs.overlays { if b.findDefaultConfiguredProject(overlay.FileName(), path) == nil { inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) } } if len(inferredProjectFiles) > 0 { b.updateInferredProjectRoots(inferredProjectFiles, logger) } } if b.inferredProject.Value() != nil { b.updateProgram(b.inferredProject, logger) } // At this point we should be able to find the default project for the file without // creating anything else. Initially, I verified that and panicked if nothing was found, // but that panic was getting triggered by fourslash infrastructure when it told us to // open a package.json file. This is something the VS Code client would never do, but // it seems possible that another client would. There's no point in panicking; we don't // really even have an error condition until it tries to ask us language questions about // a non-TS-handleable file. if logger != nil { elapsed := time.Since(startTime) logger.Log(fmt.Sprintf("Completed file request for %s in %v", fileName, elapsed)) } } func (b *projectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path]*ATAStateChange, logger *logging.LogTree) { updateProject := func(project dirty.Value[*Project], ataChange *ATAStateChange) { project.ChangeIf( func(p *Project) bool { if p == nil { return false } // Consistency check: the ATA demands (project options, unresolved imports) of this project // has not changed since the time the ATA request was dispatched; the change can still be // applied to this project in its current state. return ataChange.TypingsInfo.Equals(p.ComputeTypingsInfo()) }, func(p *Project) { // We checked before triggering this change (in Session.triggerATAForUpdatedProjects) that // the set of typings files is actually different. p.installedTypingsInfo = ataChange.TypingsInfo p.typingsFiles = ataChange.TypingsFiles typingsWatchGlobs := getTypingsLocationsGlobs( ataChange.TypingsFilesToWatch, b.sessionOptions.TypingsLocation, b.sessionOptions.CurrentDirectory, p.currentDirectory, b.fs.fs.UseCaseSensitiveFileNames(), ) p.typingsWatch = p.typingsWatch.Clone(typingsWatchGlobs) p.dirty = true p.dirtyFilePath = "" }, ) } for projectPath, ataChange := range ataChanges { logger.Embed(ataChange.Logs) if projectPath == inferredProjectName { updateProject(b.inferredProject, ataChange) } else if project, ok := b.configuredProjects.Load(projectPath); ok { updateProject(project, ataChange) } if logger != nil { logger.Log(fmt.Sprintf("Updated ATA state for project %s", projectPath)) } } } func (b *projectCollectionBuilder) markProjectsAffectedByConfigChanges( configChangeResult changeFileResult, logger *logging.LogTree, ) bool { for projectPath := range configChangeResult.affectedProjects { project, ok := b.configuredProjects.Load(projectPath) if !ok { panic(fmt.Sprintf("project %s affected by config change not found", projectPath)) } project.ChangeIf( func(p *Project) bool { return !p.dirty || p.dirtyFilePath != "" }, func(p *Project) { p.dirty = true p.dirtyFilePath = "" if logger != nil { logger.Logf("Marking project %s as dirty due to change affecting config", projectPath) } }, ) } // Recompute default projects for open files that now have different config file presence. var hasChanges bool for path := range configChangeResult.affectedFiles { fileName := b.fs.overlays[path].FileName() _ = b.ensureConfiguredProjectAndAncestorsForOpenFile(fileName, path, logger) hasChanges = true } return hasChanges } func (b *projectCollectionBuilder) findDefaultProject(fileName string, path tspath.Path) dirty.Value[*Project] { if configuredProject := b.findDefaultConfiguredProject(fileName, path); configuredProject != nil { return configuredProject } if key, ok := b.fileDefaultProjects[path]; ok && key == inferredProjectName { return b.inferredProject } if inferredProject := b.inferredProject.Value(); inferredProject != nil && inferredProject.containsFile(path) { if b.fileDefaultProjects == nil { b.fileDefaultProjects = make(map[tspath.Path]tspath.Path) } b.fileDefaultProjects[path] = inferredProjectName return b.inferredProject } return nil } func (b *projectCollectionBuilder) findDefaultConfiguredProject(fileName string, path tspath.Path) *dirty.SyncMapEntry[tspath.Path, *Project] { // !!! look in fileDefaultProjects first? // Sort configured projects so we can use a deterministic "first" as a last resort. var configuredProjectPaths []tspath.Path configuredProjects := make(map[tspath.Path]*dirty.SyncMapEntry[tspath.Path, *Project]) b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { configuredProjectPaths = append(configuredProjectPaths, entry.Key()) configuredProjects[entry.Key()] = entry return true }) slices.Sort(configuredProjectPaths) project, multipleCandidates := findDefaultConfiguredProjectFromProgramInclusion(fileName, path, configuredProjectPaths, func(path tspath.Path) *Project { return configuredProjects[path].Value() }) if multipleCandidates { if p := b.findOrCreateDefaultConfiguredProjectForOpenScriptInfo(fileName, path, projectLoadKindFind, nil).project; p != nil { return p } } return configuredProjects[project] } func (b *projectCollectionBuilder) ensureConfiguredProjectAndAncestorsForOpenFile(fileName string, path tspath.Path, logger *logging.LogTree) searchResult { result := b.findOrCreateDefaultConfiguredProjectForOpenScriptInfo(fileName, path, projectLoadKindCreate, logger) if result.project != nil { // !!! sheetal todo this later // // Create ancestor tree for findAllRefs (dont load them right away) // forEachAncestorProjectLoad( // info, // tsconfigProject!, // ancestor => { // seenProjects.set(ancestor.project, kind); // }, // kind, // `Creating project possibly referencing default composite project ${defaultProject.getProjectName()} of open file ${info.fileName}`, // allowDeferredClosed, // reloadedProjects, // /*searchOnlyPotentialSolution*/ true, // delayReloadedConfiguredProjects, // ); } return result } type searchNode struct { configFileName string loadKind projectLoadKind logger *logging.LogTree } type searchNodeKey struct { configFileName string loadKind projectLoadKind } type searchResult struct { project *dirty.SyncMapEntry[tspath.Path, *Project] retain collections.Set[tspath.Path] } func (b *projectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker( fileName string, path tspath.Path, configFileName string, loadKind projectLoadKind, visited *collections.SyncSet[searchNodeKey], fallback *searchResult, logger *logging.LogTree, ) searchResult { var configs collections.SyncMap[tspath.Path, *tsoptions.ParsedCommandLine] if visited == nil { visited = &collections.SyncSet[searchNodeKey]{} } search := core.BreadthFirstSearchParallelEx( searchNode{configFileName: configFileName, loadKind: loadKind, logger: logger}, func(node searchNode) []searchNode { if config, ok := configs.Load(b.toPath(node.configFileName)); ok && len(config.ProjectReferences()) > 0 { referenceLoadKind := node.loadKind if config.CompilerOptions().DisableReferencedProjectLoad.IsTrue() { referenceLoadKind = projectLoadKindFind } var refLogger *logging.LogTree references := config.ResolvedProjectReferencePaths() if len(references) > 0 && node.logger != nil { refLogger = node.logger.Fork(fmt.Sprintf("Searching %d project references of %s", len(references), node.configFileName)) } return core.Map(references, func(configFileName string) searchNode { return searchNode{configFileName: configFileName, loadKind: referenceLoadKind, logger: refLogger.Fork("Searching project reference " + configFileName)} }) } return nil }, func(node searchNode) (isResult bool, stop bool) { configFilePath := b.toPath(node.configFileName) config := b.configFileRegistryBuilder.findOrAcquireConfigForOpenFile(node.configFileName, configFilePath, path, node.loadKind, node.logger.Fork("Acquiring config for open file")) if config == nil { node.logger.Log("Config file for project does not already exist") return false, false } configs.Store(configFilePath, config) if len(config.FileNames()) == 0 { // Likely a solution tsconfig.json - the search will fan out to its references. node.logger.Log("Project does not contain file (no root files)") return false, false } if config.CompilerOptions().Composite == core.TSTrue { // For composite projects, we can get an early negative result. // !!! what about declaration files in node_modules? wouldn't it be better to // check project inclusion if the project is already loaded? if _, ok := config.FileNamesByPath()[path]; !ok { node.logger.Log("Project does not contain file (by composite config inclusion)") return false, false } } project := b.findOrCreateProject(node.configFileName, configFilePath, node.loadKind, node.logger) if project == nil { node.logger.Log("Project does not already exist") return false, false } if node.loadKind == projectLoadKindCreate { // Ensure project is up to date before checking for file inclusion b.updateProgram(project, node.logger) } if project.Value().containsFile(path) { isDirectInclusion := !project.Value().IsSourceFromProjectReference(path) if node.logger != nil { node.logger.Logf("Project contains file %s", core.IfElse(isDirectInclusion, "directly", "as a source of a referenced project")) } return true, isDirectInclusion } node.logger.Log("Project does not contain file") return false, false }, core.BreadthFirstSearchOptions[searchNodeKey, searchNode]{ Visited: visited, PreprocessLevel: func(level *core.BreadthFirstSearchLevel[searchNodeKey, searchNode]) { level.Range(func(node searchNode) bool { if node.loadKind == projectLoadKindFind && level.Has(searchNodeKey{configFileName: node.configFileName, loadKind: projectLoadKindCreate}) { // Remove find requests when a create request for the same project is already present. level.Delete(searchNodeKey{configFileName: node.configFileName, loadKind: node.loadKind}) } return true }) }, }, func(node searchNode) searchNodeKey { return searchNodeKey{configFileName: node.configFileName, loadKind: node.loadKind} }, ) var retain collections.Set[tspath.Path] var project *dirty.SyncMapEntry[tspath.Path, *Project] if len(search.Path) > 0 { project, _ = b.configuredProjects.Load(b.toPath(search.Path[0].configFileName)) // If we found a project, we retain each project along the BFS path. // We don't want to retain everything we visited since BFS can terminate // early, and we don't want to retain nondeterministically. for _, node := range search.Path { retain.Add(b.toPath(node.configFileName)) } } if search.Stopped { // Found a project that directly contains the file. return searchResult{ project: project, retain: retain, } } if project != nil { // If we found a project that contains the file, but it is a source from // a project reference, record it as a fallback. fallback = &searchResult{ project: project, retain: retain, } } // Look for tsconfig.json files higher up the directory tree and do the same. This handles // the common case where a higher-level "solution" tsconfig.json contains all projects in a // workspace. if config, ok := configs.Load(b.toPath(configFileName)); ok && config.CompilerOptions().DisableSolutionSearching.IsTrue() { if fallback != nil { return *fallback } } if ancestorConfigName := b.configFileRegistryBuilder.getAncestorConfigFileName(fileName, path, configFileName, loadKind, logger); ancestorConfigName != "" { return b.findOrCreateDefaultConfiguredProjectWorker( fileName, path, ancestorConfigName, loadKind, visited, fallback, logger.Fork("Searching ancestor config file at "+ancestorConfigName), ) } if fallback != nil { return *fallback } // If we didn't find anything, we can retain everything we visited, // since the whole graph must have been traversed (i.e., the set of // retained projects is guaranteed to be deterministic). visited.Range(func(node searchNodeKey) bool { retain.Add(b.toPath(node.configFileName)) return true }) return searchResult{retain: retain} } func (b *projectCollectionBuilder) findOrCreateDefaultConfiguredProjectForOpenScriptInfo( fileName string, path tspath.Path, loadKind projectLoadKind, logger *logging.LogTree, ) searchResult { if key, ok := b.fileDefaultProjects[path]; ok { if key == inferredProjectName { // The file belongs to the inferred project return searchResult{} } entry, _ := b.configuredProjects.Load(key) return searchResult{project: entry} } if configFileName := b.configFileRegistryBuilder.getConfigFileNameForFile(fileName, path, loadKind, logger); configFileName != "" { startTime := time.Now() result := b.findOrCreateDefaultConfiguredProjectWorker( fileName, path, configFileName, loadKind, nil, nil, logger.Fork("Searching for default configured project for "+fileName), ) if result.project != nil { if b.fileDefaultProjects == nil { b.fileDefaultProjects = make(map[tspath.Path]tspath.Path) } b.fileDefaultProjects[path] = result.project.Value().configFilePath } if logger != nil { elapsed := time.Since(startTime) if result.project != nil { logger.Log(fmt.Sprintf("Found default configured project for %s: %s (in %v)", fileName, result.project.Value().configFileName, elapsed)) } else { logger.Log(fmt.Sprintf("No default configured project found for %s (searched in %v)", fileName, elapsed)) } } return result } return searchResult{} } func (b *projectCollectionBuilder) findOrCreateProject( configFileName string, configFilePath tspath.Path, loadKind projectLoadKind, logger *logging.LogTree, ) *dirty.SyncMapEntry[tspath.Path, *Project] { if loadKind == projectLoadKindFind { entry, _ := b.configuredProjects.Load(configFilePath) return entry } entry, _ := b.configuredProjects.LoadOrStore(configFilePath, NewConfiguredProject(configFileName, configFilePath, b, logger)) return entry } func (b *projectCollectionBuilder) toPath(fileName string) tspath.Path { return tspath.ToPath(fileName, b.sessionOptions.CurrentDirectory, b.fs.fs.UseCaseSensitiveFileNames()) } func (b *projectCollectionBuilder) updateInferredProjectRoots(rootFileNames []string, logger *logging.LogTree) bool { if len(rootFileNames) == 0 { if b.inferredProject.Value() != nil { if logger != nil { logger.Log("Deleting inferred project") } b.inferredProject.Delete() return true } return false } if b.inferredProject.Value() == nil { b.inferredProject.Set(NewInferredProject(b.sessionOptions.CurrentDirectory, b.compilerOptionsForInferredProjects, rootFileNames, b, logger)) } else { newCompilerOptions := b.inferredProject.Value().CommandLine.CompilerOptions() if b.compilerOptionsForInferredProjects != nil { newCompilerOptions = b.compilerOptionsForInferredProjects } newCommandLine := tsoptions.NewParsedCommandLine(newCompilerOptions, rootFileNames, tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: b.fs.fs.UseCaseSensitiveFileNames(), CurrentDirectory: b.sessionOptions.CurrentDirectory, }) changed := b.inferredProject.ChangeIf( func(p *Project) bool { return !maps.Equal(p.CommandLine.FileNamesByPath(), newCommandLine.FileNamesByPath()) }, func(p *Project) { if logger != nil { logger.Log(fmt.Sprintf("Updating inferred project config with %d root files", len(rootFileNames))) } p.CommandLine = newCommandLine p.commandLineWithTypingsFiles = nil p.dirty = true p.dirtyFilePath = "" }, ) if !changed { return false } } return true } // updateProgram updates the program for the given project entry if necessary. It returns // a boolean indicating whether the update could have caused any structure-affecting changes. func (b *projectCollectionBuilder) updateProgram(entry dirty.Value[*Project], logger *logging.LogTree) bool { var updateProgram bool var filesChanged bool configFileName := entry.Value().configFileName startTime := time.Now() entry.Locked(func(entry dirty.Value[*Project]) { if entry.Value().Kind == KindConfigured { commandLine := b.configFileRegistryBuilder.acquireConfigForProject( entry.Value().configFileName, entry.Value().configFilePath, entry.Value(), logger.Fork("Acquiring config for project"), ) if entry.Value().CommandLine != commandLine { updateProgram = true if commandLine == nil { b.deleteConfiguredProject(entry, logger) filesChanged = true return } entry.Change(func(p *Project) { p.CommandLine = commandLine p.commandLineWithTypingsFiles = nil }) } } if !updateProgram { updateProgram = entry.Value().dirty } if updateProgram { entry.Change(func(project *Project) { oldHost := project.host project.host = newCompilerHost(project.currentDirectory, project, b, logger.Fork("CompilerHost")) result := project.CreateProgram() project.Program = result.Program project.checkerPool = result.CheckerPool project.ProgramUpdateKind = result.UpdateKind project.ProgramLastUpdate = b.newSnapshotID if result.UpdateKind == ProgramUpdateKindCloned { project.host.seenFiles = oldHost.seenFiles } if result.UpdateKind == ProgramUpdateKindNewFiles { filesChanged = true if b.sessionOptions.WatchEnabled { programFilesWatch, failedLookupsWatch, affectingLocationsWatch := project.CloneWatchers(b.sessionOptions.CurrentDirectory, b.sessionOptions.DefaultLibraryPath) project.programFilesWatch = programFilesWatch project.failedLookupsWatch = failedLookupsWatch project.affectingLocationsWatch = affectingLocationsWatch } } project.dirty = false project.dirtyFilePath = "" }) } }) if updateProgram && logger != nil { elapsed := time.Since(startTime) logger.Log(fmt.Sprintf("Program update for %s completed in %v", configFileName, elapsed)) } return filesChanged } func (b *projectCollectionBuilder) markFilesChanged(entry dirty.Value[*Project], paths []tspath.Path, changeType lsproto.FileChangeType, logger *logging.LogTree) { var dirty bool var dirtyFilePath tspath.Path entry.ChangeIf( func(p *Project) bool { if p.Program == nil || p.dirty && p.dirtyFilePath == "" { return false } dirtyFilePath = p.dirtyFilePath for _, path := range paths { if changeType == lsproto.FileChangeTypeCreated { if _, ok := p.affectingLocationsWatch.input[path]; ok { dirty = true dirtyFilePath = "" break } if _, ok := p.failedLookupsWatch.input[path]; ok { dirty = true dirtyFilePath = "" break } } else if p.containsFile(path) { dirty = true if changeType == lsproto.FileChangeTypeDeleted { dirtyFilePath = "" break } if dirtyFilePath == "" { dirtyFilePath = path } else if dirtyFilePath != path { dirtyFilePath = "" break } } } return dirty || p.dirtyFilePath != dirtyFilePath }, func(p *Project) { p.dirty = true p.dirtyFilePath = dirtyFilePath if logger != nil { if dirtyFilePath != "" { logger.Logf("Marking project %s as dirty due to changes in %s", p.configFileName, dirtyFilePath) } else { logger.Logf("Marking project %s as dirty", p.configFileName) } } }, ) } func (b *projectCollectionBuilder) deleteConfiguredProject(project dirty.Value[*Project], logger *logging.LogTree) { projectPath := project.Value().configFilePath if logger != nil { logger.Log("Deleting configured project: " + project.Value().configFileName) } if program := project.Value().Program; program != nil { program.ForEachResolvedProjectReference(func(referencePath tspath.Path, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) { b.configFileRegistryBuilder.releaseConfigForProject(referencePath, projectPath) }) } b.configFileRegistryBuilder.releaseConfigForProject(projectPath, projectPath) project.Delete() }