270 lines
9.2 KiB
Go
270 lines
9.2 KiB
Go
package project
|
|
|
|
import (
|
|
"cmp"
|
|
"slices"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
)
|
|
|
|
type ProjectCollection struct {
|
|
toPath func(fileName string) tspath.Path
|
|
configFileRegistry *ConfigFileRegistry
|
|
// fileDefaultProjects is a map of file paths to the config file path (the key
|
|
// into `configuredProjects`) of the default project for that file. If the file
|
|
// belongs to the inferred project, the value is `inferredProjectName`. This map
|
|
// contains quick lookups for only the associations discovered during the latest
|
|
// snapshot update.
|
|
fileDefaultProjects map[tspath.Path]tspath.Path
|
|
// configuredProjects is the set of loaded projects associated with a tsconfig
|
|
// file, keyed by the config file path.
|
|
configuredProjects map[tspath.Path]*Project
|
|
// inferredProject is a fallback project that is used when no configured
|
|
// project can be found for an open file.
|
|
inferredProject *Project
|
|
// apiOpenedProjects is the set of projects that should be kept open for
|
|
// API clients.
|
|
apiOpenedProjects map[tspath.Path]struct{}
|
|
}
|
|
|
|
func (c *ProjectCollection) ConfiguredProject(path tspath.Path) *Project {
|
|
return c.configuredProjects[path]
|
|
}
|
|
|
|
func (c *ProjectCollection) GetProjectByPath(projectPath tspath.Path) *Project {
|
|
if project, ok := c.configuredProjects[projectPath]; ok {
|
|
return project
|
|
}
|
|
|
|
if projectPath == inferredProjectName {
|
|
return c.inferredProject
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConfiguredProjects returns all configured projects in a stable order.
|
|
func (c *ProjectCollection) ConfiguredProjects() []*Project {
|
|
projects := make([]*Project, 0, len(c.configuredProjects))
|
|
c.fillConfiguredProjects(&projects)
|
|
return projects
|
|
}
|
|
|
|
func (c *ProjectCollection) fillConfiguredProjects(projects *[]*Project) {
|
|
for _, p := range c.configuredProjects {
|
|
*projects = append(*projects, p)
|
|
}
|
|
slices.SortFunc(*projects, func(a, b *Project) int {
|
|
return cmp.Compare(a.Name(), b.Name())
|
|
})
|
|
}
|
|
|
|
// ProjectsByPath returns an ordered map of configured projects keyed by their config file path,
|
|
// plus the inferred project, if it exists, with the key `inferredProjectName`.
|
|
func (c *ProjectCollection) ProjectsByPath() *collections.OrderedMap[tspath.Path, *Project] {
|
|
projects := collections.NewOrderedMapWithSizeHint[tspath.Path, *Project](
|
|
len(c.configuredProjects) + core.IfElse(c.inferredProject != nil, 1, 0),
|
|
)
|
|
for _, project := range c.ConfiguredProjects() {
|
|
projects.Set(project.configFilePath, project)
|
|
}
|
|
if c.inferredProject != nil {
|
|
projects.Set(inferredProjectName, c.inferredProject)
|
|
}
|
|
return projects
|
|
}
|
|
|
|
// Projects returns all projects, including the inferred project if it exists, in a stable order.
|
|
func (c *ProjectCollection) Projects() []*Project {
|
|
if c.inferredProject == nil {
|
|
return c.ConfiguredProjects()
|
|
}
|
|
projects := make([]*Project, 0, len(c.configuredProjects)+1)
|
|
c.fillConfiguredProjects(&projects)
|
|
projects = append(projects, c.inferredProject)
|
|
return projects
|
|
}
|
|
|
|
func (c *ProjectCollection) InferredProject() *Project {
|
|
return c.inferredProject
|
|
}
|
|
|
|
// !!! result could be cached
|
|
func (c *ProjectCollection) GetDefaultProject(fileName string, path tspath.Path) *Project {
|
|
if result, ok := c.fileDefaultProjects[path]; ok {
|
|
if result == inferredProjectName {
|
|
return c.inferredProject
|
|
}
|
|
return c.configuredProjects[result]
|
|
}
|
|
|
|
var (
|
|
containingProjects []*Project
|
|
firstConfiguredProject *Project
|
|
firstNonSourceOfProjectReferenceRedirect *Project
|
|
multipleDirectInclusions bool
|
|
)
|
|
for _, p := range c.ConfiguredProjects() {
|
|
if p.containsFile(path) {
|
|
containingProjects = append(containingProjects, p)
|
|
if !multipleDirectInclusions && !p.IsSourceFromProjectReference(path) {
|
|
if firstNonSourceOfProjectReferenceRedirect == nil {
|
|
firstNonSourceOfProjectReferenceRedirect = p
|
|
} else {
|
|
multipleDirectInclusions = true
|
|
}
|
|
}
|
|
if firstConfiguredProject == nil {
|
|
firstConfiguredProject = p
|
|
}
|
|
}
|
|
}
|
|
if len(containingProjects) == 1 {
|
|
return containingProjects[0]
|
|
}
|
|
if len(containingProjects) == 0 {
|
|
if c.inferredProject != nil && c.inferredProject.containsFile(path) {
|
|
return c.inferredProject
|
|
}
|
|
return nil
|
|
}
|
|
if !multipleDirectInclusions {
|
|
if firstNonSourceOfProjectReferenceRedirect != nil {
|
|
// Multiple projects include the file, but only one is a direct inclusion.
|
|
return firstNonSourceOfProjectReferenceRedirect
|
|
}
|
|
// Multiple projects include the file, and none are direct inclusions.
|
|
return firstConfiguredProject
|
|
}
|
|
// Multiple projects include the file directly.
|
|
if defaultProject := c.findDefaultConfiguredProject(fileName, path); defaultProject != nil {
|
|
return defaultProject
|
|
}
|
|
return firstConfiguredProject
|
|
}
|
|
|
|
func (c *ProjectCollection) findDefaultConfiguredProject(fileName string, path tspath.Path) *Project {
|
|
if configFileName := c.configFileRegistry.GetConfigFileName(path); configFileName != "" {
|
|
return c.findDefaultConfiguredProjectWorker(fileName, path, configFileName, nil, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ProjectCollection) findDefaultConfiguredProjectWorker(fileName string, path tspath.Path, configFileName string, visited *collections.SyncSet[*Project], fallback *Project) *Project {
|
|
configFilePath := c.toPath(configFileName)
|
|
project, ok := c.configuredProjects[configFilePath]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if visited == nil {
|
|
visited = &collections.SyncSet[*Project]{}
|
|
}
|
|
|
|
// Look in the config's project and its references recursively.
|
|
search := core.BreadthFirstSearchParallelEx(
|
|
project,
|
|
func(project *Project) []*Project {
|
|
if project.CommandLine == nil {
|
|
return nil
|
|
}
|
|
return core.Map(project.CommandLine.ResolvedProjectReferencePaths(), func(configFileName string) *Project {
|
|
return c.configuredProjects[c.toPath(configFileName)]
|
|
})
|
|
},
|
|
func(project *Project) (isResult bool, stop bool) {
|
|
if project.containsFile(path) {
|
|
return true, !project.IsSourceFromProjectReference(path)
|
|
}
|
|
return false, false
|
|
},
|
|
core.BreadthFirstSearchOptions[*Project, *Project]{
|
|
Visited: visited,
|
|
},
|
|
core.Identity,
|
|
)
|
|
|
|
if search.Stopped {
|
|
// If we found a project that directly contains the file, return it.
|
|
return search.Path[0]
|
|
}
|
|
if len(search.Path) > 0 && fallback == 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 = search.Path[0]
|
|
}
|
|
|
|
// 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 := c.configFileRegistry.GetConfig(path); config != nil && config.CompilerOptions().DisableSolutionSearching.IsTrue() {
|
|
return fallback
|
|
}
|
|
if ancestorConfigName := c.configFileRegistry.GetAncestorConfigFileName(path, configFileName); ancestorConfigName != "" {
|
|
return c.findDefaultConfiguredProjectWorker(fileName, path, ancestorConfigName, visited, fallback)
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// clone creates a shallow copy of the project collection.
|
|
func (c *ProjectCollection) clone() *ProjectCollection {
|
|
return &ProjectCollection{
|
|
toPath: c.toPath,
|
|
configuredProjects: c.configuredProjects,
|
|
inferredProject: c.inferredProject,
|
|
fileDefaultProjects: c.fileDefaultProjects,
|
|
}
|
|
}
|
|
|
|
// findDefaultConfiguredProjectFromProgramInclusion finds the default configured project for a file
|
|
// based on the file's inclusion in existing projects. The projects should be sorted, as ties will
|
|
// be broken by slice order. `getProject` should return a project with an up-to-date program.
|
|
// Along with the resulting project path, a boolean is returned indicating whether there were multiple
|
|
// direct inclusions of the file in different projects, indicating that the caller may want to perform
|
|
// additional logic to determine the best project.
|
|
func findDefaultConfiguredProjectFromProgramInclusion(
|
|
fileName string,
|
|
path tspath.Path,
|
|
projectPaths []tspath.Path,
|
|
getProject func(tspath.Path) *Project,
|
|
) (result tspath.Path, multipleCandidates bool) {
|
|
var (
|
|
containingProjects []tspath.Path
|
|
firstConfiguredProject tspath.Path
|
|
firstNonSourceOfProjectReferenceRedirect tspath.Path
|
|
multipleDirectInclusions bool
|
|
)
|
|
|
|
for _, projectPath := range projectPaths {
|
|
p := getProject(projectPath)
|
|
if p.containsFile(path) {
|
|
containingProjects = append(containingProjects, projectPath)
|
|
if !multipleDirectInclusions && !p.IsSourceFromProjectReference(path) {
|
|
if firstNonSourceOfProjectReferenceRedirect == "" {
|
|
firstNonSourceOfProjectReferenceRedirect = projectPath
|
|
} else {
|
|
multipleDirectInclusions = true
|
|
}
|
|
}
|
|
if firstConfiguredProject == "" {
|
|
firstConfiguredProject = projectPath
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(containingProjects) == 1 {
|
|
return containingProjects[0], false
|
|
}
|
|
if !multipleDirectInclusions {
|
|
if firstNonSourceOfProjectReferenceRedirect != "" {
|
|
// Multiple projects include the file, but only one is a direct inclusion.
|
|
return firstNonSourceOfProjectReferenceRedirect, false
|
|
}
|
|
// Multiple projects include the file, and none are direct inclusions.
|
|
return firstConfiguredProject, false
|
|
}
|
|
// Multiple projects include the file directly.
|
|
return firstConfiguredProject, true
|
|
}
|