diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 76d242e8b08..088733e21ef 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1394,6 +1394,10 @@ namespace ts { return SpecialPropertyAssignmentKind.None; } + export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration { + return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral; + } + export function getExternalModuleName(node: Node): Expression { if (node.kind === SyntaxKind.ImportDeclaration) { return (node).moduleSpecifier; @@ -1407,8 +1411,8 @@ namespace ts { if (node.kind === SyntaxKind.ExportDeclaration) { return (node).moduleSpecifier; } - if (node.kind === SyntaxKind.ModuleDeclaration && (node).name.kind === SyntaxKind.StringLiteral) { - return (node).name; + if (isModuleWithStringLiteralName(node)) { + return node.name; } } diff --git a/src/server/builder.ts b/src/server/builder.ts index 8a10682b4cc..285ab9ece60 100644 --- a/src/server/builder.ts +++ b/src/server/builder.ts @@ -3,24 +3,130 @@ /// namespace ts.server { - export function shouldEmitFile(scriptInfo: ScriptInfo) { return !scriptInfo.hasMixedContent; } - /** - * An abstract file info that maintains a shape signature. - */ - export class BuilderFileInfo { + export interface Builder { + /** + * This is the callback when file infos in the builder are updated + */ + onProjectUpdateGraph(): void; + getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; + /** + * @returns {boolean} whether the emit was conducted or not + */ + emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; + clear(): void; + } - private lastCheckedShapeSignature: string; + interface EmitHandler { + addScriptInfo(scriptInfo: ScriptInfo): void; + removeScriptInfo(path: Path): void; + updateScriptInfo(scriptInfo: ScriptInfo): void; + /** + * Gets the files affected by the script info which has updated shape from the known one + */ + getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[]; + } - constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) { + export function createBuilder(project: Project): Builder { + let isModuleEmit: boolean | undefined; + let projectVersionForDependencyGraph: string; + // Last checked shape signature for the file info + let fileInfos: Map; + let emitHandler: EmitHandler; + return { + onProjectUpdateGraph, + getFilesAffectedBy, + emitFile, + clear + }; + + function createProjectGraph() { + const currentIsModuleEmit = project.getCompilerOptions().module !== ModuleKind.None; + if (isModuleEmit !== currentIsModuleEmit) { + isModuleEmit = currentIsModuleEmit; + emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); + fileInfos = undefined; + } + + fileInfos = mutateExistingMap( + fileInfos, arrayToMap(project.getScriptInfos(), info => info.path), + (_path, info) => { + emitHandler.addScriptInfo(info); + return ""; + }, + (path: Path, _value) => emitHandler.removeScriptInfo(path), + /*isSameValue*/ undefined, + /*OnDeleteExistingMismatchValue*/ undefined, + (_prevValue, scriptInfo) => emitHandler.updateScriptInfo(scriptInfo) + ); + projectVersionForDependencyGraph = project.getProjectVersion(); } - public isExternalModuleOrHasOnlyAmbientExternalModules() { - const sourceFile = this.getSourceFile(); - return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile); + function ensureFileInfos() { + if (!emitHandler) { + createProjectGraph(); + } + Debug.assert(projectVersionForDependencyGraph === project.getProjectVersion()); + } + + function onProjectUpdateGraph() { + if (emitHandler) { + createProjectGraph(); + } + } + + function getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { + ensureFileInfos(); + + const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; + const path = scriptInfo.path; + if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(scriptInfo)) { + return singleFileResult; + } + + return emitHandler.getFilesAffectedByUpdatedShape(scriptInfo, singleFileResult); + } + + /** + * @returns {boolean} whether the emit was conducted or not + */ + function emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { + ensureFileInfos(); + if (!fileInfos || !fileInfos.has(scriptInfo.path)) { + return false; + } + + const { emitSkipped, outputFiles } = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ false); + if (!emitSkipped) { + const projectRootPath = project.getProjectRootPath(); + for (const outputFile of outputFiles) { + const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); + writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); + } + } + return !emitSkipped; + } + + function clear() { + isModuleEmit = undefined; + emitHandler = undefined; + fileInfos = undefined; + projectVersionForDependencyGraph = undefined; + } + + function getSourceFile(path: Path) { + return project.getSourceFile(path); + } + + function getScriptInfo(path: Path) { + return project.projectService.getScriptInfoForPath(path); + } + + function isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile: SourceFile) { + return sourceFile && (isExternalModule(sourceFile) || containsOnlyAmbientModules(sourceFile)); } /** @@ -29,331 +135,149 @@ namespace ts.server { * there are no point to rebuild all script files if these special files have changed. However, if any statement * in the file is not ambient external module, we treat it as a regular script file. */ - private containsOnlyAmbientModules(sourceFile: SourceFile) { + function containsOnlyAmbientModules(sourceFile: SourceFile) { for (const statement of sourceFile.statements) { - if (statement.kind !== SyntaxKind.ModuleDeclaration || (statement).name.kind !== SyntaxKind.StringLiteral) { + if (!isModuleWithStringLiteralName(statement)) { return false; } } return true; } - private computeHash(text: string): string { - return this.project.projectService.host.createHash(text); - } - - private getSourceFile(): SourceFile { - return this.project.getSourceFile(this.scriptInfo.path); - } - /** * @return {boolean} indicates if the shape signature has changed since last update. */ - public updateShapeSignature() { - const sourceFile = this.getSourceFile(); + function updateShapeSignature(scriptInfo: ScriptInfo) { + const path = scriptInfo.path; + const sourceFile = getSourceFile(path); if (!sourceFile) { return true; } - const lastSignature = this.lastCheckedShapeSignature; + const prevSignature = fileInfos.get(path); + let latestSignature = prevSignature; if (sourceFile.isDeclarationFile) { - this.lastCheckedShapeSignature = this.computeHash(sourceFile.text); + latestSignature = computeHash(sourceFile.text); + fileInfos.set(path, latestSignature); } else { - const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true); + const emitOutput = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ true); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { - this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text); + latestSignature = computeHash(emitOutput.outputFiles[0].text); + fileInfos.set(path, latestSignature); } } - return !lastSignature || this.lastCheckedShapeSignature !== lastSignature; - } - } - export interface Builder { - readonly project: Project; - getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - onProjectUpdateGraph(): void; - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; - clear(): void; - } - - abstract class AbstractBuilder implements Builder { - - /** - * stores set of files from the project. - * NOTE: this field is created on demand and should not be accessed directly. - * Use 'getFileInfos' instead. - */ - private fileInfos_doNotAccessDirectly: Map; - - constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) { + return !prevSignature || latestSignature !== prevSignature; } - private getFileInfos() { - return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createMap()); + function computeHash(text: string) { + return project.projectService.host.createHash(text); } - protected hasFileInfos() { - return !!this.fileInfos_doNotAccessDirectly; - } + function noop() { } - public clear() { - // drop the existing list - it will be re-created as necessary - this.fileInfos_doNotAccessDirectly = undefined; - } + function getNonModuleEmitHandler(): EmitHandler { + return { + addScriptInfo: noop, + removeScriptInfo: noop, + updateScriptInfo: noop, + getFilesAffectedByUpdatedShape + }; - protected getFileInfo(path: Path): T { - return this.getFileInfos().get(path); - } - - protected getOrCreateFileInfo(path: Path): T { - let fileInfo = this.getFileInfo(path); - if (!fileInfo) { - const scriptInfo = this.project.getScriptInfo(path); - fileInfo = new this.ctor(scriptInfo, this.project); - this.setFileInfo(path, fileInfo); - } - return fileInfo; - } - - protected getFileInfoPaths(): Path[] { - return arrayFrom(this.getFileInfos().keys() as Iterator); - } - - protected setFileInfo(path: Path, info: T) { - this.getFileInfos().set(path, info); - } - - protected removeFileInfo(path: Path) { - this.getFileInfos().delete(path); - } - - protected forEachFileInfo(action: (fileInfo: T) => any) { - this.getFileInfos().forEach(action); - } - - abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - abstract onProjectUpdateGraph(): void; - protected abstract ensureFileInfoIfInProject(scriptInfo: ScriptInfo): void; - - /** - * @returns {boolean} whether the emit was conducted or not - */ - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { - this.ensureFileInfoIfInProject(scriptInfo); - const fileInfo = this.getFileInfo(scriptInfo.path); - if (!fileInfo) { - return false; - } - - const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false); - if (!emitSkipped) { - const projectRootPath = this.project.getProjectRootPath(); - for (const outputFile of outputFiles) { - const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); - writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); - } - } - return !emitSkipped; - } - } - - class NonModuleBuilder extends AbstractBuilder { - - constructor(public readonly project: Project) { - super(project, BuilderFileInfo); - } - - protected ensureFileInfoIfInProject(scriptInfo: ScriptInfo) { - if (this.project.containsScriptInfo(scriptInfo)) { - this.getOrCreateFileInfo(scriptInfo.path); - } - } - - onProjectUpdateGraph() { - if (this.hasFileInfos()) { - this.forEachFileInfo(fileInfo => { - if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) { - // This file was deleted from this project - this.removeFileInfo(fileInfo.scriptInfo.path); - } - }); - } - } - - /** - * Note: didn't use path as parameter because the returned file names will be directly - * consumed by the API user, which will use it to interact with file systems. Path - * should only be used internally, because the case sensitivity is not trustable. - */ - getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - const info = this.getOrCreateFileInfo(scriptInfo.path); - const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; - if (info.updateShapeSignature()) { - const options = this.project.getCompilerOptions(); + function getFilesAffectedByUpdatedShape(_scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { + const options = project.getCompilerOptions(); // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, // so returning the file itself is good enough. if (options && (options.out || options.outFile)) { return singleFileResult; } - return this.project.getAllEmittableFiles(); - } - return singleFileResult; - } - } - - class ModuleBuilderFileInfo extends BuilderFileInfo { - references = createSortedArray(); - readonly referencedBy = createSortedArray(); - scriptVersionForReferences: string; - - static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): Comparison { - return compareStrings(lf.scriptInfo.fileName, rf.scriptInfo.fileName); - } - - addReferencedBy(fileInfo: ModuleBuilderFileInfo): void { - insertSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos); - } - - removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void { - removeSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos); - } - - removeFileReferences() { - for (const reference of this.references) { - reference.removeReferencedBy(this); - } - clear(this.references); - } - } - - class ModuleBuilder extends AbstractBuilder { - - constructor(public readonly project: Project) { - super(project, ModuleBuilderFileInfo); - } - - private projectVersionForDependencyGraph: string; - - public clear() { - this.projectVersionForDependencyGraph = undefined; - super.clear(); - } - - private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): SortedArray { - if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { - return createSortedArray(); - } - - const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path); - return toSortedArray(referencedFilePaths.map(f => this.getOrCreateFileInfo(f)), ModuleBuilderFileInfo.compareFileInfos); - } - - protected ensureFileInfoIfInProject(_scriptInfo: ScriptInfo) { - this.ensureProjectDependencyGraphUpToDate(); - } - - onProjectUpdateGraph() { - // Update the graph only if we have computed graph earlier - if (this.hasFileInfos()) { - this.ensureProjectDependencyGraphUpToDate(); + return project.getAllEmittableFiles(); } } - private ensureProjectDependencyGraphUpToDate() { - if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) { - const currentScriptInfos = this.project.getScriptInfos(); - for (const scriptInfo of currentScriptInfos) { - const fileInfo = this.getOrCreateFileInfo(scriptInfo.path); - this.updateFileReferences(fileInfo); + function getModuleEmitHandler(): EmitHandler { + const references = createMap>(); + const referencedBy = createMultiMap(); + const scriptVersionForReferences = createMap(); + return { + addScriptInfo, + removeScriptInfo, + updateScriptInfo, + getFilesAffectedByUpdatedShape + }; + + function setReferences(path: Path, latestVersion: string, existingMap: Map) { + existingMap = mutateExistingMapWithNewSet(existingMap, project.getReferencedFiles(path), + // Creating new Reference: Also add referenced by + key => { referencedBy.add(key, path); return true; }, + // Remove existing reference + (key, _existingValue) => { referencedBy.remove(key, path); } + ); + references.set(path, existingMap); + scriptVersionForReferences.set(path, latestVersion); + } + + function addScriptInfo(info: ScriptInfo) { + setReferences(info.path, info.getLatestVersion(), undefined); + } + + function removeScriptInfo(path: Path) { + references.delete(path); + scriptVersionForReferences.delete(path); + } + + function updateScriptInfo(scriptInfo: ScriptInfo) { + const path = scriptInfo.path; + const lastUpdatedVersion = scriptVersionForReferences.get(path); + const latestVersion = scriptInfo.getLatestVersion(); + if (lastUpdatedVersion !== latestVersion) { + setReferences(path, latestVersion, references.get(path)); } - this.forEachFileInfo(fileInfo => { - if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) { - // This file was deleted from this project - fileInfo.removeFileReferences(); - this.removeFileInfo(fileInfo.scriptInfo.path); - } - }); - this.projectVersionForDependencyGraph = this.project.getProjectVersion(); - } - } - - private updateFileReferences(fileInfo: ModuleBuilderFileInfo) { - // Only need to update if the content of the file changed. - if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) { - return; } - const newReferences = this.getReferencedFileInfos(fileInfo); - const oldReferences = fileInfo.references; - enumerateInsertsAndDeletes(newReferences, oldReferences, - /*inserted*/ newReference => newReference.addReferencedBy(fileInfo), - /*deleted*/ oldReference => { - // New reference is greater then current reference. That means - // the current reference doesn't exist anymore after parsing. So delete - // references. - oldReference.removeReferencedBy(fileInfo); - }, - /*compare*/ ModuleBuilderFileInfo.compareFileInfos); - - fileInfo.references = newReferences; - fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion(); - } - - getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - this.ensureProjectDependencyGraphUpToDate(); - - const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; - const fileInfo = this.getFileInfo(scriptInfo.path); - if (!fileInfo || !fileInfo.updateShapeSignature()) { - return singleFileResult; + function getReferencedByPaths(path: Path) { + return referencedBy.get(path) || []; } - if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { - return this.project.getAllEmittableFiles(); - } + function getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { + const path = scriptInfo.path; + const sourceFile = getSourceFile(path); + if (!isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile)) { + return project.getAllEmittableFiles(); + } - const options = this.project.getCompilerOptions(); - if (options && (options.isolatedModules || options.out || options.outFile)) { - return singleFileResult; - } + const options = project.getCompilerOptions(); + if (options && (options.isolatedModules || options.out || options.outFile)) { + return singleFileResult; + } - // Now we need to if each file in the referencedBy list has a shape change as well. - // Because if so, its own referencedBy files need to be saved as well to make the - // emitting result consistent with files on disk. + // Now we need to if each file in the referencedBy list has a shape change as well. + // Because if so, its own referencedBy files need to be saved as well to make the + // emitting result consistent with files on disk. - // Use slice to clone the array to avoid manipulating in place - const queue = fileInfo.referencedBy.slice(0); - const fileNameSet = createMap(); - fileNameSet.set(scriptInfo.fileName, scriptInfo); - while (queue.length > 0) { - const processingFileInfo = queue.pop(); - if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) { - for (const potentialFileInfo of processingFileInfo.referencedBy) { - if (!fileNameSet.has(potentialFileInfo.scriptInfo.fileName)) { - queue.push(potentialFileInfo); + const fileNamesMap = createMap(); + const setFileName = (path: Path, scriptInfo: ScriptInfo) => { + fileNamesMap.set(path, scriptInfo && shouldEmitFile(scriptInfo) ? scriptInfo.fileName : undefined); + }; + + // Start with the paths this file was referenced by + setFileName(path, scriptInfo); + const queue = getReferencedByPaths(path).slice(); + while (queue.length > 0) { + const currentPath = queue.pop(); + if (!fileNamesMap.has(currentPath)) { + const currentScriptInfo = getScriptInfo(currentPath); + if (currentScriptInfo && updateShapeSignature(currentScriptInfo)) { + queue.push(...getReferencedByPaths(currentPath)); } + setFileName(currentPath, currentScriptInfo); } } - fileNameSet.set(processingFileInfo.scriptInfo.fileName, processingFileInfo.scriptInfo); - } - const result: string[] = []; - fileNameSet.forEach((scriptInfo, fileName) => { - if (shouldEmitFile(scriptInfo)) { - result.push(fileName); - } - }); - return result; - } - } - export function createBuilder(project: Project): Builder { - const moduleKind = project.getCompilerOptions().module; - switch (moduleKind) { - case ModuleKind.None: - return new NonModuleBuilder(project); - default: - return new ModuleBuilder(project); + // Return array of values that needs emit + return flatMapIter(fileNamesMap.values(), value => value); + } } } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 003bb87f6a9..ad8e355a500 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -519,6 +519,11 @@ namespace ts.server { return scriptInfo && scriptInfo.getDefaultProject(); } + getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { + this.ensureInferredProjectsUpToDate(); + return this.getScriptInfo(uncheckedFileName); + } + /** * Ensures the project structures are upto date * @param refreshInferredProjects when true updates the inferred projects even if there is no pending work diff --git a/src/server/project.ts b/src/server/project.ts index 62c450f9f4f..73d7f2d5017 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -811,19 +811,19 @@ namespace ts.server { } } - getReferencedFiles(path: Path): Path[] { + getReferencedFiles(path: Path): Map { + const referencedFiles = createMap(); if (!this.languageServiceEnabled) { - return []; + return referencedFiles; } const sourceFile = this.getSourceFile(path); if (!sourceFile) { - return []; + return referencedFiles; } // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - const referencedFiles = createMap(); if (sourceFile.imports && sourceFile.imports.length > 0) { const checker: TypeChecker = this.program.getTypeChecker(); for (const importName of sourceFile.imports) { @@ -859,8 +859,7 @@ namespace ts.server { }); } - const allFileNames = arrayFrom(referencedFiles.keys()) as Path[]; - return filter(allFileNames, file => this.lsHost.host.fileExists(file)); + return referencedFiles; } // remove a root file from project @@ -1138,12 +1137,6 @@ namespace ts.server { watchWildcards(wildcardDirectories: Map) { this.directoriesWatchedForWildcards = mutateExistingMap( this.directoriesWatchedForWildcards, wildcardDirectories, - // Watcher is same if the recursive flags match - ({ recursive: existingRecursive }, flag) => { - // If the recursive dont match, it needs update - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return existingRecursive !== recursive; - }, // Create new watch and recursive info (directory, flag) => { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; @@ -1160,6 +1153,12 @@ namespace ts.server { (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded ), + // Watcher is same if the recursive flags match + ({ recursive: existingRecursive }, flag) => { + // If the recursive dont match, it needs update + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return existingRecursive !== recursive; + }, // Close existing watch that doesnt match in recursive flag (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged diff --git a/src/server/session.ts b/src/server/session.ts index 0df2dd1e399..1e851d09615 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1193,11 +1193,11 @@ namespace ts.server { } private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): protocol.CompileOnSaveAffectedFileListSingleProject[] { - const info = this.projectService.getScriptInfo(args.file); + const info = this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file); const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = []; if (!info) { - return []; + return result; } // if specified a project, we only return affected file list in this project diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 6c884cebf59..36b769182e5 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -312,21 +312,18 @@ namespace ts.server { ): Map { return mutateExistingMap( existingMap, newMap, - // Same value if the value is set in the map - /*isSameValue*/(_existingValue, _valueInNewMap) => true, /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), onDeleteExistingValue, - // Should never be called since we say yes to same values all the time - /*OnDeleteExistingMismatchValue*/(_key, _existingValue) => notImplemented() ); } export function mutateExistingMap( existingMap: Map, newMap: Map, - isSameValue: (existingValue: T, valueInNewMap: U) => boolean, createNewValue: (key: string, valueInNewMap: U) => T, onDeleteExistingValue: (key: string, existingValue: T) => void, - OnDeleteExistingMismatchValue: (key: string, existingValue: T) => void + isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, + OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, + onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void ): Map { // If there are new values update them if (newMap) { @@ -340,10 +337,13 @@ namespace ts.server { onDeleteExistingValue(key, existingValue); } // different value - remove it - else if (!isSameValue(existingValue, valueInNewMap)) { + else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { existingMap.delete(key); OnDeleteExistingMismatchValue(key, existingValue); } + else if (onSameExistingValue) { + onSameExistingValue(existingValue, valueInNewMap); + } }); } else {