Skip to content

Commit 0de9529

Browse files
committed
Handle the configFile and extended config cache in editor
1 parent a44901d commit 0de9529

File tree

10 files changed

+612
-389
lines changed

10 files changed

+612
-389
lines changed

internal/api/api.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,17 @@ func (api *API) DocumentRegistry() *project.DocumentRegistry {
8383
return api.documentRegistry
8484
}
8585

86-
func (api *API) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine {
86+
// GetResolvedProjectReference implements ProjectHost.
87+
func (api *API) GetResolvedProjectReference(fileName string, path tspath.Path, forProject tspath.Path) *tsoptions.ParsedCommandLine {
8788
commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, api.host, nil)
8889
return commandLine
8990
}
9091

92+
// ReleaseResolvedProjectReference implements ProjectHost.
93+
func (api *API) ReleaseResolvedProjectReference(path tspath.Path, forProject tspath.Path) {
94+
// No-op, as we don't cache resolved project references.
95+
}
96+
9197
// FS implements ProjectHost.
9298
func (api *API) FS() vfs.FS {
9399
return api.host.FS()

internal/compiler/program.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ func (p *Program) GetSourceAndProjectReference(path tspath.Path) *tsoptions.Sour
118118
return p.projectReferenceFileMapper.getSourceAndProjectReference(path)
119119
}
120120

