From 5369b37f0ad2c95edc78f85be630573e150a44ee Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 18 Jun 2018 11:46:55 -0700 Subject: [PATCH 1/7] Make GetEditsForFileRenameRequestArgs not extend FileRequestArgs --- src/compiler/utilities.ts | 6 +++- src/server/editorServices.ts | 34 +++++++++++++------ src/server/protocol.ts | 5 ++- src/server/session.ts | 29 ++++++++++++---- .../unittests/tsserverProjectSystem.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 12 +++++-- 6 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 49769953f029e..d98209d43ce9d 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7494,7 +7494,11 @@ namespace ts { } } - // Reserved characters, forces escaping of any non-word (or digit), non-whitespace character. + export function startsWithDirectory(path: string, dirPath: string): boolean { + return tryRemoveDirectoryPrefix(path, dirPath) !== undefined; + } + + // Reserved characters, forces escaping of any non-word (or digit), non-whitespace character. // It may be inefficient (we could just match (/[-[\]{}()*+?.,\\^$|#\s]/g), but this is future // proof. const reservedCharacterPattern = /[^\w\s\/]/g; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 77e5b2ed8294d..532265decdb47 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -692,17 +692,31 @@ namespace ts.server { return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } - getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined { - let scriptInfo = this.getScriptInfoForNormalizedPath(fileName); - if (ensureProject && (!scriptInfo || scriptInfo.isOrphan())) { - this.ensureProjectStructuresUptoDate(); - scriptInfo = this.getScriptInfoForNormalizedPath(fileName); - if (!scriptInfo) { - return Errors.ThrowNoProject(); + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject = false): Project | undefined { + const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : ensureProject ? this.doEnsureDefaultProjectForFile(fileName) : undefined; + } + + ensureDefaultProjectForFile(fileName: NormalizedPath): Project { + return this.getDefaultProjectForFile(fileName) || this.doEnsureDefaultProjectForFile(fileName); + } + + private doEnsureDefaultProjectForFile(fileName: NormalizedPath): Project { + this.ensureProjectStructuresUptoDate(); + const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + return scriptInfo ? scriptInfo.getDefaultProject() : Errors.ThrowNoProject(); + } + + /* @internal */ + tryGetSomeFileInDirectory(directoryPath: NormalizedPath): { readonly file: NormalizedPath, readonly project: Project } | undefined { + const scriptInfo = forEachEntry(this.filenameToScriptInfo, scriptInfo => { + if (startsWithDirectory(scriptInfo.path, directoryPath)) { + return scriptInfo; } - return scriptInfo.getDefaultProject(); - } - return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined; + }); + if (!scriptInfo) return undefined; + const file = toNormalizedPath(scriptInfo.path); + return { file, project: this.ensureDefaultProjectForFile(file) }; } getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 2f128aa1663f7..0da3657241f3f 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -625,9 +625,8 @@ namespace ts.server.protocol { arguments: GetEditsForFileRenameRequestArgs; } - // Note: The file from FileRequestArgs is just any file in the project. - // We will generate code changes for every file in that project, so the choice is arbitrary. - export interface GetEditsForFileRenameRequestArgs extends FileRequestArgs { + /** Note: Paths may also be directories. */ + export interface GetEditsForFileRenameRequestArgs { readonly oldFilePath: string; readonly newFilePath: string; } diff --git a/src/server/session.ts b/src/server/session.ts index 74629d97e4877..1df222aa2ba72 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1142,15 +1142,27 @@ namespace ts.server { return this.getPosition(args, scriptInfo); } - private getFileAndProject(args: protocol.FileRequestArgs): { file: NormalizedPath, project: Project } { + private getFileAndProject(args: protocol.FileRequestArgs): FileAndProject { return this.getFileAndProjectWorker(args.file, args.projectFileName); } + private getFileAndProjectForFileRename(args: protocol.GetEditsForFileRenameRequestArgs): FileAndProject { + const oldFilePath = toNormalizedPath(args.oldFilePath); + const oldProject = this.projectService.getDefaultProjectForFile(oldFilePath); + if (oldProject) return { file: oldFilePath, project: oldProject }; + + const newFilePath = toNormalizedPath(args.newFilePath); + const newProject = this.projectService.getDefaultProjectForFile(newFilePath); + if (newProject) return { file: newFilePath, project: newProject }; + + return Debug.assertDefined(this.projectService.tryGetSomeFileInDirectory(oldFilePath) || this.projectService.tryGetSomeFileInDirectory(newFilePath)); + } + private getFileAndLanguageServiceForSyntacticOperation(args: protocol.FileRequestArgs) { // Since this is syntactic operation, there should always be project for the file // we wouldnt have to ensure project but rather throw if we dont get project const file = toNormalizedPath(args.file); - const project = this.getProject(args.projectFileName) || this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ false); + const project = this.getProject(args.projectFileName) || this.projectService.getDefaultProjectForFile(file); if (!project) { return Errors.ThrowNoProject(); } @@ -1162,7 +1174,7 @@ namespace ts.server { private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string | undefined): { file: NormalizedPath, project: Project } { const file = toNormalizedPath(uncheckedFileName); - const project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ true)!; // TODO: GH#18217 + const project = this.getProject(projectFileName) || this.projectService.ensureDefaultProjectForFile(file); return { file, project }; } @@ -1454,7 +1466,7 @@ namespace ts.server { private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] { return mapDefined(fileNames, uncheckedFileName => { const fileName = toNormalizedPath(uncheckedFileName); - const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*ensureProject*/ false); + const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName); return project && { fileName, project }; }); } @@ -1731,7 +1743,7 @@ namespace ts.server { } private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { - const { file, project } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProjectForFileRename(args); const changes = project.getLanguageService().getEditsForFileRename(toNormalizedPath(args.oldFilePath), toNormalizedPath(args.newFilePath), this.getFormatOptions(file), this.getPreferences(file)); return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; } @@ -1852,7 +1864,7 @@ namespace ts.server { const lowPriorityFiles: NormalizedPath[] = []; const veryLowPriorityFiles: NormalizedPath[] = []; const normalizedFileName = toNormalizedPath(fileName); - const project = this.projectService.getDefaultProjectForFile(normalizedFileName, /*ensureProject*/ true)!; + const project = this.projectService.ensureDefaultProjectForFile(normalizedFileName); for (const fileNameInProject of fileNamesInProject) { if (this.getCanonicalFileName(fileNameInProject) === this.getCanonicalFileName(fileName)) { highPriorityFiles.push(fileNameInProject); @@ -2295,6 +2307,11 @@ namespace ts.server { } } + interface FileAndProject { + readonly file: NormalizedPath; + readonly project: Project; + } + function mapTextChangesToCodeEdits(textChanges: FileTextChanges, sourceFile: SourceFile | undefined): protocol.FileCodeEdits { Debug.assert(!!textChanges.isNewFile === !sourceFile); if (sourceFile) { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index feb7377a86852..aad546c0ffeed 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -3097,7 +3097,7 @@ namespace ts.projectSystem { checkProjectRootFiles(project, [file.path]); checkProjectActualFiles(project, [file.path, libFile.path]); - assert.strictEqual(projectService.getDefaultProjectForFile(server.toNormalizedPath(file.path), /*ensureProject*/ true), project); + assert.strictEqual(projectService.ensureDefaultProjectForFile(server.toNormalizedPath(file.path)), project); const indexOfX = file.content.indexOf("x"); assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { kind: ScriptElementKind.variableElement, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a9fe9a32556dc..8beeb7a1ae5db 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7252,6 +7252,7 @@ declare namespace ts { function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean; function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean; function tryRemoveDirectoryPrefix(path: string, dirPath: string): string | undefined; + function startsWithDirectory(path: string, dirPath: string): boolean; function hasExtension(fileName: string): boolean; const commonPackageFolders: ReadonlyArray; function getRegularExpressionForWildcard(specs: ReadonlyArray | undefined, basePath: string, usage: "files" | "directories" | "exclude"): string | undefined; @@ -12437,7 +12438,7 @@ declare namespace ts.server.protocol { command: CommandTypes.GetEditsForFileRename; arguments: GetEditsForFileRenameRequestArgs; } - interface GetEditsForFileRenameRequestArgs extends FileRequestArgs { + interface GetEditsForFileRenameRequestArgs { readonly oldFilePath: string; readonly newFilePath: string; } @@ -13800,7 +13801,13 @@ declare namespace ts.server { private delayUpdateProjectGraphs; setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void; findProject(projectName: string): Project | undefined; - getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined; + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject?: boolean): Project | undefined; + ensureDefaultProjectForFile(fileName: NormalizedPath): Project; + private doEnsureDefaultProjectForFile; + tryGetSomeFileInDirectory(directoryPath: NormalizedPath): { + readonly file: NormalizedPath; + readonly project: Project; + } | undefined; getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string): ScriptInfo | undefined; private ensureProjectStructuresUptoDate; getFormatCodeOptions(file: NormalizedPath): FormatCodeSettings; @@ -13988,6 +13995,7 @@ declare namespace ts.server { private getPosition; private getPositionInFile; private getFileAndProject; + private getFileAndProjectForFileRename; private getFileAndLanguageServiceForSyntacticOperation; private getFileAndProjectWorker; private getOutliningSpans; From a8723a06b0a6340f541bbffa1e9bf350bb9df8fa Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 18 Jun 2018 14:05:31 -0700 Subject: [PATCH 2/7] Code review: check new location first, and use scriptInfo.getDefaultProject() --- src/server/editorServices.ts | 2 +- src/server/session.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 532265decdb47..9f411c8a97193 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -716,7 +716,7 @@ namespace ts.server { }); if (!scriptInfo) return undefined; const file = toNormalizedPath(scriptInfo.path); - return { file, project: this.ensureDefaultProjectForFile(file) }; + return { file, project: scriptInfo.getDefaultProject() }; } getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { diff --git a/src/server/session.ts b/src/server/session.ts index 1df222aa2ba72..7a6f0f4cfa33e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1147,15 +1147,15 @@ namespace ts.server { } private getFileAndProjectForFileRename(args: protocol.GetEditsForFileRenameRequestArgs): FileAndProject { - const oldFilePath = toNormalizedPath(args.oldFilePath); - const oldProject = this.projectService.getDefaultProjectForFile(oldFilePath); - if (oldProject) return { file: oldFilePath, project: oldProject }; - const newFilePath = toNormalizedPath(args.newFilePath); const newProject = this.projectService.getDefaultProjectForFile(newFilePath); if (newProject) return { file: newFilePath, project: newProject }; - return Debug.assertDefined(this.projectService.tryGetSomeFileInDirectory(oldFilePath) || this.projectService.tryGetSomeFileInDirectory(newFilePath)); + const oldFilePath = toNormalizedPath(args.oldFilePath); + const oldProject = this.projectService.getDefaultProjectForFile(oldFilePath); + if (oldProject) return { file: oldFilePath, project: oldProject }; + + return Debug.assertDefined(this.projectService.tryGetSomeFileInDirectory(newFilePath) || this.projectService.tryGetSomeFileInDirectory(oldFilePath)); } private getFileAndLanguageServiceForSyntacticOperation(args: protocol.FileRequestArgs) { From cf16bdee7f445f87cc24797a364e803ffc544088 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 18 Jun 2018 14:49:15 -0700 Subject: [PATCH 3/7] Remove changes to e getDefaultProjectForFile (now #25060) --- src/server/editorServices.ts | 24 +++++++++---------- src/server/session.ts | 12 +++++----- .../unittests/tsserverProjectSystem.ts | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9f411c8a97193..b08d7c3fcb79f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -692,19 +692,17 @@ namespace ts.server { return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } - getDefaultProjectForFile(fileName: NormalizedPath, ensureProject = false): Project | undefined { - const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); - return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : ensureProject ? this.doEnsureDefaultProjectForFile(fileName) : undefined; - } - - ensureDefaultProjectForFile(fileName: NormalizedPath): Project { - return this.getDefaultProjectForFile(fileName) || this.doEnsureDefaultProjectForFile(fileName); - } - - private doEnsureDefaultProjectForFile(fileName: NormalizedPath): Project { - this.ensureProjectStructuresUptoDate(); - const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); - return scriptInfo ? scriptInfo.getDefaultProject() : Errors.ThrowNoProject(); + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined { + let scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + if (ensureProject && (!scriptInfo || scriptInfo.isOrphan())) { + this.ensureProjectStructuresUptoDate(); + scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + if (!scriptInfo) { + return Errors.ThrowNoProject(); + } + return scriptInfo.getDefaultProject(); + } + return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined; } /* @internal */ diff --git a/src/server/session.ts b/src/server/session.ts index 7a6f0f4cfa33e..c32a9dfab6d69 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1148,11 +1148,11 @@ namespace ts.server { private getFileAndProjectForFileRename(args: protocol.GetEditsForFileRenameRequestArgs): FileAndProject { const newFilePath = toNormalizedPath(args.newFilePath); - const newProject = this.projectService.getDefaultProjectForFile(newFilePath); + const newProject = this.projectService.getDefaultProjectForFile(newFilePath, /*ensureProject*/ false); if (newProject) return { file: newFilePath, project: newProject }; const oldFilePath = toNormalizedPath(args.oldFilePath); - const oldProject = this.projectService.getDefaultProjectForFile(oldFilePath); + const oldProject = this.projectService.getDefaultProjectForFile(oldFilePath, /*ensureProject*/ false); if (oldProject) return { file: oldFilePath, project: oldProject }; return Debug.assertDefined(this.projectService.tryGetSomeFileInDirectory(newFilePath) || this.projectService.tryGetSomeFileInDirectory(oldFilePath)); @@ -1162,7 +1162,7 @@ namespace ts.server { // Since this is syntactic operation, there should always be project for the file // we wouldnt have to ensure project but rather throw if we dont get project const file = toNormalizedPath(args.file); - const project = this.getProject(args.projectFileName) || this.projectService.getDefaultProjectForFile(file); + const project = this.getProject(args.projectFileName) || this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ false); if (!project) { return Errors.ThrowNoProject(); } @@ -1174,7 +1174,7 @@ namespace ts.server { private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string | undefined): { file: NormalizedPath, project: Project } { const file = toNormalizedPath(uncheckedFileName); - const project = this.getProject(projectFileName) || this.projectService.ensureDefaultProjectForFile(file); + const project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ true)!; // TODO: GH#18217 return { file, project }; } @@ -1466,7 +1466,7 @@ namespace ts.server { private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] { return mapDefined(fileNames, uncheckedFileName => { const fileName = toNormalizedPath(uncheckedFileName); - const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName); + const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*ensureProject*/ false); return project && { fileName, project }; }); } @@ -1864,7 +1864,7 @@ namespace ts.server { const lowPriorityFiles: NormalizedPath[] = []; const veryLowPriorityFiles: NormalizedPath[] = []; const normalizedFileName = toNormalizedPath(fileName); - const project = this.projectService.ensureDefaultProjectForFile(normalizedFileName); + const project = this.projectService.getDefaultProjectForFile(normalizedFileName, /*ensureProject*/ true)!; for (const fileNameInProject of fileNamesInProject) { if (this.getCanonicalFileName(fileNameInProject) === this.getCanonicalFileName(fileName)) { highPriorityFiles.push(fileNameInProject); diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index aad546c0ffeed..feb7377a86852 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -3097,7 +3097,7 @@ namespace ts.projectSystem { checkProjectRootFiles(project, [file.path]); checkProjectActualFiles(project, [file.path, libFile.path]); - assert.strictEqual(projectService.ensureDefaultProjectForFile(server.toNormalizedPath(file.path)), project); + assert.strictEqual(projectService.getDefaultProjectForFile(server.toNormalizedPath(file.path), /*ensureProject*/ true), project); const indexOfX = file.content.indexOf("x"); assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { kind: ScriptElementKind.variableElement, From 8eedd3432f670b0eb8fc20f126f1e52053c3e134 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 19 Jun 2018 11:54:32 -0700 Subject: [PATCH 4/7] Undo API changes (#24966) --- tests/baselines/reference/api/tsserverlibrary.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8beeb7a1ae5db..8f0e9542014e4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -13801,9 +13801,7 @@ declare namespace ts.server { private delayUpdateProjectGraphs; setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void; findProject(projectName: string): Project | undefined; - getDefaultProjectForFile(fileName: NormalizedPath, ensureProject?: boolean): Project | undefined; - ensureDefaultProjectForFile(fileName: NormalizedPath): Project; - private doEnsureDefaultProjectForFile; + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined; tryGetSomeFileInDirectory(directoryPath: NormalizedPath): { readonly file: NormalizedPath; readonly project: Project; From bb2e407cafdca1854bcf6a77f149512c62cf76e0 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 19 Jun 2018 14:34:12 -0700 Subject: [PATCH 5/7] Combine edits from all projects (fixes #25052) --- src/server/editorServices.ts | 27 +++++---- src/server/session.ts | 49 +++++++++------ .../unittests/tsserverProjectSystem.ts | 60 ++++++++++++++++++- 3 files changed, 102 insertions(+), 34 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b08d7c3fcb79f..f7c1d036f84c2 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -692,6 +692,13 @@ namespace ts.server { return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } + /* @internal */ + forEachProject(cb: (project: Project) => void) { + for (const p of this.inferredProjects) cb(p); + this.configuredProjects.forEach(cb); + this.externalProjects.forEach(cb); + } + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined { let scriptInfo = this.getScriptInfoForNormalizedPath(fileName); if (ensureProject && (!scriptInfo || scriptInfo.isOrphan())) { @@ -705,18 +712,6 @@ namespace ts.server { return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined; } - /* @internal */ - tryGetSomeFileInDirectory(directoryPath: NormalizedPath): { readonly file: NormalizedPath, readonly project: Project } | undefined { - const scriptInfo = forEachEntry(this.filenameToScriptInfo, scriptInfo => { - if (startsWithDirectory(scriptInfo.path, directoryPath)) { - return scriptInfo; - } - }); - if (!scriptInfo) return undefined; - const file = toNormalizedPath(scriptInfo.path); - return { file, project: scriptInfo.getDefaultProject() }; - } - getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { this.ensureProjectStructuresUptoDate(); return this.getScriptInfo(uncheckedFileName); @@ -754,6 +749,14 @@ namespace ts.server { return info && info.getPreferences() || this.hostConfiguration.preferences; } + getHostFormatCodeOptions(): FormatCodeSettings { + return this.hostConfiguration.formatCodeOptions; + } + + getHostPreferences(): UserPreferences { + return this.hostConfiguration.preferences; + } + private onSourceFileChanged(fileName: string, eventKind: FileWatcherEventKind, path: Path) { const info = this.getScriptInfoForPath(path); if (!info) { diff --git a/src/server/session.ts b/src/server/session.ts index c32a9dfab6d69..28f46b2966a4d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1146,18 +1146,6 @@ namespace ts.server { return this.getFileAndProjectWorker(args.file, args.projectFileName); } - private getFileAndProjectForFileRename(args: protocol.GetEditsForFileRenameRequestArgs): FileAndProject { - const newFilePath = toNormalizedPath(args.newFilePath); - const newProject = this.projectService.getDefaultProjectForFile(newFilePath, /*ensureProject*/ false); - if (newProject) return { file: newFilePath, project: newProject }; - - const oldFilePath = toNormalizedPath(args.oldFilePath); - const oldProject = this.projectService.getDefaultProjectForFile(oldFilePath, /*ensureProject*/ false); - if (oldProject) return { file: oldFilePath, project: oldProject }; - - return Debug.assertDefined(this.projectService.tryGetSomeFileInDirectory(newFilePath) || this.projectService.tryGetSomeFileInDirectory(oldFilePath)); - } - private getFileAndLanguageServiceForSyntacticOperation(args: protocol.FileRequestArgs) { // Since this is syntactic operation, there should always be project for the file // we wouldnt have to ensure project but rather throw if we dont get project @@ -1743,9 +1731,22 @@ namespace ts.server { } private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { - const { file, project } = this.getFileAndProjectForFileRename(args); - const changes = project.getLanguageService().getEditsForFileRename(toNormalizedPath(args.oldFilePath), toNormalizedPath(args.newFilePath), this.getFormatOptions(file), this.getPreferences(file)); - return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; + const oldPath = toNormalizedPath(args.oldFilePath); + const newPath = toNormalizedPath(args.newFilePath); + const formatOptions = this.getHostFormatOptions(); + const preferences = this.getHostPreferences(); + + const changes: (protocol.FileCodeEdits | FileTextChanges)[] = []; + this.projectService.forEachProject(project => { + for (const fileTextChanges of project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences)) { + // Subsequent projects may make conflicting edits to the same file -- just go with the first. + if (!changes.some(f => f.fileName === fileTextChanges.fileName)) { + changes.push(simplifiedResult ? this.mapTextChangeToCodeEdit(project, fileTextChanges) : fileTextChanges); + } + } + }); + + return changes as ReadonlyArray | ReadonlyArray; } private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray | undefined { @@ -1815,10 +1816,12 @@ namespace ts.server { } private mapTextChangesToCodeEdits(project: Project, textChanges: ReadonlyArray): protocol.FileCodeEdits[] { - return textChanges.map(change => { - const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName)); - return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path)); - }); + return textChanges.map(change => this.mapTextChangeToCodeEdit(project, change)); + } + + private mapTextChangeToCodeEdit(project: Project, change: FileTextChanges): protocol.FileCodeEdits { + const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName)); + return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path)); } private convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit { @@ -2305,6 +2308,14 @@ namespace ts.server { private getPreferences(file: NormalizedPath): UserPreferences { return this.projectService.getPreferences(file); } + + private getHostFormatOptions(): FormatCodeSettings { + return this.projectService.getHostFormatCodeOptions(); + } + + private getHostPreferences(): UserPreferences { + return this.projectService.getHostPreferences(); + } } interface FileAndProject { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index feb7377a86852..62ecdf6334ab9 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -421,6 +421,18 @@ namespace ts.projectSystem { return createTextSpan(start, substring.length); } + function protocolTextSpanFromSubstring(str: string, substring: string): protocol.TextSpan { + const start = str.indexOf(substring); + Debug.assert(start !== -1); + const lineStarts = computeLineStarts(str); + const toLocation = (pos: number) => lineAndCharacterToLocation(computeLineAndCharacterOfPosition(lineStarts, pos)); + return { start: toLocation(start), end: toLocation(start + substring.length) }; + } + + function lineAndCharacterToLocation(lc: LineAndCharacter): protocol.Location { + return { line: lc.line + 1, offset: lc.character + 1 }; + } + /** * Test server cancellation token used to mock host token cancellation requests. * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls @@ -464,14 +476,13 @@ namespace ts.projectSystem { } } - export function makeSessionRequest(command: string, args: T) { - const newRequest: protocol.Request = { + export function makeSessionRequest(command: string, args: T): protocol.Request { + return { seq: 0, type: "request", command, arguments: args }; - return newRequest; } export function openFilesForSession(files: ReadonlyArray, session: server.Session) { @@ -8682,6 +8693,49 @@ export const x = 10;` }], }]); }); + + it("works with multiple projects", () => { + const aUserTs: File = { + path: "/a/user.ts", + content: 'import { x } from "./old";', + }; + const aOldTs: File = { + path: "/a/old.ts", + content: "export const x = 0;", + }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: "{}", + }; + const bUserTs: File = { + path: "/b/user.ts", + content: 'import { x } from "../a/old";', + }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aUserTs, bUserTs], session); + + const renameRequest = makeSessionRequest(CommandNames.GetEditsForFileRename, { + oldFilePath: "/a/old.ts", + newFilePath: "/a/new.ts", + }); + const response = session.executeCommand(renameRequest).response as protocol.GetEditsForFileRenameResponse["body"]; + assert.deepEqual(response, [ + { + fileName: aUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], + }, + { + fileName: bUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], + }, + ]); + }); }); describe("tsserverProjectSystem document registry in project service", () => { From 83e2a2cbb4baf69598e288af105ef6a421c2cb59 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 19 Jun 2018 14:52:49 -0700 Subject: [PATCH 6/7] Update API (#24966) --- tests/baselines/reference/api/tsserverlibrary.d.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8f0e9542014e4..9e9d8974b8788 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -13801,15 +13801,14 @@ declare namespace ts.server { private delayUpdateProjectGraphs; setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void; findProject(projectName: string): Project | undefined; + forEachProject(cb: (project: Project) => void): void; getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined; - tryGetSomeFileInDirectory(directoryPath: NormalizedPath): { - readonly file: NormalizedPath; - readonly project: Project; - } | undefined; getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string): ScriptInfo | undefined; private ensureProjectStructuresUptoDate; getFormatCodeOptions(file: NormalizedPath): FormatCodeSettings; getPreferences(file: NormalizedPath): UserPreferences; + getHostFormatCodeOptions(): FormatCodeSettings; + getHostPreferences(): UserPreferences; private onSourceFileChanged; private handleDeletedFile; watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject): FileWatcher; @@ -13993,7 +13992,6 @@ declare namespace ts.server { private getPosition; private getPositionInFile; private getFileAndProject; - private getFileAndProjectForFileRename; private getFileAndLanguageServiceForSyntacticOperation; private getFileAndProjectWorker; private getOutliningSpans; @@ -14041,6 +14039,7 @@ declare namespace ts.server { private mapCodeAction; private mapCodeFixAction; private mapTextChangesToCodeEdits; + private mapTextChangeToCodeEdit; private convertTextChangeToCodeEdit; private getBraceMatching; private getDiagnosticsForProject; @@ -14057,6 +14056,8 @@ declare namespace ts.server { onMessage(message: string): void; private getFormatOptions; private getPreferences; + private getHostFormatOptions; + private getHostPreferences; } interface HandlerResponse { response?: {}; From 5f749c49afa3eb42ae61d0ae48996df58663a446 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 22 Jun 2018 12:18:55 -0700 Subject: [PATCH 7/7] Ignore orphan projects or projects with language service disabled --- src/server/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/session.ts b/src/server/session.ts index 4dc759bcd7c6f..8b9bb32ef40c4 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1745,6 +1745,7 @@ namespace ts.server { const changes: (protocol.FileCodeEdits | FileTextChanges)[] = []; this.projectService.forEachProject(project => { + if (project.isOrphan() || !project.languageServiceEnabled) return; for (const fileTextChanges of project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences)) { // Subsequent projects may make conflicting edits to the same file -- just go with the first. if (!changes.some(f => f.fileName === fileTextChanges.fileName)) {