From c8d37dc87e12efa333cbc4bb42674300bac18cd3 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Wed, 22 Jun 2016 16:51:09 -0700 Subject: [PATCH] [in progress] project system work - versions --- src/server/editorServices.ts | 207 +++++++++------ src/server/project.ts | 188 ++++++++------ src/server/protocol.d.ts | 13 +- src/server/scriptInfo.ts | 8 +- src/server/session.ts | 241 +++++++----------- src/server/utilities.ts | 6 +- .../cases/unittests/cachingInServerLSHost.ts | 8 +- 7 files changed, 354 insertions(+), 317 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 08014379345..e111053315a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -28,7 +28,7 @@ namespace ts.server { hostInfo: string; } - function findVersionedProjectByFileName(projectName: string, projects: T[]): T { + function findProjectByName(projectName: string, projects: T[]): T { for (const proj of projects) { if (proj.getProjectName() === projectName) { return proj; @@ -42,7 +42,54 @@ namespace ts.server { project?: Project; } + class DirectoryWatchers { + /** + * a path to directory watcher map that detects added tsconfig files + **/ + private directoryWatchersForTsconfig: ts.Map = {}; + /** + * count of how many projects are using the directory watcher. + * If the number becomes 0 for a watcher, then we should close it. + **/ + private directoryWatchersRefCount: ts.Map = {}; + + constructor(private readonly projectService: ProjectService) { + } + + stopWatchingDirectory(directory: string) { + // if the ref count for this directory watcher drops to 0, it's time to close it + this.directoryWatchersRefCount[directory]--; + if (this.directoryWatchersRefCount[directory] === 0) { + this.projectService.log("Close directory watcher for: " + directory); + this.directoryWatchersForTsconfig[directory].close(); + delete this.directoryWatchersForTsconfig[directory]; + } + } + + startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) { + let currentPath = ts.getDirectoryPath(fileName); + let parentPath = ts.getDirectoryPath(currentPath); + while (currentPath != parentPath) { + if (!this.directoryWatchersForTsconfig[currentPath]) { + this.projectService.log("Add watcher for: " + currentPath); + this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback); + this.directoryWatchersRefCount[currentPath] = 1; + } + else { + this.directoryWatchersRefCount[currentPath] += 1; + } + project.directoriesWatchedForTsconfig.push(currentPath); + currentPath = parentPath; + parentPath = ts.getDirectoryPath(parentPath); + } + + } + } + export class ProjectService { + /** + * Container of all known scripts + */ private filenameToScriptInfo = createNormalizedPathMap(); /** * maps external project file name to list of config files that were the part of this project @@ -64,10 +111,9 @@ namespace ts.server { /** * open, non-configured root files **/ - openFileRoots: ScriptInfo[] = []; /** - * open files referenced by a project + * open files referenced by some project **/ openFilesReferenced: ScriptInfo[] = []; /** @@ -75,25 +121,19 @@ namespace ts.server { **/ openFileRootsConfigured: ScriptInfo[] = []; - /** - * a path to directory watcher map that detects added tsconfig files - **/ - private directoryWatchersForTsconfig: ts.Map = {}; - /** - * count of how many projects are using the directory watcher. - * If the number becomes 0 for a watcher, then we should close it. - **/ - private directoryWatchersRefCount: ts.Map = {}; + private directoryWatchers: DirectoryWatchers; private hostConfiguration: HostConfiguration; private timerForDetectingProjectFileListChanges: Map = {}; private documentRegistry: ts.DocumentRegistry; - constructor(public host: ServerHost, - public psLogger: Logger, - public cancellationToken: HostCancellationToken, - public eventHandler?: ProjectServiceEventHandler) { + constructor(public readonly host: ServerHost, + public readonly psLogger: Logger, + public readonly cancellationToken: HostCancellationToken, + private readonly eventHandler?: ProjectServiceEventHandler) { + + this.directoryWatchers = new DirectoryWatchers(this); // ts.disableIncrementalParsing = true; this.setDefaultHostConfiguration(); this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); @@ -107,13 +147,7 @@ namespace ts.server { } stopWatchingDirectory(directory: string) { - // if the ref count for this directory watcher drops to 0, it's time to close it - this.directoryWatchersRefCount[directory]--; - if (this.directoryWatchersRefCount[directory] === 0) { - this.log("Close directory watcher for: " + directory); - this.directoryWatchersForTsconfig[directory].close(); - delete this.directoryWatchersForTsconfig[directory]; - } + this.directoryWatchers.stopWatchingDirectory(directory); } findProject(projectName: string): Project { @@ -302,10 +336,18 @@ namespace ts.server { return undefined; } + private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { + for (const proj of this.externalProjects) { + if (proj.containsFile(fileName)) { + return proj; + } + } + return undefined; + } + private addOpenFile(info: ScriptInfo) { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { - // info.defaultProject = externalProject; return; } const configuredProject = this.findContainingConfiguredProject(info); @@ -403,15 +445,6 @@ namespace ts.server { info.isOpen = false; } - private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { - for (const proj of this.externalProjects) { - if (proj.containsFile(fileName)) { - return proj; - } - } - return undefined; - } - /** * This function 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 @@ -511,11 +544,11 @@ namespace ts.server { } private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { - return findVersionedProjectByFileName(configFileName, this.configuredProjects); + return findProjectByName(configFileName, this.configuredProjects); } private findExternalProjectByProjectName(projectFileName: string) { - return findVersionedProjectByFileName(projectFileName, this.externalProjects); + return findProjectByName(projectFileName, this.externalProjects); } private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { @@ -580,7 +613,7 @@ namespace ts.server { return { project, errors }; } - private createAndAddConfiguredProject(configFileName: string, projectOptions: ProjectOptions, clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) { const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files); const project = new ConfiguredProject( configFileName, @@ -611,7 +644,7 @@ namespace ts.server { let errors: Diagnostic[]; for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfo(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename); + const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename); project.addRoot(info); } else { @@ -622,7 +655,7 @@ namespace ts.server { return errors; } - private openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { + private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); if (!succeeded) { return { success: false, errors }; @@ -633,7 +666,7 @@ namespace ts.server { } } - private updateVersionedProjectWorker(project: VersionedProject, newRootFiles: string[], newOptions: CompilerOptions) { + private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f))); const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); @@ -649,7 +682,7 @@ namespace ts.server { for (const fileName of fileNamesToAdd) { let info = this.getScriptInfo(fileName); if (!info) { - info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); } else { // if the root file was opened by client, it would belong to either @@ -710,7 +743,7 @@ namespace ts.server { project.enableLanguageService(); } this.watchConfigDirectoryForProject(project, projectOptions); - this.updateVersionedProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); + this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); } } } @@ -720,21 +753,10 @@ namespace ts.server { const project = new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); project.addRoot(root); - let currentPath = ts.getDirectoryPath(root.fileName); - let parentPath = ts.getDirectoryPath(currentPath); - while (currentPath != parentPath) { - if (!this.directoryWatchersForTsconfig[currentPath]) { - this.log("Add watcher for: " + currentPath); - this.directoryWatchersForTsconfig[currentPath] = this.host.watchDirectory(currentPath, fileName => this.onConfigChangeForInferredProject(fileName)); - this.directoryWatchersRefCount[currentPath] = 1; - } - else { - this.directoryWatchersRefCount[currentPath] += 1; - } - project.directoriesWatchedForTsconfig.push(currentPath); - currentPath = parentPath; - parentPath = ts.getDirectoryPath(parentPath); - } + this.directoryWatchers.startWatchingContainingDirectoriesForFile( + root.fileName, + project, + fileName => this.onConfigChangeForInferredProject(fileName)); project.updateGraph(); this.inferredProjects.push(project); @@ -745,7 +767,11 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - getOrCreateScriptInfo(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + + getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(fileName), openedByClient, fileContent, scriptKind); + } + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { let info = this.filenameToScriptInfo.get(fileName); if (!info) { let content: string; @@ -879,12 +905,37 @@ namespace ts.server { // the file to the open, referenced file list. const openFileRoots: ScriptInfo[] = []; for (const rootFile of this.openFileRoots) { + let inConfiguredProject = false; + let inExternalProject = false; + for (const p of rootFile.containingProjects) { + inConfiguredProject = inConfiguredProject || p.projectKind === ProjectKind.Configured; + inExternalProject = inExternalProject || p.projectKind === ProjectKind.External; + } + if (inConfiguredProject || inExternalProject) { + const inferredProjects = rootFile.containingProjects.filter(p => p.projectKind === ProjectKind.Inferred); + for (const p of inferredProjects) { + this.removeProject(p); + } + if (inConfiguredProject) { + this.openFileRootsConfigured.push(rootFile); + } + } + else { + // + openFileRoots.push(rootFile); + } + if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { + // file was included in non-inferred project - drop old inferred project - let inInferredProjectOnly = true; + } + else { + openFileRoots.push(rootFile); + } + let inferredProjectsToRemove: Project[]; for (const p of rootFile.containingProjects) { if (p.projectKind !== ProjectKind.Inferred) { // file was included in non-inferred project - drop old inferred project - inInferredProjectOnly = false; + infe break; } } @@ -892,6 +943,7 @@ namespace ts.server { openFileRoots.push(rootFile); } else { + } // const rootedProject = rootFile.defaultProject; @@ -941,17 +993,20 @@ namespace ts.server { * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - openClientFile(uncheckedFileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind); + } + + openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { let configFileName: string; let configFileErrors: Diagnostic[]; - const fileName = toNormalizedPath(uncheckedFileName); if (!this.findContainingExternalProject(fileName)) { ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); } // at this point if file is the part of some configured/external project then this project should be created - const info = this.getOrCreateScriptInfo(fileName, /*openedByClient*/ true, fileContent, scriptKind); + const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind); this.addOpenFile(info); this.printProjects(); return { configFileName, configFileErrors }; @@ -970,27 +1025,23 @@ namespace ts.server { this.printProjects(); } - // getProjectForFile(filename: string) { - // const scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); - // if (scriptInfo) { - // return scriptInfo.defaultProject; - // } - // } + getDefaultProjectForFile(fileName: NormalizedPath) { + const scriptInfo = this.filenameToScriptInfo.get(fileName); + return scriptInfo && scriptInfo.getDefaultProject(); + } - private addExternalProjectFilesForVersionedProjects(knownProjects: protocol.ExternalProjectInfo[], projects: VersionedProject[], result: protocol.ExternalProjectFiles[]): void { + private syncExternalFilesList(knownProjects: protocol.ProjectVersionInfo[], projects: Project[], result: protocol.ProjectFiles[]): void { for (const proj of projects) { const knownProject = ts.forEach(knownProjects, p => p.projectName === proj.getProjectName() && p); result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); } } - synchronizeProjectList(knownProjects: protocol.ExternalProjectInfo[]): protocol.ExternalProjectFiles[] { - const files: protocol.ExternalProjectFiles[] = []; - this.addExternalProjectFilesForVersionedProjects(knownProjects, this.externalProjects, files); - this.addExternalProjectFilesForVersionedProjects(knownProjects, this.configuredProjects, files); - for (const inferredProject of this.inferredProjects) { - files.push({ files: inferredProject.getFileNames() }); - } + synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] { + const files: protocol.ProjectFiles[] = []; + this.syncExternalFilesList(knownProjects, this.externalProjects, files); + this.syncExternalFilesList(knownProjects, this.configuredProjects, files); + this.syncExternalFilesList(knownProjects, this.inferredProjects, files); return files; } @@ -998,7 +1049,7 @@ namespace ts.server { for (const file of openFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!scriptInfo || !scriptInfo.isOpen); - this.openClientFile(file.fileName, file.content); + this.openClientFileWithNormalizedPath(toNormalizedPath(file.fileName), file.content); } for (const file of changedFiles) { @@ -1042,7 +1093,7 @@ namespace ts.server { openExternalProject(proj: protocol.ExternalProject): void { const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); if (externalProject) { - this.updateVersionedProjectWorker(externalProject, proj.rootFiles, proj.options); + this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options); } else { let tsConfigFiles: NormalizedPath[]; diff --git a/src/server/project.ts b/src/server/project.ts index 1b122802f56..0a36c3ee62f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1,4 +1,5 @@ /// +/// /// /// @@ -11,13 +12,33 @@ namespace ts.server { export abstract class Project { private rootFiles: ScriptInfo[] = []; - private rootFilesMap: FileMap = createFileMap(); + private readonly rootFilesMap: FileMap = createFileMap(); private lsHost: ServerLanguageServiceHost; - protected program: ts.Program; - private version = 0; + private program: ts.Program; languageService: LanguageService; + /** + * Set of files that was returned from the last call to getChangesSinceVersion. + */ + private lastReportedFileNames: Map; + /** + * Last version that was reported. + */ + private lastReportedVersion = 0; + /** + * Current project structure version. + * This property is changed in 'updateGraph' based on the set of files in program + */ + private projectStructureVersion = 0; + /** + * Current version of the project state. It is changed when: + * - new root file was added/removed + * - edit happen in some file that is currently included in the project. + * This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project + */ + private projectStateVersion = 0; + constructor( readonly projectKind: ProjectKind, readonly projectService: ProjectService, @@ -46,7 +67,7 @@ namespace ts.server { } getProjectVersion() { - return this.version.toString(); + return this.projectStateVersion.toString(); } enableLanguageService() { @@ -68,8 +89,8 @@ namespace ts.server { close() { for (const fileName of this.getFileNames()) { - const info = this.projectKind.getScriptInfoForNormalizedPath(fileName); - info.detachFromProject(project); + const info = this.projectService.getScriptInfoForNormalizedPath(fileName); + info.detachFromProject(this); } // signal language service to release files acquired from document registry this.languageService.dispose(); @@ -85,6 +106,10 @@ namespace ts.server { } getFileNames() { + if (!this.program) { + return []; + } + if (!this.languageServiceEnabled) { // if language service is disabled assume that all files in program are root files + default library let rootFiles = this.getRootFiles(); @@ -128,16 +153,18 @@ namespace ts.server { } } - removeFile(info: ScriptInfo) { + removeFile(info: ScriptInfo, detachFromProject: boolean = true) { if (!this.removeRoot(info)) { this.removeReferencedFile(info) } - info.detachFromProject(this); + if (detachFromProject) { + info.detachFromProject(this); + } this.markAsDirty(); } markAsDirty() { - this.version++; + this.projectStateVersion++; } // remove a root file from project @@ -153,21 +180,36 @@ namespace ts.server { private removeReferencedFile(info: ScriptInfo) { this.lsHost.removeReferencedFile(info) - this.updateGraph(); } updateGraph() { + if (!this.languageServiceEnabled) { + return; + } + + const oldProgram = this.program; this.program = this.languageService.getProgram(); + + // bump up the version if + // - oldProgram is not set - this is a first time updateGraph is called + // - newProgram is different from the old program and structure of the old program was not reused. + if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) { + this.projectStructureVersion++; + } } - getScriptInfo(uncheckedFileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(toNormalizedPath(uncheckedFileName), /*openedByClient*/ false); - if (scriptInfo.attachToProject(this)) { + getScriptInfoFromNormalizedPath(fileName: NormalizedPath) { + const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); + if (scriptInfo && scriptInfo.attachToProject(this)) { this.markAsDirty(); } return scriptInfo; } + getScriptInfo(uncheckedFileName: string) { + return this.getScriptInfoFromNormalizedPath(toNormalizedPath(uncheckedFileName)); + } + filesToString() { if (!this.program) { return ""; @@ -197,19 +239,66 @@ namespace ts.server { } } - reloadScript(filename: string, tmpfilename: string, cb: () => void) { - const script = this.getScriptInfo(filename); + reloadScript(filename: NormalizedPath, cb: () => void) { + const script = this.getScriptInfoFromNormalizedPath(filename); if (script) { script.reloadFromFile(filename, cb); } } + + getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles { + const info = { + projectName: this.getProjectName(), + version: this.projectStructureVersion, + isInferred: this.projectKind === ProjectKind.Inferred + }; + // check if requested version is the same that we have reported last time + if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { + // if current structure version is the same - return info witout any changes + if (this.projectStructureVersion == this.lastReportedVersion) { + return { info }; + } + // compute and return the difference + const lastReportedFileNames = this.lastReportedFileNames; + const currentFiles = arrayToMap(this.getFileNames(), x => x); + + const added: string[] = []; + const removed: string[] = []; + for (const id in currentFiles) { + if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { + added.push(id); + } + } + for (const id in lastReportedFileNames) { + if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { + removed.push(id); + } + } + this.lastReportedFileNames = currentFiles; + + this.lastReportedFileNames = currentFiles; + this.lastReportedVersion = this.projectStructureVersion; + return { info, changes: { added, removed } }; + } + else { + // unknown version - return everything + const projectFileNames = this.getFileNames(); + this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); + this.lastReportedVersion = this.projectStructureVersion; + return { info, files: projectFileNames }; + } + } } export class InferredProject extends Project { - static NextId = 0; + private static NextId = 1; + + /** + * Unique name that identifies this particular inferred project + */ + private readonly inferredProjectName: string; - readonly inferredProjectName; // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; @@ -237,73 +326,14 @@ namespace ts.server { } } - export abstract class VersionedProject extends Project { - - private lastReportedFileNames: Map; - private lastReportedVersion: number = 0; - currentVersion: number = 1; - - updateGraph() { - if (!this.languageServiceEnabled) { - return; - } - const oldProgram = this.program; - - super.updateGraph(); - - if (!oldProgram || !oldProgram.structureIsReused) { - this.currentVersion++; - } - } - - getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles { - const info = { - projectName: this.getProjectName(), - version: this.currentVersion - }; - if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { - if (this.currentVersion == this.lastReportedVersion) { - return { info }; - } - const lastReportedFileNames = this.lastReportedFileNames; - const currentFiles = arrayToMap(this.getFileNames(), x => x); - - const added: string[] = []; - const removed: string[] = []; - for (const id in currentFiles) { - if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { - added.push(id); - } - } - for (const id in lastReportedFileNames) { - if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { - removed.push(id); - } - } - this.lastReportedFileNames = currentFiles; - - this.lastReportedFileNames = currentFiles; - this.lastReportedVersion = this.currentVersion; - return { info, changes: { added, removed } }; - } - else { - // unknown version - return everything - const projectFileNames = this.getFileNames(); - this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); - this.lastReportedVersion = this.currentVersion; - return { info, files: projectFileNames }; - } - } - } - - export class ConfiguredProject extends VersionedProject { + export class ConfiguredProject extends Project { private projectFileWatcher: FileWatcher; private directoryWatcher: FileWatcher; private directoriesWatchedForWildcards: Map; /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(readonly configFileName: string, + constructor(readonly configFileName: NormalizedPath, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, @@ -380,7 +410,7 @@ namespace ts.server { } } - export class ExternalProject extends VersionedProject { + export class ExternalProject extends Project { constructor(readonly externalProjectName: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index e1eb1ecb015..a4e9062717a 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -494,12 +494,13 @@ declare namespace ts.server.protocol { options: CompilerOptions; } - export interface ExternalProjectInfo { + export interface ProjectVersionInfo { projectName: string; + isInferred: boolean; version: number; } - export interface ExternalProjectChanges { + export interface ProjectChanges { added: string[]; removed: string[]; } @@ -511,10 +512,10 @@ declare namespace ts.server.protocol { * if changes is set - then this is the set of changes that should be applied to existing project * otherwise - assume that nothing is changed */ - export interface ExternalProjectFiles { - info?: ExternalProjectInfo; + export interface ProjectFiles { + info?: ProjectVersionInfo; files?: string[]; - changes?: ExternalProjectChanges; + changes?: ProjectChanges; } /** @@ -674,7 +675,7 @@ declare namespace ts.server.protocol { } export interface SynchronizeProjectListRequestArgs { - knownProjects: protocol.ExternalProjectInfo[]; + knownProjects: protocol.ProjectVersionInfo[]; } export interface ApplyChangedToOpenFilesRequest extends Request { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index ddf8008600e..52e1baf36b4 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -37,17 +37,13 @@ namespace ts.server { } detachFromProject(project: Project) { - const index = this.containingProjects.indexOf(project); - if (index < 0) { - // TODO: (assert?) attempt to detach file from project that didn't include this file - return; - } removeItemFromSet(this.containingProjects, project); } detachAllProjects() { for (const p of this.containingProjects) { - p.removeFile(this); + // detach is unnecessary since we'll clean the list of containing projects anyways + p.removeFile(this, /*detachFromProjects*/ false); } this.containingProjects.length = 0; } diff --git a/src/server/session.ts b/src/server/session.ts index e3d7fff3068..8a480580ddd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -397,7 +397,7 @@ namespace ts.server { private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[]) { const { project, file } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfo(file); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); const diagnostics = selector(project, file); return this.convertDiagnostics(diagnostics, scriptInfo); } @@ -436,15 +436,10 @@ namespace ts.server { } } - private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + private getTypeDefinition(args: protocol.FileLocationRequestArgs): protocol.FileSpan[] { + const { file, project } = this.getFileAndProject(args) + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const position = this.getPosition(args, scriptInfo); const definitions = project.languageService.getTypeDefinitionAtPosition(file, position); if (!definitions) { @@ -461,18 +456,12 @@ namespace ts.server { }); } - private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[] { - fileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(fileName); + private getOccurrences(args: protocol.FileLocationRequestArgs): protocol.OccurrencesResponseItem[] { + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); + const position = this.getPosition(args, scriptInfo); - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(fileName); - const position = scriptInfo.lineOffsetToPosition(line, offset); - - const occurrences = project.languageService.getOccurrencesAtPosition(fileName, position); + const occurrences = project.languageService.getOccurrencesAtPosition(file, position); if (!occurrences) { return undefined; @@ -493,17 +482,11 @@ namespace ts.server { } private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): protocol.DocumentHighlightsItem[] | DocumentHighlights[] { - const fileName = ts.normalizePath(args.file); - const project = this.projectService.getProjectForFile(fileName); - - if (!project) { - throw Errors.NoProject; - } - - const scriptInfo = project.getScriptInfo(fileName); + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); const position = this.getPosition(args, scriptInfo); - const documentHighlights = project.languageService.getDocumentHighlights(fileName, position, args.filesToSearch); + const documentHighlights = project.languageService.getDocumentHighlights(file, position, args.filesToSearch); if (!documentHighlights) { return undefined; @@ -534,27 +517,23 @@ namespace ts.server { } } - private getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo { - fileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(fileName); - if (!project) { - throw Errors.NoProject; - } + private getProjectInfo(args: protocol.ProjectInfoRequestArgs): protocol.ProjectInfo { + return this.getProjectInfoWorker(args.file, args.projectFileName, args.needFileNameList); + } - const projectInfo: protocol.ProjectInfo = { - configFileName: project.getProjectFileName(), - languageServiceDisabled: !project.languageServiceEnabled + private getProjectInfoWorker(uncheckedFileName: string, projectFileName: string, needFileNameList: boolean) { + const { file, project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName, /*errorOnMissingProject*/ true); + const projectInfo = { + configFileName: project.getProjectName(), + languageServiceDisabled: !project.languageServiceEnabled, + fileNames: needFileNameList ? project.getFileNames() : undefined }; - - if (needFileNameList) { - projectInfo.fileNames = project.getFileNames(); - } return projectInfo; } private getRenameInfo(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProject(args.file); - const scriptInfo = project.getScriptInfo(file); + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoFromNormalizedPath(file); const position = this.getPosition(args, scriptInfo); return project.languageService.getRenameInfo(file, position); } @@ -568,10 +547,10 @@ namespace ts.server { } } else { - const file = normalizePath(args.file); - const info = this.projectService.getScriptInfo(file); - projects = this.projectService.findReferencingProjects(info); + const scriptInfo = this.projectService.getScriptInfo(args.file); + projects = scriptInfo.containingProjects; } + // ts.filter handles case when 'projects' is undefined projects = filter(projects, p => p.languageServiceEnabled); if (!projects || !projects.length) { throw Errors.NoProject; @@ -756,9 +735,8 @@ namespace ts.server { * @param fileName is the name of the file to be opened * @param fileContent is a version of the file content that is known to be more up to date than the one on disk */ - private openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind) { - const file = ts.normalizePath(fileName); - const { configFileName, configFileErrors } = this.projectService.openClientFile(file, fileContent, scriptKind); + private openClientFile(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind) { + const { configFileName, configFileErrors } = this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind); if (configFileErrors) { this.configFileDiagnosticEvent(fileName, configFileName, configFileErrors); } @@ -768,10 +746,14 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getFileAndProject(args: protocol.FileLocationRequestArgs) { - const file = ts.normalizePath(args.file); - const project: Project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file); - if (!project) { + private getFileAndProject(args: protocol.FileRequestArgs, errorOnMissingProject = true) { + return this.getFileAndProjectWorker(args.file, args.projectFileName, errorOnMissingProject); + } + + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, errorOnMissingProject: boolean) { + const file = toNormalizedPath(uncheckedFileName); + const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file); + if (!project && errorOnMissingProject) { throw Errors.NoProject; } return { file, project }; @@ -844,16 +826,12 @@ namespace ts.server { } } - private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + private getFormattingEditsForRange(args: protocol.FormatRequestArgs): protocol.CodeEdit[] { + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); - const startPosition = scriptInfo.lineOffsetToPosition(line, offset); - const endPosition = scriptInfo.lineOffsetToPosition(endLine, endOffset); + + const startPosition = scriptInfo.lineOffsetToPosition(args.line, args.offset); + const endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); // TODO: avoid duplicate code (with formatonkey) const edits = project.languageService.getFormattingEditsForRange(file, startPosition, endPosition, @@ -886,18 +864,12 @@ namespace ts.server { return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options); } - private getFormattingEditsAfterKeystroke(line: number, offset: number, key: string, fileName: string): protocol.CodeEdit[] { - const file = ts.normalizePath(fileName); - - const project = this.projectService.getProjectForFile(file); - if (!project) { - throw Errors.NoProject; - } - + private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] { + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); - const position = scriptInfo.lineOffsetToPosition(line, offset); + const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); const formatOptions = this.projectService.getFormatCodeOptions(file); - const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, key, + const edits = project.languageService.getFormattingEditsAfterKeystroke(file, position, args.key, formatOptions); // Check whether we should auto-indent. This will be when // the position is on a line containing only whitespace. @@ -905,8 +877,8 @@ namespace ts.server { // getFormattingEditsAfterKeystroke either empty or pertaining // only to the previous line. If all this is true, then // add edits necessary to properly indent the current line. - if ((key == "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { - const lineInfo = scriptInfo.getLineInfo(line); + if ((args.key == "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { + const lineInfo = scriptInfo.getLineInfo(args.line); if (lineInfo && (lineInfo.leaf) && (lineInfo.leaf.text)) { const lineText = lineInfo.leaf.text; if (lineText.search("\\S") < 0) { @@ -1023,10 +995,9 @@ namespace ts.server { } private getDiagnostics(delay: number, fileNames: string[]) { - const checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { - - fileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(fileName); + const checkList = fileNames.reduce((accum: PendingErrorCheck[], uncheckedFileName: string) => { + const fileName = toNormalizedPath(uncheckedFileName); + const project = this.projectService.getDefaultProjectForFile(fileName); if (project) { accum.push({ fileName, project }); } @@ -1038,39 +1009,37 @@ namespace ts.server { } } - private change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) { - const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); + private change(args: protocol.ChangeRequestArgs) { + const { file, project } = this.getFileAndProject(args, /*errorOnMissingProject*/ false); if (project) { const scriptInfo = project.getScriptInfo(file); - const start = scriptInfo.lineOffsetToPosition(line, offset); - const end = scriptInfo.lineOffsetToPosition(endLine, endOffset); + const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); + const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { - scriptInfo.editContent(start, end, insertString); + scriptInfo.editContent(start, end, args.insertString); this.changeSeq++; } this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq); } } - private reload(fileName: string, tempFileName: string, reqSeq = 0) { - const file = ts.normalizePath(fileName); - const tmpfile = ts.normalizePath(tempFileName); - const project = this.projectService.getProjectForFile(file); + private reload(args: protocol.ReloadRequestArgs, reqSeq: number) { + const file = toNormalizedPath(args.file); + const project = this.projectService.getDefaultProjectForFile(file); if (project) { this.changeSeq++; // make sure no changes happen before this one is finished - project.reloadScript(file, tmpfile, () => { + project.reloadScript(file, () => { this.output(undefined, CommandNames.Reload, reqSeq); }); } } private saveToTmp(fileName: string, tempFileName: string) { - const file = ts.normalizePath(fileName); + const file = toNormalizedPath(fileName); const tmpfile = ts.normalizePath(tempFileName); - const project = this.projectService.getProjectForFile(file); + const project = this.projectService.getDefaultProjectForFile(file); if (project) { project.saveTo(file, tmpfile); } @@ -1084,12 +1053,12 @@ namespace ts.server { this.projectService.closeClientFile(file); } - private decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { + private decorateNavigationBarItem(project: Project, fileName: NormalizedPath, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { if (!items) { return undefined; } - const scriptInfo = project.getScriptInfo(fileName); + const scriptInfo = project.getScriptInfoFromNormalizedPath(fileName); return items.map(item => ({ text: item.text, @@ -1104,16 +1073,15 @@ namespace ts.server { })); } - private getNavigationBarItems(fileName: string, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] { - const { file, project } = this.getFileAndProject(fileName); - + private getNavigationBarItems(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] { + const { file, project } = this.getFileAndProject(args); const items = project.languageService.getNavigationBarItems(file); if (!items) { return undefined; } return simplifiedResult - ? this.decorateNavigationBarItem(project, fileName, items) + ? this.decorateNavigationBarItem(project, file, items) : items; } @@ -1197,7 +1165,7 @@ namespace ts.server { } private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { - const { file, project } = this.getFileAndProject(args.file); + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfo(file); const position = this.getPosition(args, scriptInfo); @@ -1219,7 +1187,7 @@ namespace ts.server { } getDiagnosticsForProject(delay: number, fileName: string) { - const { fileNames, languageServiceDisabled } = this.getProjectInfo(fileName, /*needFileNameList*/ true); + const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(fileName, /*projectFileName*/ undefined, /*needFileNameList*/ true); if (languageServiceDisabled) { return; } @@ -1228,12 +1196,12 @@ namespace ts.server { let fileNamesInProject = fileNames.filter((value, index, array) => value.indexOf("lib.d.ts") < 0); // Sort the file name list to make the recently touched files come first - const highPriorityFiles: string[] = []; - const mediumPriorityFiles: string[] = []; - const lowPriorityFiles: string[] = []; - const veryLowPriorityFiles: string[] = []; - const normalizedFileName = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(normalizedFileName); + const highPriorityFiles: NormalizedPath[] = []; + const mediumPriorityFiles: NormalizedPath[] = []; + const lowPriorityFiles: NormalizedPath[] = []; + const veryLowPriorityFiles: NormalizedPath[] = []; + const normalizedFileName = toNormalizedPath(fileName); + const project = this.projectService.getDefaultProjectForFile(normalizedFileName); for (const fileNameInProject of fileNamesInProject) { if (this.getCanonicalFileName(fileNameInProject) == this.getCanonicalFileName(fileName)) highPriorityFiles.push(fileNameInProject); @@ -1253,10 +1221,7 @@ namespace ts.server { fileNamesInProject = highPriorityFiles.concat(mediumPriorityFiles).concat(lowPriorityFiles).concat(veryLowPriorityFiles); if (fileNamesInProject.length > 0) { - const checkList = fileNamesInProject.map((fileName: string) => { - const normalizedFileName = ts.normalizePath(fileName); - return { fileName: normalizedFileName, project }; - }); + const checkList = fileNamesInProject.map(fileName => ({ fileName, project })); // Project level error analysis runs on background files too, therefore // doesn't require the file to be opened this.updateErrorCheck(checkList, this.changeSeq, (n) => n == this.changeSeq, delay, 200, /*requireOpen*/ false); @@ -1316,9 +1281,8 @@ namespace ts.server { [CommandNames.DefinitionFull]: (request: protocol.DefinitionRequest) => { return this.requiredResponse(this.getDefinition(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.TypeDefinition]: (request: protocol.Request) => { - const defArgs = request.arguments; - return this.requiredResponse(this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file)); + [CommandNames.TypeDefinition]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getTypeDefinition(request.arguments)); }, [CommandNames.References]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ true)); @@ -1352,7 +1316,7 @@ namespace ts.server { scriptKind = ScriptKind.JSX; break; } - this.openClientFile(openArgs.file, openArgs.fileContent, scriptKind); + this.openClientFile(toNormalizedPath(openArgs.file), openArgs.fileContent, scriptKind); return this.notRequired(); }, [CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => { @@ -1382,13 +1346,11 @@ namespace ts.server { [CommandNames.DocCommentTemplate]: (request: protocol.FileLocationRequest) => { return this.requiredResponse(this.getDocCommentTemplate(request.arguments)); }, - [CommandNames.Format]: (request: protocol.Request) => { - const formatArgs = request.arguments; - return this.requiredResponse(this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file)); + [CommandNames.Format]: (request: protocol.FormatRequest) => { + return this.requiredResponse(this.getFormattingEditsForRange(request.arguments)); }, - [CommandNames.Formatonkey]: (request: protocol.Request) => { - const formatOnKeyArgs = request.arguments; - return this.requiredResponse(this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file)); + [CommandNames.Formatonkey]: (request: protocol.FormatOnKeyRequest) => { + return this.requiredResponse(this.getFormattingEditsAfterKeystroke(request.arguments)); }, [CommandNames.FormatFull]: (request: protocol.FormatRequest) => { return this.requiredResponse(this.getFormattingEditsForDocumentFull(request.arguments)); @@ -1438,32 +1400,29 @@ namespace ts.server { const { file, delay } = request.arguments; return { response: this.getDiagnosticsForProject(delay, file), responseRequired: false }; }, - [CommandNames.Change]: (request: protocol.Request) => { - const changeArgs = request.arguments; - this.change(changeArgs.line, changeArgs.offset, changeArgs.endLine, changeArgs.endOffset, - changeArgs.insertString, changeArgs.file); - return { responseRequired: false }; + [CommandNames.Change]: (request: protocol.ChangeRequest) => { + this.change(request.arguments); + return this.notRequired(); }, [CommandNames.Configure]: (request: protocol.Request) => { const configureArgs = request.arguments; this.projectService.setHostConfiguration(configureArgs); this.output(undefined, CommandNames.Configure, request.seq); - return { responseRequired: false }; + return this.notRequired(); }, - [CommandNames.Reload]: (request: protocol.Request) => { - const reloadArgs = request.arguments; - this.reload(reloadArgs.file, reloadArgs.tmpfile, request.seq); - return { response: { reloadFinished: true }, responseRequired: true }; + [CommandNames.Reload]: (request: protocol.ReloadRequest) => { + this.reload(request.arguments, request.seq); + return this.requiredResponse({ reloadFinished: true }); }, [CommandNames.Saveto]: (request: protocol.Request) => { const savetoArgs = request.arguments; this.saveToTmp(savetoArgs.file, savetoArgs.tmpfile); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Close]: (request: protocol.Request) => { const closeArgs = request.arguments; this.closeClientFile(closeArgs.file); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Navto]: (request: protocol.NavtoRequest) => { return this.requiredResponse(this.getNavigateToItems(request.arguments, /*simplifiedResult*/ true)); @@ -1478,14 +1437,13 @@ namespace ts.server { return this.requiredResponse(this.getBraceMatching(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.NavBar]: (request: protocol.FileRequest) => { - return this.requiredResponse(this.getNavigationBarItems(request.arguments.file, /*simplifiedResult*/ true)); + return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.NavBarFull]: (request: protocol.FileRequest) => { - return this.requiredResponse(this.getNavigationBarItems(request.arguments.file, /*simplifiedResult*/ false)); + return this.requiredResponse(this.getNavigationBarItems(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.Occurrences]: (request: protocol.Request) => { - const { line, offset, file: fileName } = request.arguments; - return { response: this.getOccurrences(line, offset, fileName), responseRequired: true }; + [CommandNames.Occurrences]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getOccurrences(request.arguments));; }, [CommandNames.DocumentHighlights]: (request: protocol.DocumentHighlightsRequest) => { return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ true)); @@ -1493,13 +1451,12 @@ namespace ts.server { [CommandNames.DocumentHighlightsFull]: (request: protocol.DocumentHighlightsRequest) => { return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.ProjectInfo]: (request: protocol.Request) => { - const { file, needFileNameList } = request.arguments; - return { response: this.getProjectInfo(file, needFileNameList), responseRequired: true }; + [CommandNames.ProjectInfo]: (request: protocol.ProjectInfoRequest) => { + return this.requiredResponse(this.getProjectInfo(request.arguments)); }, [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { this.reloadProjects(); - return { responseRequired: false }; + return this.notRequired(); } }; public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 6fbff20fa96..c6da27505ac 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,5 +1,4 @@ /// -/// namespace ts.server { export interface Logger { @@ -42,11 +41,14 @@ namespace ts.server { } export function removeItemFromSet(items: T[], itemToRemove: T) { + if (items.length === 0) { + return; + } const index = items.indexOf(itemToRemove); if (index < 0) { return; } - if (items.length === 0) { + if (items.length === 1) { items.pop(); } else { diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index d0f63e7c339..2402102496d 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -118,7 +118,7 @@ namespace ts { const newContent = `import {x} from "f1" var x: string = 1;`; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); + rootScriptInfo.editContent(0, root.content.length, newContent); // trigger synchronization to make sure that import will be fetched from the cache diags = project.languageService.getSemanticDiagnostics(imported.name); // ensure file has correct number of errors after edit @@ -135,7 +135,7 @@ namespace ts { return originalFileExists.call(serverHost, fileName); }; const newContent = `import {x} from "f2"`; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); + rootScriptInfo.editContent(0, root.content.length, newContent); try { // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk @@ -160,7 +160,7 @@ namespace ts { }; const newContent = `import {x} from "f1"`; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent); + rootScriptInfo.editContent(0, root.content.length, newContent); project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(fileExistsCalled); @@ -213,7 +213,7 @@ namespace ts { // assert that import will success once file appear on disk fileMap[imported.name] = imported; fileExistsCalledForBar = false; - rootScriptInfo.editContent(0, rootScriptInfo.content.length, `import {y} from "bar"`); + rootScriptInfo.editContent(0, root.content.length, `import {y} from "bar"`); diags = project.languageService.getSemanticDiagnostics(root.name); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");