kittenipc/kitcom/internal/tsgo/project/projectcollectionbuilder.go
2025-10-15 10:12:44 +03:00

879 lines
30 KiB
Go

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()
}