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

362 lines
11 KiB
Go

package project
import (
"fmt"
"maps"
"slices"
"strings"
"sync"
"sync/atomic"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"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/module"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
const (
minWatchLocationDepth = 2
)
type fileSystemWatcherKey struct {
pattern string
kind lsproto.WatchKind
}
type fileSystemWatcherValue struct {
count int
id WatcherID
}
type patternsAndIgnored struct {
patterns []string
ignored map[string]struct{}
}
func toFileSystemWatcherKey(w *lsproto.FileSystemWatcher) fileSystemWatcherKey {
if w.GlobPattern.RelativePattern != nil {
panic("relative globs not implemented")
}
kind := w.Kind
if kind == nil {
kind = ptrTo(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete)
}
return fileSystemWatcherKey{pattern: *w.GlobPattern.Pattern, kind: *kind}
}
type WatcherID string
var watcherID atomic.Uint64
type WatchedFiles[T any] struct {
name string
watchKind lsproto.WatchKind
computeGlobPatterns func(input T) patternsAndIgnored
mu sync.RWMutex
input T
computeWatchersOnce sync.Once
watchers []*lsproto.FileSystemWatcher
ignored map[string]struct{}
id uint64
}
func NewWatchedFiles[T any](name string, watchKind lsproto.WatchKind, computeGlobPatterns func(input T) patternsAndIgnored) *WatchedFiles[T] {
return &WatchedFiles[T]{
id: watcherID.Add(1),
name: name,
watchKind: watchKind,
computeGlobPatterns: computeGlobPatterns,
}
}
func (w *WatchedFiles[T]) Watchers() (WatcherID, []*lsproto.FileSystemWatcher, map[string]struct{}) {
w.computeWatchersOnce.Do(func() {
w.mu.Lock()
defer w.mu.Unlock()
result := w.computeGlobPatterns(w.input)
globs := result.patterns
ignored := result.ignored
// ignored is only used for logging and doesn't affect watcher identity
w.ignored = ignored
if !slices.EqualFunc(w.watchers, globs, func(a *lsproto.FileSystemWatcher, b string) bool {
return *a.GlobPattern.Pattern == b
}) {
w.watchers = core.Map(globs, func(glob string) *lsproto.FileSystemWatcher {
return &lsproto.FileSystemWatcher{
GlobPattern: lsproto.PatternOrRelativePattern{
Pattern: &glob,
},
Kind: &w.watchKind,
}
})
w.id = watcherID.Add(1)
}
})
w.mu.RLock()
defer w.mu.RUnlock()
return WatcherID(fmt.Sprintf("%s watcher %d", w.name, w.id)), w.watchers, w.ignored
}
func (w *WatchedFiles[T]) ID() WatcherID {
if w == nil {
return ""
}
id, _, _ := w.Watchers()
return id
}
func (w *WatchedFiles[T]) Name() string {
return w.name
}
func (w *WatchedFiles[T]) WatchKind() lsproto.WatchKind {
return w.watchKind
}
func (w *WatchedFiles[T]) Clone(input T) *WatchedFiles[T] {
w.mu.RLock()
defer w.mu.RUnlock()
return &WatchedFiles[T]{
name: w.name,
watchKind: w.watchKind,
computeGlobPatterns: w.computeGlobPatterns,
watchers: w.watchers,
input: input,
}
}
func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data map[tspath.Path]string) patternsAndIgnored {
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: currentDirectory,
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
}
return func(data map[tspath.Path]string) patternsAndIgnored {
var ignored map[string]struct{}
var seenDirs collections.Set[string]
var includeWorkspace, includeRoot, includeLib bool
var nodeModulesDirectories, externalDirectories map[tspath.Path]string
for path, fileName := range data {
// Assuming all of the input paths are filenames, we can avoid
// duplicate work by only taking one file per dir, since their outputs
// will always be the same.
if !seenDirs.AddIfAbsent(tspath.GetDirectoryPath(string(path))) {
continue
}
if tspath.ContainsPath(workspaceDirectory, fileName, comparePathsOptions) {
includeWorkspace = true
} else if tspath.ContainsPath(currentDirectory, fileName, comparePathsOptions) {
includeRoot = true
} else if tspath.ContainsPath(libDirectory, fileName, comparePathsOptions) {
includeLib = true
} else if idx := strings.Index(fileName, "/node_modules/"); idx != -1 {
if nodeModulesDirectories == nil {
nodeModulesDirectories = make(map[tspath.Path]string)
}
dir := fileName[:idx+len("/node_modules")]
nodeModulesDirectories[tspath.ToPath(dir, currentDirectory, useCaseSensitiveFileNames)] = dir
} else {
if externalDirectories == nil {
externalDirectories = make(map[tspath.Path]string)
}
externalDirectories[path.GetDirectoryPath()] = tspath.GetDirectoryPath(fileName)
}
}
var globs []string
if includeWorkspace {
globs = append(globs, getRecursiveGlobPattern(workspaceDirectory))
}
if includeRoot {
globs = append(globs, getRecursiveGlobPattern(currentDirectory))
}
if includeLib {
globs = append(globs, getRecursiveGlobPattern(libDirectory))
}
for _, dir := range nodeModulesDirectories {
globs = append(globs, getRecursiveGlobPattern(dir))
}
if len(externalDirectories) > 0 {
externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents(
slices.Collect(maps.Values(externalDirectories)),
minWatchLocationDepth,
getPathComponentsForWatching,
comparePathsOptions,
)
slices.Sort(externalDirectoryParents)
ignored = ignoredExternalDirs
for _, dir := range externalDirectoryParents {
globs = append(globs, getRecursiveGlobPattern(dir))
}
}
return patternsAndIgnored{
patterns: globs,
ignored: ignored,
}
}
}
func getTypingsLocationsGlobs(
typingsFiles []string,
typingsLocation string,
workspaceDirectory string,
currentDirectory string,
useCaseSensitiveFileNames bool,
) patternsAndIgnored {
var includeTypingsLocation, includeWorkspace bool
externalDirectories := make(map[tspath.Path]string)
globs := make(map[tspath.Path]string)
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: currentDirectory,
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
}
for _, file := range typingsFiles {
if tspath.ContainsPath(typingsLocation, file, comparePathsOptions) {
includeTypingsLocation = true
} else if !tspath.ContainsPath(workspaceDirectory, file, comparePathsOptions) {
directory := tspath.GetDirectoryPath(file)
externalDirectories[tspath.ToPath(directory, currentDirectory, useCaseSensitiveFileNames)] = directory
} else {
includeWorkspace = true
}
}
externalDirectoryParents, ignored := tspath.GetCommonParents(
slices.Collect(maps.Values(externalDirectories)),
minWatchLocationDepth,
getPathComponentsForWatching,
comparePathsOptions,
)
slices.Sort(externalDirectoryParents)
if includeWorkspace {
globs[tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(workspaceDirectory)
}
if includeTypingsLocation {
globs[tspath.ToPath(typingsLocation, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(typingsLocation)
}
for _, dir := range externalDirectoryParents {
globs[tspath.ToPath(dir, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(dir)
}
return patternsAndIgnored{
patterns: slices.Collect(maps.Values(globs)),
ignored: ignored,
}
}
func getPathComponentsForWatching(path string, currentDirectory string) []string {
components := tspath.GetPathComponents(path, currentDirectory)
rootLength := perceivedOsRootLengthForWatching(components)
if rootLength <= 1 {
return components
}
newRoot := tspath.CombinePaths(components[0], components[1:rootLength]...)
return append([]string{newRoot}, components[rootLength:]...)
}
func perceivedOsRootLengthForWatching(pathComponents []string) int {
length := len(pathComponents)
if length <= 1 {
return length
}
if strings.HasPrefix(pathComponents[0], "//") {
// Group UNC roots (//server/share) into a single component
return 2
}
if len(pathComponents[0]) == 3 && tspath.IsVolumeCharacter(pathComponents[0][0]) && pathComponents[0][1] == ':' && pathComponents[0][2] == '/' {
// Windows-style volume
if strings.EqualFold(pathComponents[1], "users") {
// Group C:/Users/username into a single component
return min(3, length)
}
return 1
}
if pathComponents[1] == "home" {
// Group /home/username into a single component
return min(3, length)
}
return 1
}
func ptrTo[T any](v T) *T {
return &v
}
type resolutionWithLookupLocations interface {
GetLookupLocations() *module.LookupLocations
}
func extractLookups[T resolutionWithLookupLocations](
projectToPath func(string) tspath.Path,
failedLookups map[tspath.Path]string,
affectingLocations map[tspath.Path]string,
cache map[tspath.Path]module.ModeAwareCache[T],
) {
for _, resolvedModulesInFile := range cache {
for _, resolvedModule := range resolvedModulesInFile {
for _, failedLookupLocation := range resolvedModule.GetLookupLocations().FailedLookupLocations {
path := projectToPath(failedLookupLocation)
if _, ok := failedLookups[path]; !ok {
failedLookups[path] = failedLookupLocation
}
}
for _, affectingLocation := range resolvedModule.GetLookupLocations().AffectingLocations {
path := projectToPath(affectingLocation)
if _, ok := affectingLocations[path]; !ok {
affectingLocations[path] = affectingLocation
}
}
}
}
}
func getNonRootFileGlobs(workspaceDir string, libDirectory string, sourceFiles []*ast.SourceFile, rootFiles map[tspath.Path]string, comparePathsOptions tspath.ComparePathsOptions) patternsAndIgnored {
var globs []string
var includeWorkspace, includeLib bool
var ignored map[string]struct{}
externalDirectories := make([]string, 0, max(0, len(sourceFiles)-len(rootFiles)))
for _, sourceFile := range sourceFiles {
if _, ok := rootFiles[sourceFile.Path()]; !ok {
if tspath.ContainsPath(workspaceDir, sourceFile.FileName(), comparePathsOptions) {
includeWorkspace = true
} else if tspath.ContainsPath(libDirectory, sourceFile.FileName(), comparePathsOptions) {
includeLib = true
} else {
externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(sourceFile.FileName()))
}
}
}
if includeWorkspace {
globs = append(globs, getRecursiveGlobPattern(workspaceDir))
}
if includeLib {
globs = append(globs, getRecursiveGlobPattern(libDirectory))
}
if len(externalDirectories) > 0 {
commonParents, ignoredDirs := tspath.GetCommonParents(
externalDirectories,
minWatchLocationDepth,
getPathComponentsForWatching,
comparePathsOptions,
)
globs = append(globs, core.Map(commonParents, func(dir string) string {
return getRecursiveGlobPattern(dir)
})...)
ignored = ignoredDirs
}
return patternsAndIgnored{
patterns: globs,
ignored: ignored,
}
}
func getRecursiveGlobPattern(directory string) string {
return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory), "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}")
}