Skip to content

Commit 941d154

Browse files
authored
Use open file to determine file existence (#59418)
1 parent 7319968 commit 941d154

File tree

26 files changed

+5288
-439
lines changed

26 files changed

+5288
-439
lines changed

src/compiler/watchUtilities.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -486,8 +486,8 @@ export function updateMissingFilePathsWatch(
486486
}
487487

488488
/** @internal */
489-
export interface WildcardDirectoryWatcher {
490-
watcher: FileWatcher;
489+
export interface WildcardDirectoryWatcher<T extends FileWatcher = FileWatcher> {
490+
watcher: T;
491491
flags: WatchDirectoryFlags;
492492
}
493493

@@ -499,10 +499,10 @@ export interface WildcardDirectoryWatcher {
499499
*
500500
* @internal
501501
*/
502-
export function updateWatchingWildcardDirectories(
503-
existingWatchedForWildcards: Map<string, WildcardDirectoryWatcher>,
502+
export function updateWatchingWildcardDirectories<T extends FileWatcher>(
503+
existingWatchedForWildcards: Map<string, WildcardDirectoryWatcher<T>>,
504504
wildcardDirectories: MapLike<WatchDirectoryFlags> | undefined,
505-
watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher,
505+
watchDirectory: (directory: string, flags: WatchDirectoryFlags) => T,
506506
) {
507507
if (wildcardDirectories) {
508508
mutateMap(
@@ -522,15 +522,15 @@ export function updateWatchingWildcardDirectories(
522522
clearMap(existingWatchedForWildcards, closeFileWatcherOf);
523523
}
524524

525-
function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher {
525+
function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher<T> {
526526
// Create new watch and recursive info
527527
return {
528528
watcher: watchDirectory(directory, flags),
529529
flags,
530530
};
531531
}
532532

533-
function updateWildcardDirectoryWatcher(existingWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags, directory: string) {
533+
function updateWildcardDirectoryWatcher(existingWatcher: WildcardDirectoryWatcher<T>, flags: WatchDirectoryFlags, directory: string) {
534534
// Watcher needs to be updated if the recursive flags dont match
535535
if (existingWatcher.flags === flags) {
536536
return;

src/server/editorServices.ts

Lines changed: 142 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
emptyOptions,
3434
endsWith,
3535
ensureTrailingDirectorySeparator,
36+
equateStringsCaseInsensitive,
37+
equateStringsCaseSensitive,
3638
ExtendedConfigCacheEntry,
3739
FileExtensionInfo,
3840
fileExtensionIs,
@@ -1060,7 +1062,7 @@ export interface ParsedConfig {
10601062
*/
10611063
projects: Map<NormalizedPath, boolean>;
10621064
parsedCommandLine?: ParsedCommandLine;
1063-
watchedDirectories?: Map<string, WildcardDirectoryWatcher>;
1065+
watchedDirectories?: Map<string, WildcardDirectoryWatcher<WildcardWatcher>>;
10641066
/**
10651067
* true if watchedDirectories need to be updated as per parsedCommandLine's updated watched directories
10661068
*/
@@ -1852,80 +1854,22 @@ export class ProjectService {
18521854
/**
18531855
* This is to watch whenever files are added or removed to the wildcard directories
18541856
*/
1855-
private watchWildcardDirectory(directory: string, flags: WatchDirectoryFlags, configFileName: NormalizedPath, config: ParsedConfig) {
1857+
private watchWildcardDirectory(
1858+
directory: string,
1859+
flags: WatchDirectoryFlags,
1860+
configFileName: NormalizedPath,
1861+
config: ParsedConfig,
1862+
) {
18561863
let watcher: FileWatcher | undefined = this.watchFactory.watchDirectory(
18571864
directory,
1858-
fileOrDirectory => {
1859-
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
1860-
const fsResult = config.cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
1861-
if (
1862-
getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) &&
1863-
(fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectory))
1864-
) {
1865-
const file = this.getNormalizedAbsolutePath(fileOrDirectory);
1866-
this.logger.info(`Config: ${configFileName} Detected new package.json: ${file}`);
1867-
this.packageJsonCache.addOrUpdate(file, fileOrDirectoryPath);
1868-
this.watchPackageJsonFile(file, fileOrDirectoryPath, result);
1869-
}
1870-
1871-
const configuredProjectForConfig = this.findConfiguredProjectByProjectName(configFileName);
1872-
if (
1873-
isIgnoredFileFromWildCardWatching({
1874-
watchedDirPath: this.toPath(directory),
1875-
fileOrDirectory,
1876-
fileOrDirectoryPath,
1877-
configFileName,
1878-
extraFileExtensions: this.hostConfiguration.extraFileExtensions,
1879-
currentDirectory: this.currentDirectory,
1880-
options: config.parsedCommandLine!.options,
1881-
program: configuredProjectForConfig?.getCurrentProgram() || config.parsedCommandLine!.fileNames,
1882-
useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames,
1883-
writeLog: s => this.logger.info(s),
1884-
toPath: s => this.toPath(s),
1885-
getScriptKind: configuredProjectForConfig ? (fileName => configuredProjectForConfig.getScriptKind(fileName)) : undefined,
1886-
})
1887-
) return;
1888-
1889-
// Reload is pending, do the reload
1890-
if (config.updateLevel !== ProgramUpdateLevel.Full) config.updateLevel = ProgramUpdateLevel.RootNamesAndUpdate;
1891-
config.projects.forEach((watchWildcardDirectories, projectCanonicalPath) => {
1892-
if (!watchWildcardDirectories) return;
1893-
const project = this.getConfiguredProjectByCanonicalConfigFilePath(projectCanonicalPath);
1894-
if (!project) return;
1895-
1896-
if (
1897-
configuredProjectForConfig !== project &&
1898-
this.getHostPreferences().includeCompletionsForModuleExports
1899-
) {
1900-
const path = this.toPath(configFileName);
1901-
if (find(project.getCurrentProgram()?.getResolvedProjectReferences(), ref => ref?.sourceFile.path === path)) {
1902-
project.markAutoImportProviderAsDirty();
1903-
}
1904-
}
1905-
1906-
// Load root file names for configured project with the config file name
1907-
// But only schedule update if project references this config file
1908-
const updateLevel = configuredProjectForConfig === project ? ProgramUpdateLevel.RootNamesAndUpdate : ProgramUpdateLevel.Update;
1909-
if (project.pendingUpdateLevel > updateLevel) return;
1910-
1911-
// don't trigger callback on open, existing files
1912-
if (this.openFiles.has(fileOrDirectoryPath)) {
1913-
const info = Debug.checkDefined(this.getScriptInfoForPath(fileOrDirectoryPath));
1914-
if (info.isAttached(project)) {
1915-
const loadLevelToSet = Math.max(updateLevel, project.openFileWatchTriggered.get(fileOrDirectoryPath) || ProgramUpdateLevel.Update) as ProgramUpdateLevel;
1916-
project.openFileWatchTriggered.set(fileOrDirectoryPath, loadLevelToSet);
1917-
}
1918-
else {
1919-
project.pendingUpdateLevel = updateLevel;
1920-
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1921-
}
1922-
}
1923-
else {
1924-
project.pendingUpdateLevel = updateLevel;
1925-
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1926-
}
1927-
});
1928-
},
1865+
fileOrDirectory =>
1866+
this.onWildCardDirectoryWatcherInvoke(
1867+
directory,
1868+
configFileName,
1869+
config,
1870+
result,
1871+
fileOrDirectory,
1872+
),
19291873
flags,
19301874
this.getWatchOptionsFromProjectWatchOptions(config.parsedCommandLine!.watchOptions, getDirectoryPath(configFileName)),
19311875
WatchType.WildcardDirectory,
@@ -1949,6 +1893,84 @@ export class ProjectService {
19491893
return result;
19501894
}
19511895

1896+
private onWildCardDirectoryWatcherInvoke(
1897+
directory: string,
1898+
configFileName: NormalizedPath,
1899+
config: ParsedConfig,
1900+
wildCardWatcher: WildcardWatcher,
1901+
fileOrDirectory: string,
1902+
) {
1903+
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
1904+
const fsResult = config.cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
1905+
if (
1906+
getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) &&
1907+
(fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectory))
1908+
) {
1909+
const file = this.getNormalizedAbsolutePath(fileOrDirectory);
1910+
this.logger.info(`Config: ${configFileName} Detected new package.json: ${file}`);
1911+
this.packageJsonCache.addOrUpdate(file, fileOrDirectoryPath);
1912+
this.watchPackageJsonFile(file, fileOrDirectoryPath, wildCardWatcher);
1913+
}
1914+
1915+
const configuredProjectForConfig = this.findConfiguredProjectByProjectName(configFileName);
1916+
if (
1917+
isIgnoredFileFromWildCardWatching({
1918+
watchedDirPath: this.toPath(directory),
1919+
fileOrDirectory,
1920+
fileOrDirectoryPath,
1921+
configFileName,
1922+
extraFileExtensions: this.hostConfiguration.extraFileExtensions,
1923+
currentDirectory: this.currentDirectory,
1924+
options: config.parsedCommandLine!.options,
1925+
program: configuredProjectForConfig?.getCurrentProgram() || config.parsedCommandLine!.fileNames,
1926+
useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames,
1927+
writeLog: s => this.logger.info(s),
1928+
toPath: s => this.toPath(s),
1929+
getScriptKind: configuredProjectForConfig ? (fileName => configuredProjectForConfig.getScriptKind(fileName)) : undefined,
1930+
})
1931+
) return;
1932+
1933+
// Reload is pending, do the reload
1934+
if (config.updateLevel !== ProgramUpdateLevel.Full) config.updateLevel = ProgramUpdateLevel.RootNamesAndUpdate;
1935+
config.projects.forEach((watchWildcardDirectories, projectCanonicalPath) => {
1936+
if (!watchWildcardDirectories) return;
1937+
const project = this.getConfiguredProjectByCanonicalConfigFilePath(projectCanonicalPath);
1938+
if (!project) return;
1939+
1940+
if (
1941+
configuredProjectForConfig !== project &&
1942+
this.getHostPreferences().includeCompletionsForModuleExports
1943+
) {
1944+
const path = this.toPath(configFileName);
1945+
if (find(project.getCurrentProgram()?.getResolvedProjectReferences(), ref => ref?.sourceFile.path === path)) {
1946+
project.markAutoImportProviderAsDirty();
1947+
}
1948+
}
1949+
1950+
// Load root file names for configured project with the config file name
1951+
// But only schedule update if project references this config file
1952+
const updateLevel = configuredProjectForConfig === project ? ProgramUpdateLevel.RootNamesAndUpdate : ProgramUpdateLevel.Update;
1953+
if (project.pendingUpdateLevel > updateLevel) return;
1954+
1955+
// don't trigger callback on open, existing files
1956+
if (this.openFiles.has(fileOrDirectoryPath)) {
1957+
const info = Debug.checkDefined(this.getScriptInfoForPath(fileOrDirectoryPath));
1958+
if (info.isAttached(project)) {
1959+
const loadLevelToSet = Math.max(updateLevel, project.openFileWatchTriggered.get(fileOrDirectoryPath) || ProgramUpdateLevel.Update) as ProgramUpdateLevel;
1960+
project.openFileWatchTriggered.set(fileOrDirectoryPath, loadLevelToSet);
1961+
}
1962+
else {
1963+
project.pendingUpdateLevel = updateLevel;
1964+
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1965+
}
1966+
}
1967+
else {
1968+
project.pendingUpdateLevel = updateLevel;
1969+
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1970+
}
1971+
});
1972+
}
1973+
19521974
private delayUpdateProjectsFromParsedConfigOnConfigFileChange(canonicalConfigFilePath: NormalizedPath, loadReason: string) {
19531975
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
19541976
if (!configFileExistenceInfo?.config) return false;
@@ -4434,8 +4456,42 @@ export class ProjectService {
44344456
this.removeOrphanScriptInfos();
44354457
}
44364458

4459+
private tryInvokeWildCardDirectories(info: ScriptInfo) {
4460+
// This might not have reflected in projects,
4461+
this.configFileExistenceInfoCache.forEach((configFileExistenceInfo, config) => {
4462+
if (
4463+
!configFileExistenceInfo.config?.parsedCommandLine ||
4464+
contains(
4465+
configFileExistenceInfo.config.parsedCommandLine.fileNames,
4466+
info.fileName,
4467+
!this.host.useCaseSensitiveFileNames ? equateStringsCaseInsensitive : equateStringsCaseSensitive,
4468+
)
4469+
) {
4470+
return;
4471+
}
4472+
configFileExistenceInfo.config.watchedDirectories?.forEach((watcher, directory) => {
4473+
if (containsPath(directory, info.fileName, !this.host.useCaseSensitiveFileNames)) {
4474+
this.logger.info(`Invoking ${config}:: wildcard for open scriptInfo:: ${info.fileName}`);
4475+
this.onWildCardDirectoryWatcherInvoke(
4476+
directory,
4477+
config,
4478+
configFileExistenceInfo.config!,
4479+
watcher.watcher,
4480+
info.fileName,
4481+
);
4482+
}
4483+
});
4484+
});
4485+
}
4486+
44374487
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult {
4488+
const existing = this.getScriptInfoForPath(normalizedPathToPath(
4489+
fileName,
4490+
projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory,
4491+
this.toCanonicalFileName,
4492+
));
44384493
const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath);
4494+
if (!existing && info && !info.isDynamic) this.tryInvokeWildCardDirectories(info);
44394495
const { retainProjects, ...result } = this.assignProjectToOpenedScriptInfo(info);
44404496
this.cleanupProjectsAndScriptInfos(
44414497
retainProjects,
@@ -4636,10 +4692,16 @@ export class ProjectService {
46364692

46374693
/** @internal */
46384694
applyChangesInOpenFiles(openFiles: Iterable<OpenFileArguments> | undefined, changedFiles?: Iterable<ChangeFileArguments>, closedFiles?: string[]): void {
4695+
let existingOpenScriptInfos: (ScriptInfo | undefined)[] | undefined;
46394696
let openScriptInfos: ScriptInfo[] | undefined;
46404697
let assignOrphanScriptInfosToInferredProject = false;
46414698
if (openFiles) {
46424699
for (const file of openFiles) {
4700+
(existingOpenScriptInfos ??= []).push(this.getScriptInfoForPath(normalizedPathToPath(
4701+
toNormalizedPath(file.fileName),
4702+
file.projectRootPath ? this.getNormalizedAbsolutePath(file.projectRootPath) : this.currentDirectory,
4703+
this.toCanonicalFileName,
4704+
)));
46434705
// Create script infos so we have the new content for all the open files before we do any updates to projects
46444706
const info = this.getOrCreateOpenScriptInfo(
46454707
toNormalizedPath(file.fileName),
@@ -4670,6 +4732,13 @@ export class ProjectService {
46704732

46714733
// All the script infos now exist, so ok to go update projects for open files
46724734
let retainProjects: Set<ConfiguredProject> | undefined;
4735+
forEach(
4736+
existingOpenScriptInfos,
4737+
(existing, index) =>
4738+
!existing && openScriptInfos![index] && !openScriptInfos![index].isDynamic ?
4739+
this.tryInvokeWildCardDirectories(openScriptInfos![index]) :
4740+
undefined,
4741+
);
46734742
openScriptInfos?.forEach(info => this.assignProjectToOpenedScriptInfo(info).retainProjects?.forEach(p => (retainProjects ??= new Set()).add(p)));
46744743

46754744
// While closing files there could be open files that needed assigning new inferred projects, do it now

src/server/project.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
775775
// As an optimization, don't hit the disks for files we already know don't exist
776776
// (because we're watching for their creation).
777777
const path = this.toPath(file);
778-
return !this.isWatchedMissingFile(path) && this.directoryStructureHost.fileExists(file);
778+
return !!this.projectService.getScriptInfoForPath(path) ||
779+
(!this.isWatchedMissingFile(path) && this.directoryStructureHost.fileExists(file));
779780
}
780781

781782
/** @internal */

0 commit comments

Comments
 (0)