121+
func (p *Program) GetResolvedProjectReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) {
122+
return p.projectReferenceFileMapper.getResolvedReferenceFor(path)
123+
}
124+
125+
func (p *Program) ForEachResolvedProjectReference(
126+
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool,
127+
) {
128+
p.projectReferenceFileMapper.forEachResolvedProjectReference(fn)
129+
}
130+
121131
// UseCaseSensitiveFileNames implements checker.Program.
122132
func (p *Program) UseCaseSensitiveFileNames() bool {
123133
return p.Host().FS().UseCaseSensitiveFileNames()

internal/compiler/projectreferencefilemapper.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,26 @@ func (mapper *ProjectReferenceFileMapper) init(loader *fileLoader, rootTasks []*
4848
for key, value := range task.resolved.OutputDtsToSource() {
4949
mapper.outputDtsToSource[key] = value
5050
}
51-
52-
if mapper.opts.canUseProjectReferenceSource() {
53-
declDir := task.resolved.CompilerOptions().DeclarationDir
54-
if declDir == "" {
55-
declDir = task.resolved.CompilerOptions().OutDir
51+
if mapper.opts.Config.ConfigFile != task.resolved.ConfigFile {
52+
mapper.referencesInConfigFile[path] = referencesInConfig
53+
for key, value := range task.resolved.SourceToOutput() {
54+
mapper.sourceToOutput[key] = value
55+
}
56+
for key, value := range task.resolved.OutputDtsToSource() {
57+
mapper.outputDtsToSource[key] = value
5658
}
57-
if declDir != "" {
58-
loader.dtsDirectories.Add(loader.toPath(declDir))
59+
60+
if mapper.opts.canUseProjectReferenceSource() {
61+
declDir := task.resolved.CompilerOptions().DeclarationDir
62+
if declDir == "" {
63+
declDir = task.resolved.CompilerOptions().OutDir
64+
}
65+
if declDir != "" {
66+
loader.dtsDirectories.Add(loader.toPath(declDir))
67+
}
5968
}
6069
}
6170
}
62-
return
6371
})
6472
}
6573

@@ -134,6 +142,33 @@ func (mapper *ProjectReferenceFileMapper) getRedirectForResolution(file ast.HasF
134142
return nil
135143
}
136144

145+
func (mapper *ProjectReferenceFileMapper) getResolvedReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) {
146+
config, ok := mapper.configToProjectReference[path]
147+
return config, ok
148+
}
149+
150+
func (mapper *ProjectReferenceFileMapper) forEachResolvedProjectReference(
151+
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool,
152+
) {
153+
if mapper.opts.Config.ConfigFile == nil {
154+
return
155+
}
156+
refs := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
157+
mapper.forEachResolvedReferenceWorker(refs, fn)
158+
}
159+
160+
func (mapper *ProjectReferenceFileMapper) forEachResolvedReferenceWorker(
161+
referenes []tspath.Path,
162+
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool,
163+
) {
164+
for _, path := range referenes {
165+
config, _ := mapper.configToProjectReference[path]
166+
if !fn(path, config) {
167+
return
168+
}
169+
}
170+
}
171+
137172
func (mapper *ProjectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFileName) *tsoptions.SourceAndProjectReference {
138173
// If preserveSymlinks is true, module resolution wont jump the symlink
139174
// but the resolved real path may be the .d.ts from project reference

internal/project/configfilecache.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package project
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
"sync"
8+
9+
"github.com/microsoft/typescript-go/internal/collections"
10+
"github.com/microsoft/typescript-go/internal/core"
11+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
12+
"github.com/microsoft/typescript-go/internal/tsoptions"
13+
"github.com/microsoft/typescript-go/internal/tspath"
14+
)
15+
16+
type configFileCacheEntry struct {
17+
commandLineMu sync.RWMutex
18+
commandLine *tsoptions.ParsedCommandLine
19+
projects collections.SyncMap[tspath.Path, struct{}]
20+
pendingReload PendingReload
21+
rootFilesWatch *watchedFiles[[]string]
22+
extendedConfigs []tspath.Path
23+
}
24+
25+
var _ watchFileHost = (*configWatchFileHost)(nil)
26+
27+
type configFileCache struct {
28+
service *Service
29+
configFileCache collections.SyncMap[tspath.Path, *configFileCacheEntry]
30+
extendedConfigCache collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry]
31+
extendedConfigToConfig collections.SyncMap[tspath.Path, *collections.SyncMap[tspath.Path, struct{}]]
32+
}
33+
34+
type configWatchFileHost struct {
35+
fileName string
36+
service *Service
37+
}
38+
39+
func (h *configWatchFileHost) Name() string {
40+
return h.fileName
41+
}
42+
43+
func (c *configWatchFileHost) Client() Client {
44+
return c.service.host.Client()
45+
}
46+
47+
func (c *configWatchFileHost) Log(message string) {
48+
c.service.Log(message)
49+
}
50+
51+
func (c *configFileCache) getResolvedProjectReference(fileName string, path tspath.Path, forProject tspath.Path) *tsoptions.ParsedCommandLine {
52+
entry, ok := c.configFileCache.Load(path)
53+
if !ok {
54+
// Create parsed command line
55+
config, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, c.service.host, &c.extendedConfigCache)
56+
57+
var rootFilesWatch *watchedFiles[[]string]
58+
client := c.service.host.Client()
59+
if c.service.IsWatchEnabled() && client != nil {
60+
rootFilesWatch = newWatchedFiles(&configWatchFileHost{fileName: fileName, service: c.service}, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, core.Identity, "root files")
61+
}
62+
entry, _ = c.configFileCache.LoadOrStore(path, &configFileCacheEntry{
63+
commandLine: config,
64+
rootFilesWatch: rootFilesWatch,
65+
pendingReload: PendingReloadFull,
66+
})
67+
}
68+
entry.projects.Store(forProject, struct{}{})
69+
commandLine := c.ensureConfigUpToDate(fileName, path, entry)
70+
return commandLine
71+
}
72+
73+
func (c *configFileCache) ensureConfigUpToDate(fileName string, path tspath.Path, entry *configFileCacheEntry) *tsoptions.ParsedCommandLine {
74+
entry.commandLineMu.RLock()
75+
if entry.pendingReload == PendingReloadNone {
76+
entry.commandLineMu.RUnlock()
77+
return entry.commandLine
78+
}
79+
entry.commandLineMu.RUnlock()
80+
entry.commandLineMu.Lock()
81+
defer entry.commandLineMu.Unlock()
82+
switch entry.pendingReload {
83+
case PendingReloadFileNames:
84+
entry.commandLine = tsoptions.ReloadFileNamesOfParsedCommandLine(entry.commandLine, c.service.host.FS())
85+
case PendingReloadFull:
86+
entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, c.service.host, &c.extendedConfigCache)
87+
c.updateRootFilesWatch(fileName, path, entry)
88+
}
89+
entry.pendingReload = PendingReloadNone
90+
return entry.commandLine
91+
}
92+
93+
func (c *configFileCache) updateRootFilesWatch(fileName string, path tspath.Path, entry *configFileCacheEntry) {
94+
var extendedConfigs []string
95+
if entry.commandLine != nil {
96+
extendedConfigs = entry.commandLine.ConfigFile.ExtendedSourceFiles
97+
}
98+
c.updateExtendedConfigUse(path, entry, extendedConfigs)
99+
if entry.rootFilesWatch == nil {
100+
return
101+
}
102+
103+
wildcardGlobs := entry.commandLine.WildcardDirectories()
104+
rootFileGlobs := make([]string, 0, len(wildcardGlobs)+1+len(extendedConfigs))
105+
rootFileGlobs = append(rootFileGlobs, fileName)
106+
for _, extendedConfig := range extendedConfigs {
107+
rootFileGlobs = append(rootFileGlobs, extendedConfig)
108+
}
109+
for dir, recursive := range wildcardGlobs {
110+
rootFileGlobs = append(rootFileGlobs, fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern)))
111+
}
112+
for _, fileName := range entry.commandLine.LiteralFileNames() {
113+
rootFileGlobs = append(rootFileGlobs, fileName)
114+
}
115+
entry.rootFilesWatch.update(context.Background(), rootFileGlobs)
116+
}
117+
118+
func (c *configFileCache) updateExtendedConfigUse(path tspath.Path, entry *configFileCacheEntry, extendedConfigs []string) {
119+
newConfigs := make([]tspath.Path, 0, len(extendedConfigs))
120+
for _, extendedConfig := range extendedConfigs {
121+
extendedPath := c.service.toPath(extendedConfig)
122+
newConfigs = append(newConfigs, extendedPath)
123+
extendedEntry, _ := c.extendedConfigToConfig.LoadOrStore(extendedPath, &collections.SyncMap[tspath.Path, struct{}]{})
124+
extendedEntry.Store(path, struct{}{})
125+
}
126+
for _, extendedPath := range entry.extendedConfigs {
127+
if !slices.Contains(newConfigs, extendedPath) {
128+
extendedEntry, _ := c.extendedConfigToConfig.Load(extendedPath)
129+
extendedEntry.Delete(path)
130+
if extendedEntry.Size() == 0 {
131+
c.extendedConfigToConfig.Delete(extendedPath)
132+
c.extendedConfigCache.Delete(extendedPath)
133+
}
134+
}
135+
}
136+
entry.extendedConfigs = newConfigs
137+
}
138+
139+
func (c *configFileCache) releaseResolvedProjectReference(path tspath.Path, forProject tspath.Path) {
140+
entry, ok := c.configFileCache.Load(path)
141+
if !ok {
142+
return
143+
}
144+
entry.projects.Delete(forProject)
145+
146+
// !!! sheetal todo proper handling of config file deletion
147+
entry.commandLineMu.Lock()
148+
defer entry.commandLineMu.Unlock()
149+
if entry.projects.Size() == 0 {
150+
c.configFileCache.Delete(path)
151+
c.updateExtendedConfigUse(path, entry, nil)
152+
if entry.rootFilesWatch != nil {
153+
entry.rootFilesWatch.update(context.Background(), nil)
154+
entry.rootFilesWatch = nil
155+
}
156+
entry.commandLine = nil
157+
}
158+
}
159+
160+
func (c *configFileCache) onWatchedFilesChanged(path tspath.Path, changeKind lsproto.FileChangeType) (err error, handled bool) {
161+
if c.onConfigChange(path, changeKind) {
162+
handled = true
163+
}
164+
165+
// !!! todo wild cards
166+
if entry, loaded := c.extendedConfigToConfig.Load(path); loaded {
167+
entry.Range(func(configPath tspath.Path, _ struct{}) bool {
168+
c.onConfigChange(configPath, changeKind)
169+
return true
170+
})
171+
handled = true
172+
}
173+
return err, handled
174+
}
175+
176+
func (c *configFileCache) onConfigChange(path tspath.Path, changeKind lsproto.FileChangeType) bool {
177+
entry, ok := c.configFileCache.Load(path)
178+
if !ok {
179+
return false
180+
}
181+
182+
entry.commandLineMu.Lock()
183+
defer entry.commandLineMu.Unlock()
184+
entry.pendingReload = PendingReloadFull
185+
entry.commandLine = nil
186+
entry.projects.Range(func(projectPath tspath.Path, _ struct{}) bool {
187+
project := c.service.ConfiguredProject(projectPath)
188+
if project == nil {
189+
return true
190+
}
191+
if projectPath == path {
192+
switch changeKind {
193+
case lsproto.FileChangeTypeCreated:
194+
fallthrough
195+
case lsproto.FileChangeTypeChanged:
196+
project.deferredClose = false
197+
project.SetPendingReload(PendingReloadFull)
198+
case lsproto.FileChangeTypeDeleted:
199+
project.deferredClose = true
200+
}
201+
} else {
202+
project.markAsDirty()
203+
}
204+
return true
205+
})
206+
return true
207+
}
208+
209+
func (c *configFileCache) tryInvokeWildCardDirectories(fileName string, path tspath.Path) {
210+
// !!! sheetal concurrency
211+
c.configFileCache.Range(func(configPath tspath.Path, entry *configFileCacheEntry) bool {
212+
if entry.commandLine != nil && entry.commandLine.MatchesFileName(fileName) {
213+
entry.pendingReload = PendingReloadFileNames
214+
entry.projects.Range(func(projectPath tspath.Path, _ struct{}) bool {
215+
project := c.service.ConfiguredProject(projectPath)
216+
if project != nil {
217+
if projectPath == configPath {
218+
project.SetPendingReload(PendingReloadFileNames)
219+
} else {
220+
project.markAsDirty()
221+
}
222+
}
223+
return true
224+
})
225+
}
226+
return true
227+
})
228+
}

0 commit comments

Comments
 (0)