mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Keep scriptInfo and project alive even after file delete till next file open (#57492)
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user