Keep scriptInfo and project alive even after file delete till next file open (#57492)

This commit is contained in:
Sheetal Nandi
2024-04-12 10:43:09 -07:00
committed by GitHub
parent 551a600e84
commit 4e294963c8
244 changed files with 210245 additions and 3635 deletions
+7
View File
@@ -13,6 +13,7 @@ import {
import {
AutoImportProviderProject,
AuxiliaryProject,
ConfiguredProject,
isBackgroundProject,
isConfiguredProject,
LogLevel,
@@ -32,6 +33,7 @@ interface ProjectData {
isClosed: ReturnType<Project["isClosed"]>;
isOrphan: ReturnType<Project["isOrphan"]>;
noOpenRef: boolean;
deferredClose: ConfiguredProject["deferredClose"];
documentPositionMappers: SourceMapper["documentPositionMappers"];
autoImportProviderHost: Project["autoImportProviderHost"];
noDtsResolutionProject: Project["noDtsResolutionProject"];
@@ -46,6 +48,7 @@ interface ScriptInfoData {
open: ReturnType<ScriptInfo["isScriptOpen"]>;
version: ReturnType<TextStorage["getVersion"]>;
pendingReloadFromDisk: TextStorage["pendingReloadFromDisk"];
deferredDelete: ScriptInfo["deferredDelete"];
sourceMapFilePath: Exclude<ScriptInfo["sourceMapFilePath"], SourceMapFileWatcher> | SourceMapFileWatcherData | undefined;
declarationInfoPath: ScriptInfo["declarationInfoPath"];
sourceInfos: ScriptInfo["sourceInfos"];
@@ -116,6 +119,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isClosed", project.isClosed(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isOrphan", !isBackgroundProject(project) && project.isOrphan(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "noOpenRef", isConfiguredProject(project) && !project.hasOpenRef(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredClose", isConfiguredProject(project) && project.deferredClose, projectDiff, projectPropertyLogs);
projectDiff = printMapPropertyValue(
PrintPropertyWhen.Changed,
data?.documentPositionMappers,
@@ -146,6 +150,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
isClosed: project.isClosed(),
isOrphan: !isBackgroundProject(project) && project.isOrphan(),
noOpenRef: isConfiguredProject(project) && !project.hasOpenRef(),
deferredClose: isConfiguredProject(project) && project.deferredClose,
autoImportProviderHost: project.autoImportProviderHost,
noDtsResolutionProject: project.noDtsResolutionProject,
originalConfiguredProjects: project.originalConfiguredProjects && new Set(project.originalConfiguredProjects),
@@ -166,6 +171,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
infoDiff = printProperty(PrintPropertyWhen.Changed, data, "open", isOpen, infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.Always, data, "version", info.textStorage.getVersion(), infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "pendingReloadFromDisk", info.textStorage.pendingReloadFromDisk, infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredDelete", info.deferredDelete, infoDiff, infoPropertyLogs);
infoDiff = printScriptInfoSourceMapFilePath(data, info, infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.DefinedOrChangedOrNew, data, "declarationInfoPath", info.declarationInfoPath, infoDiff, infoPropertyLogs);
infoDiff = printSetPropertyValueWorker(PrintPropertyWhen.DefinedOrChangedOrNew, data?.sourceInfos, "sourceInfos", info.sourceInfos, infoDiff, infoPropertyLogs, identity);
@@ -200,6 +206,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
sourceInfos: info.sourceInfos && new Set(info.sourceInfos),
documentPositionMapper: info.documentPositionMapper,
containingProjects: new Set(info.containingProjects),
deferredDelete: info.deferredDelete,
}),
);
}
+295 -176
View File
@@ -160,6 +160,7 @@ import {
isDynamicFileName,
isInferredProject,
isInferredProjectName,
isProjectDeferredClose,
ITypingsInstaller,
Logger,
LogLevel,
@@ -1023,14 +1024,14 @@ export class ProjectService {
*
* @internal
*/
readonly filenameToScriptInfo = new Map<string, ScriptInfo>();
readonly filenameToScriptInfo = new Map<Path, ScriptInfo>();
private readonly nodeModulesWatchers = new Map<Path, NodeModulesWatcher>();
/**
* Contains all the deleted script info's version information so that
* it does not reset when creating script info again
* (and could have potentially collided with version where contents mismatch)
*/
private readonly filenameToScriptInfoVersion = new Map<string, number>();
private readonly filenameToScriptInfoVersion = new Map<Path, number>();
// Set of all '.js' files ever opened.
private readonly allJsFilesForOpenFileTelemetry = new Map<string, true>();
@@ -1066,7 +1067,7 @@ export class ProjectService {
/**
* Open files: with value being project root path, and key being Path of the file that is open
*/
readonly openFiles: Map<string, NormalizedPath | undefined> = new Map<Path, NormalizedPath | undefined>();
readonly openFiles: Map<Path, NormalizedPath | undefined> = new Map<Path, NormalizedPath | undefined>();
/** @internal */
readonly configFileForOpenFiles = new Map<Path, NormalizedPath | false>();
/**
@@ -1352,6 +1353,7 @@ export class ProjectService {
}
private delayUpdateProjectGraph(project: Project) {
if (isProjectDeferredClose(project)) return;
project.markAsDirty();
if (isBackgroundProject(project)) return;
const projectName = project.getProjectName();
@@ -1377,7 +1379,7 @@ export class ProjectService {
const event: ProjectsUpdatedInBackgroundEvent = {
eventName: ProjectsUpdatedInBackgroundEvent,
data: {
openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path)!.fileName),
openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path)!.fileName),
},
};
this.eventHandler(event);
@@ -1591,11 +1593,12 @@ export class ProjectService {
}
private onSourceFileChanged(info: ScriptInfo, eventKind: FileWatcherEventKind) {
Debug.assert(!info.isScriptOpen());
if (eventKind === FileWatcherEventKind.Deleted) {
// File was deleted
this.handleDeletedFile(info);
this.handleDeletedFile(info, /*deferredDelete*/ true);
}
else if (!info.isScriptOpen()) {
else {
if (info.deferredDelete) info.deferredDelete = undefined;
// file has been changed which might affect the set of referenced files in projects that include
// this file and set of inferred projects
info.delayReloadNonMixedContentFile();
@@ -1609,7 +1612,7 @@ export class ProjectService {
if (info.sourceMapFilePath) {
if (isString(info.sourceMapFilePath)) {
const sourceMapFileInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
this.delayUpdateSourceInfoProjects(sourceMapFileInfo && sourceMapFileInfo.sourceInfos);
this.delayUpdateSourceInfoProjects(sourceMapFileInfo?.sourceInfos);
}
else {
this.delayUpdateSourceInfoProjects(info.sourceMapFilePath.sourceInfos);
@@ -1635,28 +1638,17 @@ export class ProjectService {
}
}
private handleDeletedFile(info: ScriptInfo) {
this.stopWatchingScriptInfo(info);
if (!info.isScriptOpen()) {
private handleDeletedFile(info: ScriptInfo, deferredDelete: boolean) {
Debug.assert(!info.isScriptOpen());
this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ false);
this.handleSourceMapProjects(info);
info.detachAllProjects();
if (deferredDelete) {
info.delayReloadNonMixedContentFile();
info.deferredDelete = true;
}
else {
this.deleteScriptInfo(info);
// capture list of projects since detachAllProjects will wipe out original list
const containingProjects = info.containingProjects.slice();
info.detachAllProjects();
// update projects to make sure that set of referenced files is correct
this.delayUpdateProjectGraphs(containingProjects, /*clearSourceMapperCache*/ false);
this.handleSourceMapProjects(info);
info.closeSourceMapFileWatcher();
// need to recalculate source map from declaration file
if (info.declarationInfoPath) {
const declarationInfo = this.getScriptInfoForPath(info.declarationInfoPath);
if (declarationInfo) {
declarationInfo.sourceMapFilePath = undefined;
}
}
}
}
@@ -1784,21 +1776,24 @@ export class ProjectService {
/** @internal */
private onConfigFileChanged(canonicalConfigFilePath: NormalizedPath, eventKind: FileWatcherEventKind) {
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!;
const project = this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
const wasDefferedClose = project?.deferredClose;
if (eventKind === FileWatcherEventKind.Deleted) {
// Update the cached status
// We arent updating or removing the cached config file presence info as that will be taken care of by
// releaseParsedConfig when the project is closed or doesnt need this config any more (depending on tracking open files)
configFileExistenceInfo.exists = false;
// Remove the configured project for this config file
const project = configFileExistenceInfo.config?.projects.has(canonicalConfigFilePath) ?
this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath) :
undefined;
if (project) this.removeProject(project);
// Deferred remove the configured project for this config file
if (project) project.deferredClose = true;
}
else {
// Update the cached status
configFileExistenceInfo.exists = true;
if (wasDefferedClose) {
project.deferredClose = undefined;
project.markAsDirty();
}
}
// Update projects watching config
@@ -1810,11 +1805,9 @@ export class ProjectService {
// Otherwise, we scheduled the update on configured project graph,
// we would need to schedule the project reload for only the root of inferred projects
// Get open files to reload projects for
this.reloadConfiguredProjectForFiles(
configFileExistenceInfo.openFilesImpactedByConfigFile,
/*clearSemanticCache*/ false,
/*delayReload*/ true,
eventKind !== FileWatcherEventKind.Deleted ?
this.delayReloadConfiguredProjectsForFile(
configFileExistenceInfo,
!wasDefferedClose && eventKind !== FileWatcherEventKind.Deleted ?
identity : // Reload open files if they are root of inferred project
returnTrue, // Reload all the open files impacted by config file
"Change in config file detected",
@@ -1822,6 +1815,44 @@ export class ProjectService {
this.delayEnsureProjectForOpenFiles();
}
/**
* This function goes through all the openFiles and tries to file the config file for them.
* If the config file is found and it refers to existing project, it schedules the reload it for reload
* If there is no existing project it just opens the configured project for the config file
* shouldReloadProjectFor provides a way to filter out files to reload configured project for
*/
private delayReloadConfiguredProjectsForFile(
configFileExistenceInfo: ConfigFileExistenceInfo | undefined,
shouldReloadProjectFor: (infoIsRootOfInferredProject: boolean) => boolean,
reason: string,
) {
const updatedProjects = new Set<ConfiguredProject>();
// try to reload config file for all open files
configFileExistenceInfo?.openFilesImpactedByConfigFile?.forEach((infoIsRootOfInferredProject, path) => {
// Invalidate default config file name for open file
this.configFileForOpenFiles.delete(path);
// Filter out the files that need to be ignored
if (!shouldReloadProjectFor(infoIsRootOfInferredProject)) {
return;
}
const info = this.getScriptInfoForPath(path)!;
Debug.assert(info.isScriptOpen());
// This tries to search for a tsconfig.json for the given file. If we found it,
// we first detect if there is already a configured project created for it: if so,
// we re- read the tsconfig file content and update the project only if we havent already done so
// otherwise we create a new one.
const configFileName = this.getConfigFileNameForFile(info);
if (configFileName) {
const project = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
if (tryAddToSet(updatedProjects, project)) {
project.pendingUpdateLevel = ProgramUpdateLevel.Full;
project.pendingUpdateReason = reason;
this.delayUpdateProjectGraph(project);
}
}
});
}
private removeProject(project: Project) {
this.logger.info("`remove Project::");
project.print(/*writeProjectFileNames*/ true, /*writeFileExplaination*/ true, /*writeFileVersionAndText*/ false);
@@ -1930,7 +1961,7 @@ export class ProjectService {
private assignOrphanScriptInfosToInferredProject() {
// collect orphaned files and assign them to inferred project just like we treat open of a file
this.openFiles.forEach((projectRootPath, path) => {
const info = this.getScriptInfoForPath(path as Path)!;
const info = this.getScriptInfoForPath(path)!;
// collect all orphaned script infos from open files
if (info.isOrphan()) {
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
@@ -2012,19 +2043,21 @@ export class ProjectService {
this.watchClosedScriptInfo(info);
}
else {
this.handleDeletedFile(info);
this.handleDeletedFile(info, /*deferredDelete*/ false);
}
return ensureProjectsForOpenFiles;
}
private deleteScriptInfo(info: ScriptInfo) {
Debug.assert(!info.isScriptOpen());
this.filenameToScriptInfo.delete(info.path);
this.filenameToScriptInfoVersion.set(info.path, info.textStorage.version);
this.stopWatchingScriptInfo(info);
const realpath = info.getRealpathIfDifferent();
if (realpath) {
this.realpathToScriptInfos!.remove(realpath, info); // TODO: GH#18217
}
info.closeSourceMapFileWatcher();
}
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: NormalizedPath, info: OpenScriptInfoOrClosedOrConfigFileInfo) {
@@ -2332,7 +2365,7 @@ export class ProjectService {
this.logger.info("Open files: ");
this.openFiles.forEach((projectRootPath, path) => {
const info = this.getScriptInfoForPath(path as Path)!;
const info = this.getScriptInfoForPath(path)!;
this.logger.info(`\tFileName: ${info.fileName} ProjectRootPath: ${projectRootPath}`);
this.logger.info(`\t\tProjects: ${info.containingProjects.map(p => p.getProjectName())}`);
});
@@ -2344,7 +2377,8 @@ export class ProjectService {
findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined {
// make sure that casing of config file name is consistent
const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName));
return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
const result = this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
return !result?.deferredClose ? result : undefined;
}
private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined {
@@ -2494,6 +2528,7 @@ export class ProjectService {
this.documentRegistry,
configFileExistenceInfo.config.cachedDirectoryStructureHost,
);
Debug.assert(!this.configuredProjects.has(canonicalConfigFilePath));
this.configuredProjects.set(canonicalConfigFilePath, project);
this.createConfigFileWatcherForParsedConfig(configFileName, canonicalConfigFilePath, project);
return project;
@@ -2748,6 +2783,7 @@ export class ProjectService {
scriptKind,
hasMixedContent,
project.directoryStructureHost,
/*deferredDeleteOk*/ false,
));
path = scriptInfo.path;
const existingValue = projectRootFilesMap.get(path);
@@ -2982,13 +3018,19 @@ export class ProjectService {
}
/** @internal */
getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, currentDirectory: string, hostToQueryFileExistsOn: DirectoryStructureHost) {
getOrCreateScriptInfoNotOpenedByClient(
uncheckedFileName: string,
currentDirectory: string,
hostToQueryFileExistsOn: DirectoryStructureHost,
deferredDeleteOk: boolean,
) {
return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
toNormalizedPath(uncheckedFileName),
currentDirectory,
/*scriptKind*/ undefined,
/*hasMixedContent*/ undefined,
hostToQueryFileExistsOn,
deferredDeleteOk,
);
}
@@ -3007,7 +3049,13 @@ export class ProjectService {
/** @internal */
logErrorForScriptInfoNotFound(fileName: string): void {
const names = arrayFrom(this.filenameToScriptInfo.entries(), ([path, scriptInfo]) => ({ path, fileName: scriptInfo.fileName }));
const names = arrayFrom(
mapDefinedIterator(
this.filenameToScriptInfo.entries(),
entry => entry[1].deferredDelete ? undefined : entry,
),
([path, scriptInfo]) => ({ path, fileName: scriptInfo.fileName }),
);
this.logger.msg(`Could not find file ${JSON.stringify(fileName)}.\nAll files are: ${JSON.stringify(names)}`, Msg.Err);
}
@@ -3104,7 +3152,7 @@ export class ProjectService {
this.refreshScriptInfosInDirectory(dirPath);
}
else {
const info = this.getScriptInfoForPath(fileOrDirectoryPath);
const info = this.filenameToScriptInfo.get(fileOrDirectoryPath);
if (info) {
if (isScriptInfoWatchedFromNodeModules(info)) {
this.refreshScriptInfo(info);
@@ -3194,9 +3242,25 @@ export class ProjectService {
}
}
private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) {
private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
fileName: NormalizedPath,
currentDirectory: string,
scriptKind: ScriptKind | undefined,
hasMixedContent: boolean | undefined,
hostToQueryFileExistsOn: DirectoryStructureHost | undefined,
deferredDeleteOk: boolean,
) {
if (isRootedDiskPath(fileName) || isDynamicFileName(fileName)) {
return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
return this.getOrCreateScriptInfoWorker(
fileName,
currentDirectory,
/*openedByClient*/ false,
/*fileContent*/ undefined,
scriptKind,
!!hasMixedContent,
hostToQueryFileExistsOn,
deferredDeleteOk,
);
}
// This is non rooted path with different current directory than project service current directory
@@ -3211,18 +3275,39 @@ export class ProjectService {
return undefined;
}
private getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) {
return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
getOrCreateScriptInfoForNormalizedPath(
fileName: NormalizedPath,
openedByClient: boolean,
fileContent?: string,
scriptKind?: ScriptKind,
hasMixedContent?: boolean,
hostToQueryFileExistsOn?: { fileExists(path: string): boolean; },
) {
return this.getOrCreateScriptInfoWorker(
fileName,
this.currentDirectory,
openedByClient,
fileContent,
scriptKind,
!!hasMixedContent,
hostToQueryFileExistsOn,
/*deferredDeleteOk*/ false,
);
}
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) {
return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
}
private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) {
private getOrCreateScriptInfoWorker(
fileName: NormalizedPath,
currentDirectory: string,
openedByClient: boolean,
fileContent: string | undefined,
scriptKind: ScriptKind | undefined,
hasMixedContent: boolean,
hostToQueryFileExistsOn: { fileExists(path: string): boolean; } | undefined,
deferredDeleteOk: boolean,
) {
Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content");
const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName);
let info = this.getScriptInfoForPath(path);
let info = this.filenameToScriptInfo.get(path);
if (!info) {
const isDynamic = isDynamicFileName(fileName);
Debug.assert(isRootedDiskPath(fileName) || isDynamic || openedByClient, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nScript info with non-dynamic relative file name can only be open script info or in context of host currentDirectory`);
@@ -3232,7 +3317,7 @@ export class ProjectService {
if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
return;
}
info = new ScriptInfo(this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217
info = new ScriptInfo(this.host, fileName, scriptKind!, hasMixedContent, path, this.filenameToScriptInfoVersion.get(path));
this.filenameToScriptInfo.set(info.path, info);
this.filenameToScriptInfoVersion.delete(info.path);
if (!openedByClient) {
@@ -3243,6 +3328,14 @@ export class ProjectService {
this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info);
}
}
else if (info.deferredDelete) {
Debug.assert(!info.isDynamic);
// If the file is not opened by client and the file doesnot exist on the disk, return
if (!openedByClient && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
return deferredDeleteOk ? info : undefined;
}
info.deferredDelete = undefined;
}
if (openedByClient) {
// Opening closed script info
// either it was created just now, or was part of projects but was closed
@@ -3264,13 +3357,19 @@ export class ProjectService {
}
getScriptInfoForPath(fileName: Path) {
return this.filenameToScriptInfo.get(fileName);
const info = this.filenameToScriptInfo.get(fileName);
return !info || !info.deferredDelete ? info : undefined;
}
/** @internal */
getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined {
// Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host
const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host);
const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(
generatedFileName,
project.currentDirectory,
this.host,
/*deferredDeleteOk*/ false,
);
if (!declarationInfo) {
if (sourceFileName) {
// Project contains source file and it generates the generated file name
@@ -3303,16 +3402,16 @@ export class ProjectService {
}
// Create the mapper
let sourceMapFileInfo: ScriptInfo | undefined;
let mapFileNameFromDeclarationInfo: string | undefined;
let sourceMapFileInfo: ScriptInfo | string | undefined;
let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => {
const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host);
if (!mapInfo) {
mapFileNameFromDeclarationInfo = mapFileNameFromDts;
return undefined;
}
sourceMapFileInfo = mapInfo;
const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(
mapFileName,
project.currentDirectory,
this.host,
/*deferredDeleteOk*/ true,
);
sourceMapFileInfo = mapInfo || mapFileNameFromDts;
if (!mapInfo || mapInfo.deferredDelete) return undefined;
const snap = mapInfo.getSnapshot();
if (mapInfo.documentPositionMapper !== undefined) return mapInfo.documentPositionMapper;
return getSnapshotText(snap);
@@ -3326,21 +3425,23 @@ export class ProjectService {
);
readMapFile = undefined; // Remove ref to project
if (sourceMapFileInfo) {
declarationInfo.sourceMapFilePath = sourceMapFileInfo.path;
sourceMapFileInfo.declarationInfoPath = declarationInfo.path;
sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false;
sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos);
}
else if (mapFileNameFromDeclarationInfo) {
declarationInfo.sourceMapFilePath = {
watcher: this.addMissingSourceMapFile(
project.currentDirectory === this.currentDirectory ?
mapFileNameFromDeclarationInfo :
getNormalizedAbsolutePath(mapFileNameFromDeclarationInfo, project.currentDirectory),
declarationInfo.path,
),
sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project),
};
if (!isString(sourceMapFileInfo)) {
declarationInfo.sourceMapFilePath = sourceMapFileInfo.path;
sourceMapFileInfo.declarationInfoPath = declarationInfo.path;
if (!sourceMapFileInfo.deferredDelete) sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false;
sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos);
}
else {
declarationInfo.sourceMapFilePath = {
watcher: this.addMissingSourceMapFile(
project.currentDirectory === this.currentDirectory ?
sourceMapFileInfo :
getNormalizedAbsolutePath(sourceMapFileInfo, project.currentDirectory),
declarationInfo.path,
),
sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project),
};
}
}
else {
declarationInfo.sourceMapFilePath = false;
@@ -3351,7 +3452,12 @@ export class ProjectService {
private addSourceInfoToSourceMap(sourceFileName: string | undefined, project: Project, sourceInfos?: Set<Path>) {
if (sourceFileName) {
// Attach as source
const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(sourceFileName, project.currentDirectory, project.directoryStructureHost)!;
const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(
sourceFileName,
project.currentDirectory,
project.directoryStructureHost,
/*deferredDeleteOk*/ false,
)!;
(sourceInfos || (sourceInfos = new Set())).add(sourceInfo.path);
}
return sourceInfos;
@@ -3386,14 +3492,19 @@ export class ProjectService {
}
// Need to look for other files.
const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host);
const info = this.getOrCreateScriptInfoNotOpenedByClient(
fileName,
(project || this).currentDirectory,
project ? project.directoryStructureHost : this.host,
/*deferredDeleteOk*/ false,
);
if (!info) return undefined;
// Attach as source
if (declarationInfo && isString(declarationInfo.sourceMapFilePath) && info !== declarationInfo) {
const sourceMapInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath);
if (sourceMapInfo) {
(sourceMapInfo.sourceInfos || (sourceMapInfo.sourceInfos = new Set())).add(info.path);
(sourceMapInfo.sourceInfos ??= new Set()).add(info.path);
}
}
@@ -3451,6 +3562,7 @@ export class ProjectService {
this.externalProjectToConfiguredProjectMap.forEach(projects =>
projects.forEach(project => {
if (
!project.deferredClose &&
!project.isClosed() &&
project.hasExternalProjectRef() &&
project.pendingUpdateLevel === ProgramUpdateLevel.Full &&
@@ -3514,7 +3626,14 @@ export class ProjectService {
if (this.openFiles.has(info.path)) return; // Skip open files
if (!info.fileWatcher) return; // not watched file
// Handle as if file is changed or deleted
this.onSourceFileChanged(info, this.host.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted);
this.onSourceFileChanged(
info,
this.host.fileExists(info.fileName) ?
info.deferredDelete ?
FileWatcherEventKind.Created :
FileWatcherEventKind.Changed :
FileWatcherEventKind.Deleted,
);
});
// Cancel all project updates since we will be updating them now
this.pendingProjectUpdates.forEach((_project, projectName) => {
@@ -3530,7 +3649,7 @@ export class ProjectService {
});
// Reload Projects
this.reloadConfiguredProjectForFiles(this.openFiles as Map<Path, NormalizedPath | undefined>, /*clearSemanticCache*/ true, /*delayReload*/ false, returnTrue, "User requested reload projects");
this.reloadConfiguredProjectForFiles("User requested reload projects");
this.externalProjects.forEach(project => {
this.clearSemanticCache(project);
project.updateGraph();
@@ -3545,26 +3664,19 @@ export class ProjectService {
/**
* This function goes through all the openFiles and tries to file the config file for them.
* If the config file is found and it refers to existing project, it reloads it either immediately
* or schedules it for reload depending on delayReload option
* If there is no existing project it just opens the configured project for the config file
* reloadForInfo provides a way to filter out files to reload configured project for
*/
private reloadConfiguredProjectForFiles<T>(openFiles: Map<Path, T> | undefined, clearSemanticCache: boolean, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean, reason: string) {
const updatedProjects = new Map<string, true>();
private reloadConfiguredProjectForFiles(reason: string) {
const updatedProjects = new Set<ConfiguredProject>();
const reloadChildProject = (child: ConfiguredProject) => {
if (!updatedProjects.has(child.canonicalConfigFilePath)) {
updatedProjects.set(child.canonicalConfigFilePath, true);
this.reloadConfiguredProject(child, reason, /*isInitialLoad*/ false, clearSemanticCache);
if (tryAddToSet(updatedProjects, child)) {
this.reloadConfiguredProject(child, reason, /*isInitialLoad*/ false, /*clearSemanticCache*/ true);
}
};
// try to reload config file for all open files
openFiles?.forEach((openFileValue, path) => {
this.openFiles?.forEach((_projectRoot, path) => {
// Invalidate default config file name for open file
this.configFileForOpenFiles.delete(path);
// Filter out the files that need to be ignored
if (!shouldReloadProjectFor(openFileValue)) {
return;
}
const info = this.getScriptInfoForPath(path)!; // TODO: GH#18217
Debug.assert(info.isScriptOpen());
@@ -3575,37 +3687,28 @@ export class ProjectService {
const configFileName = this.getConfigFileNameForFile(info);
if (configFileName) {
const project = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
if (!updatedProjects.has(project.canonicalConfigFilePath)) {
updatedProjects.set(project.canonicalConfigFilePath, true);
if (delayReload) {
project.pendingUpdateLevel = ProgramUpdateLevel.Full;
project.pendingUpdateReason = reason;
if (clearSemanticCache) this.clearSemanticCache(project);
this.delayUpdateProjectGraph(project);
}
else {
// reload from the disk
this.reloadConfiguredProject(project, reason, /*isInitialLoad*/ false, clearSemanticCache);
// If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly
if (!projectContainsInfoDirectly(project, info)) {
const referencedProject = forEachResolvedProjectReferenceProject(
if (tryAddToSet(updatedProjects, project)) {
// reload from the disk
this.reloadConfiguredProject(project, reason, /*isInitialLoad*/ false, /*clearSemanticCache*/ true);
// If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly
if (!projectContainsInfoDirectly(project, info)) {
const referencedProject = forEachResolvedProjectReferenceProject(
project,
info.path,
child => {
reloadChildProject(child);
return projectContainsInfoDirectly(child, info);
},
ProjectReferenceProjectLoadKind.FindCreate,
);
if (referencedProject) {
// Reload the project's tree that is already present
forEachResolvedProjectReferenceProject(
project,
info.path,
child => {
reloadChildProject(child);
return projectContainsInfoDirectly(child, info);
},
ProjectReferenceProjectLoadKind.FindCreate,
/*fileName*/ undefined,
reloadChildProject,
ProjectReferenceProjectLoadKind.Find,
);
if (referencedProject) {
// Reload the project's tree that is already present
forEachResolvedProjectReferenceProject(
project,
/*fileName*/ undefined,
reloadChildProject,
ProjectReferenceProjectLoadKind.Find,
);
}
}
}
}
@@ -3656,7 +3759,7 @@ export class ProjectService {
this.printProjects();
this.openFiles.forEach((projectRootPath, path) => {
const info = this.getScriptInfoForPath(path as Path)!;
const info = this.getScriptInfoForPath(path)!;
// collect all orphaned script infos from open files
if (info.isOrphan()) {
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
@@ -3755,10 +3858,7 @@ export class ProjectService {
return originalLocation;
function addOriginalConfiguredProject(originalProject: ConfiguredProject) {
if (!project.originalConfiguredProjects) {
project.originalConfiguredProjects = new Set();
}
project.originalConfiguredProjects.add(originalProject.canonicalConfigFilePath);
(project.originalConfiguredProjects ??= new Set()).add(originalProject.canonicalConfigFilePath);
}
}
@@ -3775,8 +3875,23 @@ export class ProjectService {
});
}
private getOrCreateOpenScriptInfo(fileName: NormalizedPath, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, projectRootPath: NormalizedPath | undefined) {
const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent)!; // TODO: GH#18217
private getOrCreateOpenScriptInfo(
fileName: NormalizedPath,
fileContent: string | undefined,
scriptKind: ScriptKind | undefined,
hasMixedContent: boolean | undefined,
projectRootPath: NormalizedPath | undefined,
) {
const info = this.getOrCreateScriptInfoWorker(
fileName,
projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory,
/*openedByClient*/ true,
fileContent,
scriptKind,
!!hasMixedContent,
/*hostToQueryFileExistsOn*/ undefined,
/*deferredDeleteOk*/ true,
)!;
this.openFiles.set(info.path, projectRootPath);
return info;
}
@@ -3990,7 +4105,7 @@ export class ProjectService {
private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) {
const toRemoveConfiguredProjects = new Map(this.configuredProjects);
const markOriginalProjectsAsUsed = (project: Project) => {
if (!project.isOrphan() && project.originalConfiguredProjects) {
if (project.originalConfiguredProjects && (isConfiguredProject(project) || !project.isOrphan())) {
project.originalConfiguredProjects.forEach(
(_value, configuredProjectPath) => {
const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath);
@@ -4012,16 +4127,14 @@ export class ProjectService {
this.inferredProjects.forEach(markOriginalProjectsAsUsed);
this.externalProjects.forEach(markOriginalProjectsAsUsed);
this.configuredProjects.forEach(project => {
if (!toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) return;
// If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references
if (project.hasOpenRef()) {
retainConfiguredProject(project);
}
else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
// If the configured project for project reference has more than zero references, keep it alive
forEachReferencedProject(
project,
ref => isRetained(ref) && retainConfiguredProject(project),
);
// If the configured project for project reference has more than zero references, keep it alive
else if (forEachReferencedProject(project, ref => isRetained(ref))) {
retainConfiguredProject(project);
}
});
@@ -4029,7 +4142,7 @@ export class ProjectService {
toRemoveConfiguredProjects.forEach(project => this.removeProject(project));
function isRetained(project: ConfiguredProject) {
return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath);
return !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath) || project.hasOpenRef();
}
function retainConfiguredProject(project: ConfiguredProject) {
@@ -4045,14 +4158,15 @@ export class ProjectService {
private removeOrphanScriptInfos() {
const toRemoveScriptInfos = new Map(this.filenameToScriptInfo);
this.filenameToScriptInfo.forEach(info => {
if (info.deferredDelete) return;
// If script info is open or orphan, retain it and its dependencies
if (!info.isScriptOpen() && info.isOrphan() && !info.isContainedByBackgroundProject()) {
// Otherwise if there is any source info that is alive, this alive too
if (!info.sourceMapFilePath) return;
let sourceInfos: Set<Path> | undefined;
if (isString(info.sourceMapFilePath)) {
const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos;
const sourceMapInfo = this.filenameToScriptInfo.get(info.sourceMapFilePath);
sourceInfos = sourceMapInfo?.sourceInfos;
}
else {
sourceInfos = info.sourceMapFilePath.sourceInfos;
@@ -4070,13 +4184,22 @@ export class ProjectService {
// Retain this script info
toRemoveScriptInfos.delete(info.path);
// If we retained declaration file, retain source map and sources as well
if (info.sourceMapFilePath) {
let sourceInfos: Set<Path> | undefined;
if (isString(info.sourceMapFilePath)) {
// And map file info and source infos
toRemoveScriptInfos.delete(info.sourceMapFilePath);
const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos;
const sourceMapInfo = this.filenameToScriptInfo.get(info.sourceMapFilePath);
if (sourceMapInfo?.deferredDelete) {
info.sourceMapFilePath = {
watcher: this.addMissingSourceMapFile(sourceMapInfo.fileName, info.path),
sourceInfos: sourceMapInfo.sourceInfos,
};
}
else {
toRemoveScriptInfos.delete(info.sourceMapFilePath);
}
sourceInfos = sourceMapInfo?.sourceInfos;
}
else {
sourceInfos = info.sourceMapFilePath.sourceInfos;
@@ -4087,12 +4210,8 @@ export class ProjectService {
}
});
toRemoveScriptInfos.forEach(info => {
// if there are not projects that include this script info - delete it
this.stopWatchingScriptInfo(info);
this.deleteScriptInfo(info);
info.closeSourceMapFileWatcher();
});
// if there are not projects that include this script info - delete it
toRemoveScriptInfos.forEach(info => this.deleteScriptInfo(info));
}
private telemetryOnOpenFile(scriptInfo: ScriptInfo): void {
@@ -4142,7 +4261,7 @@ export class ProjectService {
synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] {
const files: ProjectFilesWithTSDiagnostics[] = [];
this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files);
this.collectChanges(knownProjects, this.configuredProjects.values(), includeProjectReferenceRedirectInfo, files);
this.collectChanges(knownProjects, mapDefinedIterator(this.configuredProjects.values(), p => p.deferredClose ? undefined : p), includeProjectReferenceRedirectInfo, files);
this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files);
return files;
}
@@ -4610,28 +4729,28 @@ export class ProjectService {
// Process all pending plugins, partitioned by project. This way a project with few plugins doesn't need to wait
// on a project with many plugins.
await Promise.all(map(pendingPlugins, ([project, promises]) => this.enableRequestedPluginsForProjectAsync(project, promises)));
let sendProjectsUpdatedInBackgroundEvent = false;
await Promise.all(map(pendingPlugins, async ([project, promises]) => {
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded
// prior to patching the language service, and that any promise rejections are observed.
const results = await Promise.all(promises);
if (project.isClosed() || isProjectDeferredClose(project)) {
this.logger.info(`Cancelling plugin enabling for ${project.getProjectName()} as it is ${project.isClosed() ? "closed" : "deferred close"}`);
// project is not alive, so don't enable plugins.
return;
}
sendProjectsUpdatedInBackgroundEvent = true;
for (const result of results) {
this.endEnablePlugin(project, result);
}
// Plugins may have modified external files, so mark the project as dirty.
this.delayUpdateProjectGraph(project);
}));
// Clear the pending operation and notify the client that projects have been updated.
this.currentPluginEnablementPromise = undefined;
this.sendProjectsUpdatedInBackgroundEvent();
}
private async enableRequestedPluginsForProjectAsync(project: Project, promises: Promise<BeginEnablePluginResult>[]) {
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded
// prior to patching the language service, and that any promise rejections are observed.
const results = await Promise.all(promises);
if (project.isClosed()) {
// project is not alive, so don't enable plugins.
return;
}
for (const result of results) {
this.endEnablePlugin(project, result);
}
// Plugins may have modified external files, so mark the project as dirty.
this.delayUpdateProjectGraph(project);
if (sendProjectsUpdatedInBackgroundEvent) this.sendProjectsUpdatedInBackgroundEvent();
}
configurePlugin(args: protocol.ConfigurePluginRequestArguments) {
+42 -3
View File
@@ -657,7 +657,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
private getOrCreateScriptInfoAndAttachToProject(fileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost);
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(
fileName,
this.currentDirectory,
this.directoryStructureHost,
/*deferredDeleteOk*/ false,
);
if (scriptInfo) {
const existingValue = this.rootFilesMap.get(scriptInfo.path);
if (existingValue && existingValue.info !== scriptInfo) {
@@ -678,7 +683,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
getScriptVersion(filename: string) {
// Don't attach to the project if version is asked
const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient(filename, this.currentDirectory, this.directoryStructureHost);
const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient(
filename,
this.currentDirectory,
this.directoryStructureHost,
/*deferredDeleteOk*/ false,
);
return (info && info.getLatestVersion())!; // TODO: GH#18217
}
@@ -1298,6 +1308,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
}
/** @internal */
markAsDirty() {
if (!this.dirty) {
this.projectStateVersion++;
@@ -1658,7 +1669,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
// by the host for files in the program when the program is retrieved above but
// the program doesn't contain external files so this must be done explicitly.
inserted => {
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost);
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(
inserted,
this.currentDirectory,
this.directoryStructureHost,
/*deferredDeleteOk*/ false,
);
scriptInfo?.attachToProject(this);
},
removed => this.detachScriptInfoFromProject(removed),
@@ -2584,6 +2600,7 @@ export class AutoImportProviderProject extends Project {
return !some(this.rootFileNames);
}
/** @internal */
override isOrphan() {
return true;
}
@@ -2619,6 +2636,7 @@ export class AutoImportProviderProject extends Project {
return !!this.rootFileNames?.length;
}
/** @internal */
override markAsDirty() {
this.rootFileNames = undefined;
super.markAsDirty();
@@ -2713,6 +2731,9 @@ export class ConfiguredProject extends Project {
/** @internal */
skipConfigDiagEvent?: true;
/** @internal */
deferredClose?: boolean;
/** @internal */
constructor(
configFileName: NormalizedPath,
@@ -2773,6 +2794,7 @@ export class ConfiguredProject extends Project {
* @returns: true if set of files in the project stays the same and false - otherwise.
*/
override updateGraph(): boolean {
if (this.deferredClose) return false;
const isInitialLoad = this.isInitialLoadPending();
this.isInitialLoadPending = returnFalse;
const updateLevel = this.pendingUpdateLevel;
@@ -2892,6 +2914,12 @@ export class ConfiguredProject extends Project {
super.close();
}
/** @internal */
override markAsDirty() {
if (this.deferredClose) return;
super.markAsDirty();
}
/** @internal */
addExternalProjectReference() {
this.externalProjectRefCount++;
@@ -2941,6 +2969,7 @@ export class ConfiguredProject extends Project {
}
const configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(this.canonicalConfigFilePath)!;
if (this.deferredClose) return !!configFileExistenceInfo.openFilesImpactedByConfigFile?.size;
if (this.projectService.hasPendingProjectUpdate(this)) {
// If there is pending update for this project,
// we dont know if this project would be needed by any of the open files impacted by this config file
@@ -2966,6 +2995,11 @@ export class ConfiguredProject extends Project {
) || false;
}
/** @internal */
override isOrphan(): boolean {
return !!this.deferredClose;
}
/** @internal */
hasExternalProjectRef() {
return !!this.externalProjectRefCount;
@@ -3023,3 +3057,8 @@ export function isExternalProject(project: Project): project is ExternalProject
export function isBackgroundProject(project: Project): project is AutoImportProviderProject | AuxiliaryProject {
return project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary;
}
/** @internal */
export function isProjectDeferredClose(project: Project): project is ConfiguredProject {
return isConfiguredProject(project) && !!project.deferredClose;
}
+19 -2
View File
@@ -25,6 +25,7 @@ import {
IScriptSnapshot,
isString,
LineInfo,
missingFileModifiedTime,
orderedRemoveItem,
Path,
ScriptKind,
@@ -44,6 +45,7 @@ import {
isConfiguredProject,
isExternalProject,
isInferredProject,
isProjectDeferredClose,
maxFileSize,
NormalizedPath,
Project,
@@ -180,6 +182,14 @@ export class TextStorage {
const reloaded = this.reload(newText);
this.fileSize = fileSize; // NB: after reload since reload clears it
this.ownFileText = !tempFileName || tempFileName === this.info.fileName;
// In case we update this text before mTime gets updated to present file modified time
// because its schedule to do that later, update the mTime so we dont re-update the text
// Eg. with npm ci where file gets created and editor calls say get error request before
// the timeout to update the file stamps in node_modules is run
// Test:: watching npm install in codespaces where workspaces folder is hosted at root
if (this.ownFileText && this.info.mTime === missingFileModifiedTime.getTime()) {
this.info.mTime = (this.host.getModifiedTime!(this.info.fileName) || missingFileModifiedTime).getTime();
}
return reloaded;
}
@@ -398,6 +408,9 @@ export class ScriptInfo {
/** @internal */
documentPositionMapper?: DocumentPositionMapper | false;
/** @internal */
deferredDelete?: boolean;
constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
@@ -567,7 +580,10 @@ export class ScriptInfo {
case 0:
return Errors.ThrowNoProject();
case 1:
return ensurePrimaryProjectKind(this.containingProjects[0]);
return ensurePrimaryProjectKind(
!isProjectDeferredClose(this.containingProjects[0]) ?
this.containingProjects[0] : undefined,
);
default:
// If this file belongs to multiple projects, below is the order in which default project is used
// - for open script info, its default configured project during opening is default if info is part of it
@@ -583,6 +599,7 @@ export class ScriptInfo {
for (let index = 0; index < this.containingProjects.length; index++) {
const project = this.containingProjects[index];
if (isConfiguredProject(project)) {
if (project.deferredClose) continue;
if (!project.isSourceOfProjectReferenceRedirect(this.fileName)) {
// If we havent found default configuredProject and
// its not the last one, find it and use that one if there
@@ -676,7 +693,7 @@ export class ScriptInfo {
}
isOrphan() {
return !forEach(this.containingProjects, p => !p.isOrphan());
return this.deferredDelete || !forEach(this.containingProjects, p => !p.isOrphan());
}
/** @internal */
+1
View File
@@ -1575,6 +1575,7 @@ export class Session<TMessage = string> implements EventSender {
fileNameToSearch,
noDtsProject.currentDirectory,
noDtsProject.directoryStructureHost,
/*deferredDeleteOk*/ false,
);
if (!info) continue;
if (!noDtsProject.containsScriptInfo(info)) {
@@ -1,5 +1,6 @@
import * as ts from "../../_namespaces/ts";
import { jsonToReadableText } from "../helpers";
import { compilerOptionsToConfigJson } from "../helpers/contents";
import { ensureErrorFreeBuild } from "../helpers/solutionBuilder";
import {
commonFile1,
@@ -23,13 +24,10 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
it("create configured project without file list", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `
{
"compilerOptions": {},
"exclude": [
"e"
]
}`,
content: jsonToReadableText({
compilerOptions: {},
exclude: ["e"],
}),
};
const file1: File = {
path: "/a/b/c/f1.ts",
@@ -53,11 +51,10 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
it("create configured project with the file list", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `
{
"compilerOptions": {},
"include": ["*.ts"]
}`,
content: jsonToReadableText({
compilerOptions: {},
include: ["*.ts"],
}),
};
const file1: File = {
path: "/a/b/f1.ts",
@@ -81,9 +78,7 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
it("add and then remove a config file in a folder with loose files", () => {
const configFile: File = {
path: `/user/username/projects/myproject/tsconfig.json`,
content: `{
"files": ["commonFile1.ts"]
}`,
content: jsonToReadableText({ files: ["commonFile1.ts"] }),
};
const commonFile1: File = {
path: `/user/username/projects/myproject/commonFile1.ts`,
@@ -97,6 +92,7 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
const host = createServerHost([libFile, commonFile1, commonFile2]);
const session = new TestSession(host);
// 1: when both files are open
openFilesForSession([commonFile1, commonFile2], session);
// Add a tsconfig file
@@ -107,9 +103,139 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
host.deleteFile(configFile.path);
host.runQueuedTimeoutCallbacks(); // Refresh inferred projects
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks(); // load configured project from disk + ensureProjectsForOpenFiles
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// Check status when all files are closed
closeFilesForSession([commonFile1, commonFile2, "/random/random.ts"], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// 2: when file is opened while config file is deleted
closeFilesForSession(["/random/random.ts"], session);
openFilesForSession([commonFile1], session);
// remove the tsconfig file
host.deleteFile(configFile.path);
openFilesForSession([commonFile2], session);
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks(); // load configured project from disk + ensureProjectsForOpenFiles
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// Check status when all files are closed
closeFilesForSession([commonFile1, commonFile2, "/random/random.ts"], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
baselineTsserverLogs("configuredProjects", "add and then remove a config file in a folder with loose files", session);
});
it("add and then remove a config file when parent folder has config file", () => {
const configFile: File = {
path: `/user/username/projects/myproject/folder/tsconfig.json`,
content: jsonToReadableText({ files: ["commonFile1.ts"] }),
};
const parentConfigFile: File = {
path: `/user/username/projects/myproject/tsconfig.json`,
content: jsonToReadableText({ files: ["folder/commonFile2.ts"] }),
};
const commonFile1: File = {
path: `/user/username/projects/myproject/folder/commonFile1.ts`,
content: "let x = 1",
};
const commonFile2: File = {
path: `/user/username/projects/myproject/folder/commonFile2.ts`,
content: "let y = 1",
};
const host = createServerHost([libFile, commonFile1, commonFile2, configFile, parentConfigFile]);
const session = new TestSession(host);
// 1: When config file is deleted and then another file is opened
openFilesForSession([commonFile1], session);
// remove the tsconfig file
host.deleteFile(configFile.path);
openFilesForSession([commonFile2], session);
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks();
// Check the state after files collected
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// Check status when all files are closed
closeFilesForSession([commonFile1, commonFile2, "/random/random.ts"], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// 2: When both files are open and config file is deleted
closeFilesForSession(["/random/random.ts"], session);
openFilesForSession([commonFile1, commonFile2], session);
// remove the tsconfig file
host.deleteFile(configFile.path);
host.runQueuedTimeoutCallbacks();
// Add a tsconfig file
host.writeFile(configFile.path, configFile.content);
host.runQueuedTimeoutCallbacks(); // load configured project from disk + ensureProjectsForOpenFiles
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
closeFilesForSession([commonFile1, commonFile2, "/random/random.ts"], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// 3: Check when both files are closed when config file is deleted
closeFilesForSession(["/random/random.ts"], session);
openFilesForSession([commonFile1], session);
// remove the tsconfig file
host.deleteFile(configFile.path);
openFilesForSession([commonFile2], session);
// State after open files are closed
closeFilesForSession([commonFile1, commonFile2], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// 4: Check when both files are closed one by one when file is deleted
host.writeFile(configFile.path, configFile.content);
closeFilesForSession(["/random/random.ts"], session);
openFilesForSession([commonFile1], session);
// remove the tsconfig file
host.deleteFile(configFile.path);
openFilesForSession([commonFile2], session);
// State after open files are closed
closeFilesForSession([commonFile1], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
closeFilesForSession([commonFile2, "random/random.ts"], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
// 5: Check when both files are closed one by one when file is deleted order changed
host.writeFile(configFile.path, configFile.content);
closeFilesForSession(["/random/random.ts"], session);
openFilesForSession([commonFile1], session);
// remove the tsconfig file
host.deleteFile(configFile.path);
openFilesForSession([commonFile2], session);
// State after open files are closed
closeFilesForSession([commonFile2], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
closeFilesForSession([commonFile1, "random/random.ts"], session);
openFilesForSession([{ file: "/random/random.ts", content: "export const y = 10;" }], session);
baselineTsserverLogs("configuredProjects", "add and then remove a config file when parent folder has config file", session);
});
it("add new files to a configured project without file list", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
@@ -128,13 +254,13 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
it("should ignore non-existing files specified in the config file", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {},
"files": [
"commonFile1.ts",
"commonFile3.ts"
]
}`,
content: jsonToReadableText({
compilerOptions: {},
files: [
"commonFile1.ts",
"commonFile3.ts",
],
}),
};
const host = createServerHost([commonFile1, commonFile2, configFile]);
const session = new TestSession(host);
@@ -164,10 +290,10 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
it("files explicitly excluded in config file", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {},
"exclude": ["/a/c"]
}`,
content: jsonToReadableText({
compilerOptions: {},
exclude: ["/a/c"],
}),
};
const excludedFile1: File = {
path: "/a/c/excluedFile1.ts",
@@ -200,12 +326,10 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"moduleResolution": "node"
},
"files": ["${file1.path}"]
}`,
content: jsonToReadableText({
compilerOptions: compilerOptionsToConfigJson({ moduleResolution: ts.ModuleResolutionKind.Node10 }),
files: [file1.path],
}),
};
const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile];
const host = createServerHost(files);
@@ -214,12 +338,10 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
host.writeFile(
configFile.path,
`{
"compilerOptions": {
"moduleResolution": "classic"
},
"files": ["${file1.path}"]
}`,
jsonToReadableText({
compilerOptions: compilerOptionsToConfigJson({ moduleResolution: ts.ModuleResolutionKind.Classic }),
files: [file1.path],
}),
);
host.runQueuedTimeoutCallbacks();
@@ -240,12 +362,12 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"target": "es6"
},
"files": [ "main.ts" ]
}`,
content: jsonToReadableText({
compilerOptions: {
target: "es6",
},
files: ["main.ts"],
}),
};
const host = createServerHost([file1, file2, configFile]);
const session = new TestSession(host);
@@ -258,13 +380,13 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
it("should tolerate config file errors and still try to build a project", () => {
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"target": "es6",
"allowAnything": true
},
"someOtherProperty": {}
}`,
content: jsonToReadableText({
compilerOptions: {
target: "es6",
allowAnything: true,
},
someOtherProperty: {},
}),
};
const host = createServerHost([commonFile1, commonFile2, libFile, configFile]);
const session = new TestSession(host);
@@ -283,12 +405,12 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"target": "es6"
},
"files": [ "main.ts", "main2.ts" ]
}`,
content: jsonToReadableText({
compilerOptions: {
target: "es6",
},
files: ["main.ts", "main2.ts"],
}),
};
const host = createServerHost([file1, file2, configFile, libFile]);
const session = new TestSession({ host, useSingleInferredProject: true });
@@ -305,12 +427,12 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"target": "es6"
},
"files": [ "main.ts" ]
}`,
content: jsonToReadableText({
compilerOptions: {
target: "es6",
},
files: ["main.ts"],
}),
};
const host = createServerHost([file1, configFile, libFile]);
const session = new TestSession({ host, useSingleInferredProject: true });
@@ -1,5 +1,8 @@
import * as ts from "../../_namespaces/ts";
import { defer } from "../../_namespaces/Utils";
import {
defer,
Deferred,
} from "../../_namespaces/Utils";
import {
baselineTsserverLogs,
closeFilesForSession,
@@ -22,9 +25,11 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
const { host, session } = setup(["plugin-a"]);
let pluginModuleInstantiated = false;
let pluginInvoked = false;
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
session.logger.log(`request import ${moduleName}`);
await Promise.resolve(); // simulate at least a single turn delay
pluginModuleInstantiated = true;
session.logger.log(`fulfill import ${moduleName}`);
return {
module: (() => {
pluginInvoked = true;
@@ -47,6 +52,7 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
session.logger.log(`pluginModuleInstantiated:: ${pluginModuleInstantiated}`);
await projectService.waitForPendingPlugins();
session.host.baselineHost("after waitForPendingPlugins");
session.logger.log(`at this point all plugin modules should have been instantiated and all plugins should have been invoked`);
session.logger.log(`pluginModuleInstantiated:: ${pluginModuleInstantiated}`);
@@ -85,14 +91,17 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
// wait for load to complete
await projectService.waitForPendingPlugins();
session.host.baselineHost("after waitForPendingPlugins ");
baselineTsserverLogs("pluginsAsync", "plugins evaluation in correct order even if imports resolve out of order", session);
});
it("sends projectsUpdatedInBackground event", async () => {
const { host, session } = setup(["plugin-a"]);
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
session.logger.log(`request import ${moduleName}`);
await Promise.resolve(); // simulate at least a single turn delay
session.logger.log(`fulfill import ${moduleName}`);
return {
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
error: undefined,
@@ -103,6 +112,7 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
const projectService = session.getProjectService();
await projectService.waitForPendingPlugins();
session.host.baselineHost("after waitForPendingPlugins");
baselineTsserverLogs("pluginsAsync", "sends projectsUpdatedInBackground event", session);
});
@@ -110,10 +120,11 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
it("adds external files", async () => {
const { host, session } = setup(["plugin-a"]);
const pluginAShouldLoad = defer();
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
session.logger.log(`request import ${moduleName}`);
// wait until the initial external files are requested from the project service.
await pluginAShouldLoad.promise;
session.logger.log(`fulfill import ${moduleName}`);
return {
module: (() => ({
create: info => info.languageService,
@@ -145,12 +156,16 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
const { host, session } = setup(["plugin-a"]);
const pluginALoaded = defer();
const projectClosed = defer();
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
session.logger.log(`request import ${moduleName}`);
// mark that the plugin has started loading
pluginALoaded.resolve();
// wait until after a project close has been requested to continue
session.logger.log(`Awaiting project close`);
await projectClosed.promise;
session.logger.log(`fulfill import ${moduleName}`);
return {
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
error: undefined,
@@ -165,13 +180,75 @@ describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
// close the project
closeFilesForSession(["^memfs:/foo.ts"], session);
openFilesForSession([{ file: "/random/foo2.ts", content: "" }], session);
// continue loading the plugin
projectClosed.resolve();
await projectService.waitForPendingPlugins();
session.host.baselineHost("before waitForPendingPlugins");
await projectService.waitForPendingPlugins(); // For closed foo.ts
session.host.baselineHost("after waitForPendingPlugins for closed foo.ts");
await projectService.waitForPendingPlugins(); // For random file
session.host.baselineHost("after waitForPendingPlugins for random file");
// the project was closed before plugins were ready. no project update should have been requested
baselineTsserverLogs("pluginsAsync", "project is closed before plugins are loaded", session);
});
it("project is deferred closed before plugins are loaded", async () => {
const config = "/home/src/projects/project/tsconfig.json";
const file = "/home/src/projects/project/a.ts";
const host = createServerHost({
[config]: `{}`,
[file]: "export const a = 10;",
[libFile.path]: libFile.content,
});
const session = new TestSession({ host, globalPlugins: ["plugin-a"] });
const pluginALoaded = defer();
let configFileDeleted: Deferred<void> | undefined = defer();
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
session.logger.log(`request import ${moduleName}`);
// mark that the plugin has started loading
pluginALoaded.resolve();
// wait until after a project close has been requested to continue
if (configFileDeleted) {
session.logger.log(`awaiting config file delete`);
await configFileDeleted.promise;
}
session.logger.log(`fulfill import ${moduleName}`);
return {
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
error: undefined,
};
};
openFilesForSession([file], session);
const projectService = session.getProjectService();
// wait for the plugin to start loading
await pluginALoaded.promise;
session.host.baselineHost("before deleteFile");
// close the project
host.deleteFile(config);
session.host.baselineHost("after deleteFile");
// continue loading the plugin
configFileDeleted.resolve();
session.host.baselineHost("before waitForPendingPlugins");
await projectService.waitForPendingPlugins();
session.host.baselineHost("after waitForPendingPlugins");
configFileDeleted = undefined;
host.writeFile(config, "{}");
host.runQueuedTimeoutCallbacks();
session.host.baselineHost("before enableRequestedPlugins");
projectService.enableRequestedPlugins();
session.host.baselineHost("before waitForPendingPlugins");
await projectService.waitForPendingPlugins();
session.host.baselineHost("after waitForPendingPlugins");
// the project was closed before plugins were ready. no project update should have been requested
baselineTsserverLogs("pluginsAsync", "project is deferred closed before plugins are loaded", session);
});
});
@@ -1073,11 +1073,12 @@ export function bar() {}`,
}
function verifySolutionScenario(input: Setup) {
const { session } = setup(input);
const { session, host } = setup(input);
const info = session.getProjectService().getScriptInfoForPath(main.path as ts.Path)!;
const defaultProject = info.getDefaultProject();
session.logger.startGroup();
session.logger.info(`getDefaultProject for ${main.path}: ${info.getDefaultProject().projectName}`);
session.logger.info(`getDefaultProject for ${main.path}: ${defaultProject.projectName}`);
session.logger.info(`findDefaultConfiguredProject for ${main.path}: ${session.getProjectService().findDefaultConfiguredProject(info)!.projectName}`);
session.logger.endGroup();
@@ -1093,6 +1094,24 @@ export function bar() {}`,
closeFilesForSession([dummyFilePath], session);
openFilesForSession([dummyFilePath], session);
// Verify that tsconfig can be deleted and watched
if (ts.server.isConfiguredProject(defaultProject)) {
closeFilesForSession([dummyFilePath], session);
const config = defaultProject.projectName;
const content = host.readFile(config)!;
host.deleteFile(config);
host.runQueuedTimeoutCallbacks();
host.writeFile(config, content);
host.runQueuedTimeoutCallbacks();
host.deleteFile(config);
openFilesForSession([dummyFilePath], session);
host.writeFile(config, content);
host.runQueuedTimeoutCallbacks();
}
// Verify Reload projects
session.executeCommandSeq<ts.server.protocol.ReloadProjectsRequest>({
command: ts.server.protocol.CommandTypes.ReloadProjects,
@@ -15,7 +15,7 @@ import {
TestServerHost,
} from "../helpers/virtualFileSystemWithWatch";
describe("unittests:: tsserver:: with project references and tsbuild source map", () => {
describe("unittests:: tsserver:: projectReferencesSourcemap:: with project references and tsbuild source map", () => {
const dependecyLocation = `/user/username/projects/myproject/dependency`;
const dependecyDeclsLocation = `/user/username/projects/myproject/decls`;
const mainLocation = `/user/username/projects/myproject/main`;
@@ -71,10 +71,10 @@ fn5();
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig];
function changeDtsFile(session: TestSession) {
function changeDtsFile(session: TestSession, content?: string) {
session.host.writeFile(
dtsLocation,
session.host.readFile(dtsLocation)!.replace(
(content ?? session.host.readFile(dtsLocation)!).replace(
"//# sourceMappingURL=FnS.d.ts.map",
`export declare function fn6(): void;
//# sourceMappingURL=FnS.d.ts.map`,
@@ -147,14 +147,18 @@ fn5();
function createSessionWithoutProjectReferences(onHostCreate?: OnHostCreate) {
const host = createHostWithSolutionBuild(files, [mainConfig.path]);
// Erase project reference
writeConfigWithoutProjectReferences(host);
onHostCreate?.(host);
return new TestSession(host);
}
function writeConfigWithoutProjectReferences(host: TestServerHost) {
host.writeFile(
mainConfig.path,
jsonToReadableText({
compilerOptions: { composite: true, declarationMap: true },
}),
);
onHostCreate?.(host);
return new TestSession(host);
}
function createSessionWithProjectReferences(onHostCreate?: OnHostCreate) {
@@ -166,6 +170,12 @@ fn5();
function createSessionWithDisabledProjectReferences(onHostCreate?: OnHostCreate) {
const host = createHostWithSolutionBuild(files, [mainConfig.path]);
// Erase project reference
WithDisabledProjectReferences(host);
onHostCreate?.(host);
return new TestSession(host);
}
function WithDisabledProjectReferences(host: TestServerHost) {
host.writeFile(
mainConfig.path,
jsonToReadableText({
@@ -177,8 +187,6 @@ fn5();
references: [{ path: "../dependency" }],
}),
);
onHostCreate?.(host);
return new TestSession(host);
}
function makeChangeToMainTs(session: TestSession) {
@@ -209,12 +217,28 @@ fn5();
});
}
function setup(type: SessionType, openFiles: readonly File[], action: Action | Action[], max?: number, onHostCreate?: OnHostCreate) {
const session = type === SessionType.NoReference ? createSessionWithoutProjectReferences(onHostCreate) :
function verifyForAllSessionTypes<T>(worker: (type: SessionType, options: T) => void, options: T) {
describe("when main tsconfig doesnt have project reference", () => {
worker(SessionType.NoReference, options);
});
describe("when main tsconfig has project reference", () => {
worker(SessionType.ProjectReference, options);
});
describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => {
worker(SessionType.DisableSourceOfProjectReferenceRedirect, options);
});
}
function createSession(type: SessionType, onHostCreate?: OnHostCreate) {
return type === SessionType.NoReference ? createSessionWithoutProjectReferences(onHostCreate) :
type === SessionType.ProjectReference ? createSessionWithProjectReferences(onHostCreate) :
type === SessionType.DisableSourceOfProjectReferenceRedirect ?
createSessionWithDisabledProjectReferences(onHostCreate) :
ts.Debug.assertNever(type);
}
function setup(type: SessionType, openFiles: readonly File[], action: Action | Action[], max?: number, onHostCreate?: OnHostCreate) {
const session = createSession(type, onHostCreate);
openFilesForSession(openFiles, session);
runActions(session, action, max);
return session;
@@ -275,10 +299,60 @@ fn5();
openFiles: readonly File[];
action: Action | Action[];
}
function verifyFileRenames(options: VerifyFileRenamesOptions) {
function verifyFileChangeAndRenames(options: VerifyFileRenamesOptions) {
function file(options: VerifyFileRenamesOptions) {
return options.file === "dts" ? dtsLocation : dtsMapLocation;
}
enum ChangeAsRenameType {
NoTimeout = "no timeout",
TimeoutAfterDelete = "timeout after delete",
TimeoutAfterWrite = "timeout after write",
ActionBeforeWrite = "action before write",
}
function change(options: VerifyFileRenamesOptions) {
return options.file === "dts" ? changeDtsFile : changeDtsMapFile;
}
function verifyChangeAsRename(withChange: boolean) {
[
ChangeAsRenameType.NoTimeout,
ChangeAsRenameType.TimeoutAfterDelete,
ChangeAsRenameType.TimeoutAfterWrite,
ChangeAsRenameType.ActionBeforeWrite,
].forEach(changeAsRenameType => {
it(`with ${options.file} file, change as rename ${changeAsRenameType}`, () => {
const session = setup(options.type, options.openFiles, options.action, 1);
const location = file(options);
const fileContents = session.host.readFile(location)!;
session.host.deleteFile(location);
switch (changeAsRenameType) {
case ChangeAsRenameType.TimeoutAfterDelete:
session.host.runQueuedTimeoutCallbacks();
break;
case ChangeAsRenameType.ActionBeforeWrite:
runActions(session, options.action, 2);
break;
default:
session.host.baselineHost(`Before write ${location}`);
}
if (withChange) change(options)(session, fileContents);
else session.host.writeFile(location, fileContents);
if (changeAsRenameType === ChangeAsRenameType.TimeoutAfterWrite) session.host.runQueuedTimeoutCallbacks();
runActions(session, options.action, 2);
baselineTsserverLogs("projectReferencesSourcemap", `${options.scenarioLocation}/${options.type}/dependency ${options.file} ${withChange ? "change" : "rewrite"} as rename ${changeAsRenameType}`, session);
});
});
}
// Edit to add new fn
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type: options.type,
scenario: `dependency ${options.file} changes`,
openFiles: options.openFiles,
change: change(options),
action: options.action,
});
verifyChangeAsRename(/*withChange*/ true);
verifyChangeAsRename(/*withChange*/ false);
it(`with ${options.file} file, when file is not present`, () => {
const session = setup(options.type, options.openFiles, options.action, undefined, host => host.deleteFile(file(options)));
verifyScriptInfoCollectionWith(session, options.openFiles);
@@ -314,93 +388,63 @@ fn5();
referenceChange: (session: TestSession) => void;
referenceChangeAction?: Action | Action[];
}
function verifyScenarioWorker(options: VerifyScenario, type: SessionType) {
verifyAction({
scenarioLocation: options.scenarioLocation,
type,
scenario: options.scenario,
openFiles: options.openFiles,
action: options.action,
});
// Edit
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type,
scenario: "usage file changes",
openFiles: options.openFiles,
change: options.change,
action: options.action,
});
// Edit dts to add new fn
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type,
scenario: "dependency dts changes",
openFiles: options.openFiles,
change: changeDtsFile,
action: options.action,
});
// Edit map file to represent added new line
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type,
scenario: "dependency dtsMap changes",
openFiles: options.openFiles,
change: changeDtsMapFile,
action: options.action,
});
verifyFileRenames({
scenarioLocation: options.scenarioLocation,
type,
file: "dtsMap",
openFiles: options.openFiles,
action: options.action,
});
verifyFileRenames({
scenarioLocation: options.scenarioLocation,
type,
file: "dts",
openFiles: options.openFiles,
action: options.action,
});
if (type !== SessionType.ProjectReference) return;
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type,
scenario: "dependency source changes",
openFiles: options.openFiles,
change: options.referenceChange,
action: options.action,
actionAfterChange: options.referenceChangeAction,
});
it("when projects are not built", () => {
const host = createServerHost(files);
const session = new TestSession(host);
openFilesForSession(options.openFiles, session);
runActions(session, options.action);
verifyScriptInfoCollectionWith(session, options.openFiles);
baselineTsserverLogs("projectReferencesSourcemap", `${options.scenarioLocation}/${type}/when projects are not built`, session);
});
}
function verifyScenario(options: VerifyScenario) {
describe("when main tsconfig doesnt have project reference", () => {
verifyScenarioWorker(options, SessionType.NoReference);
});
describe("when main tsconfig has project reference", () => {
verifyScenarioWorker(options, SessionType.ProjectReference);
});
describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => {
verifyScenarioWorker(options, SessionType.DisableSourceOfProjectReferenceRedirect);
});
verifyForAllSessionTypes((type, options) => {
verifyAction({
scenarioLocation: options.scenarioLocation,
type,
scenario: options.scenario,
openFiles: options.openFiles,
action: options.action,
});
// Edit
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type,
scenario: "usage file changes",
openFiles: options.openFiles,
change: options.change,
action: options.action,
});
verifyFileChangeAndRenames({
scenarioLocation: options.scenarioLocation,
type,
file: "dtsMap",
openFiles: options.openFiles,
action: options.action,
});
verifyFileChangeAndRenames({
scenarioLocation: options.scenarioLocation,
type,
file: "dts",
openFiles: options.openFiles,
action: options.action,
});
if (type !== SessionType.ProjectReference) return;
verifyScenarioWithChanges({
scenarioLocation: options.scenarioLocation,
type,
scenario: "dependency source changes",
openFiles: options.openFiles,
change: options.referenceChange,
action: options.action,
actionAfterChange: options.referenceChangeAction,
});
it("when projects are not built", () => {
const host = createServerHost(files);
const session = new TestSession(host);
openFilesForSession(options.openFiles, session);
runActions(session, options.action);
verifyScriptInfoCollectionWith(session, options.openFiles);
baselineTsserverLogs("projectReferencesSourcemap", `${options.scenarioLocation}/${type}/when projects are not built`, session);
});
}, options);
}
describe("from project that uses dependency: goToDef", () => {
@@ -465,5 +509,46 @@ fn5();
}),
referenceChangeAction: [goToDefFromMainTs, renameFromDependencyTsWithDependencyChange],
});
verifyForAllSessionTypes(type => {
it("goto Definition in usage and rename locations, deleting config file", () => {
const session = createSession(type);
openFilesForSession([mainTs], session);
session.executeCommandSeq<ts.server.protocol.RenameRequest>({
command: ts.server.protocol.CommandTypes.Rename,
arguments: { file: mainTs.path, line: 2, offset: 17 },
});
verifyMainConfigDelete(mainConfig, /*runTimeoutAfterDelete*/ true, /*openRandomAfterDelete*/ false);
verifyMainConfigDelete(mainConfig, /*runTimeoutAfterDelete*/ true, /*openRandomAfterDelete*/ true);
verifyMainConfigDelete(mainConfig, /*runTimeoutAfterDelete*/ false, /*openRandomAfterDelete*/ false);
verifyMainConfigDelete(mainConfig, /*runTimeoutAfterDelete*/ false, /*openRandomAfterDelete*/ true);
verifyMainConfigDelete(dependencyConfig, /*runTimeoutAfterDelete*/ true, /*openRandomAfterDelete*/ false);
verifyMainConfigDelete(dependencyConfig, /*runTimeoutAfterDelete*/ false, /*openRandomAfterDelete*/ true);
verifyMainConfigDelete(dependencyConfig, /*runTimeoutAfterDelete*/ true, /*openRandomAfterDelete*/ true);
verifyMainConfigDelete(dependencyConfig, /*runTimeoutAfterDelete*/ false, /*openRandomAfterDelete*/ false);
baselineTsserverLogs("projectReferencesSourcemap", `dependencyAndUsage/${type}/goToDef and rename locations and deleting config file`, session);
function verifyMainConfigDelete(
config: File,
runTimeoutAfterDelete: boolean,
openRandomAfterDelete: boolean,
) {
const configContent = session.host.readFile(config.path)!;
session.host.deleteFile(config.path);
if (runTimeoutAfterDelete) session.host.runQueuedTimeoutCallbacks();
if (openRandomAfterDelete) {
openFilesForSession([randomFile], session);
closeFilesForSession([randomFile], session);
}
session.host.writeFile(config.path, configContent);
session.host.runQueuedTimeoutCallbacks();
openFilesForSession([randomFile], session);
closeFilesForSession([randomFile], session);
}
});
}, /*options*/ undefined);
});
});