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:
+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)) {
|
||||
|
||||
Reference in New Issue
Block a user