From 505189f0fc30c244c98276d5020aec4505589892 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 19 Apr 2018 15:29:11 -0700 Subject: [PATCH 1/3] Add 'renameFile' command to services --- src/compiler/core.ts | 9 ++++++ src/compiler/moduleNameResolver.ts | 6 ++++ src/compiler/program.ts | 10 +++--- src/compiler/types.ts | 2 ++ src/harness/fourslash.ts | 19 +++++++++++ src/harness/harnessLanguageService.ts | 3 ++ src/harness/tsconfig.json | 1 + src/harness/unittests/session.ts | 2 ++ src/server/client.ts | 4 +++ src/server/protocol.ts | 19 +++++++++++ src/server/session.ts | 14 ++++++++- src/server/tsconfig.json | 1 + src/server/tsconfig.library.json | 1 + src/services/codefixes/importFixes.ts | 8 +---- src/services/renameFile.ts | 45 +++++++++++++++++++++++++++ src/services/services.ts | 11 +++++-- src/services/tsconfig.json | 1 + src/services/types.ts | 1 + src/services/utilities.ts | 8 +++++ tests/cases/fourslash/fourslash.ts | 5 +++ tests/cases/fourslash/renameFile.ts | 20 ++++++++++++ 21 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 src/services/renameFile.ts create mode 100644 tests/cases/fourslash/renameFile.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 0e53ae2946d2a..f0220fb8c93a5 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2212,6 +2212,15 @@ namespace ts { return absolutePath; } + export function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return ensurePathIsRelative(relativePath); + } + + export function ensurePathIsRelative(path: string): string { + return !pathIsRelative(path) ? "./" + path : path; + } + export function getBaseFileName(path: string) { if (path === undefined) { return undefined; diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 0027687c643cb..81aed5224902f 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -444,6 +444,12 @@ namespace ts { } } + export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined { + const containingDirectory = getDirectoryPath(containingFile); + const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory); + return perFolderCache && perFolderCache.get(moduleName); + } + export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations { const traceEnabled = isTraceEnabled(compilerOptions, host); if (traceEnabled) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 9eb82661cf56c..75cdd07845f2c 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -622,9 +622,6 @@ namespace ts { Debug.assert(!!missingFilePaths); - // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks - moduleResolutionCache = undefined; - // Release any files we have acquired in the old program but are // not part of the new program. if (oldProgram && host.onReleaseOldSourceFile) { @@ -670,7 +667,8 @@ namespace ts { sourceFileToPackageName, redirectTargetsSet, isEmittedFile, - getConfigFileParsingDiagnostics + getConfigFileParsingDiagnostics, + getResolvedModuleWithFailedLookupLocationsFromCache, }; verifyCompilerOptions(); @@ -679,6 +677,10 @@ namespace ts { return program; + function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations { + return moduleResolutionCache && resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache); + } + function toPath(fileName: string): Path { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4ea4b7ab8cc05..9c149d145e5b8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2725,6 +2725,8 @@ namespace ts { /* @internal */ redirectTargetsSet: Map; /** Is the file emitted file */ /* @internal */ isEmittedFile(file: string): boolean; + + /* @internal */ getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined; } /* @internal */ diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 75915c915ed17..69a4966bab058 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3279,6 +3279,15 @@ Actual: ${stringify(fullActual)}`); private static textSpansEqual(a: ts.TextSpan, b: ts.TextSpan) { return a && b && a.start === b.start && a.length === b.length; } + + public renameFile(options: FourSlashInterface.RenameFileOptions): void { + const changes = this.languageService.renameFile(options.oldPath, options.newPath, this.formatCodeSettings); + this.applyChanges(changes); + for (const fileName in options.newFileContents) { + this.openFile(fileName); + this.verifyCurrentFileContent(options.newFileContents[fileName]); + } + } } export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) { @@ -4370,6 +4379,10 @@ namespace FourSlashInterface { public allRangesAppearInImplementationList(markerName: string) { this.state.verifyRangesInImplementationList(markerName); } + + public renameFile(options: RenameFileOptions) { + this.state.renameFile(options); + } } export class Edit { @@ -4710,4 +4723,10 @@ namespace FourSlashInterface { range?: FourSlash.Range; code: number; } + + export interface RenameFileOptions { + readonly oldPath: string; + readonly newPath: string; + readonly newFileContents: { readonly [fileName: string]: string }; + } } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 17788f6251d71..34b819cc2cf2f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -528,6 +528,9 @@ namespace Harness.LanguageService { organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray { throw new Error("Not supported on the shim."); } + renameFile(): ReadonlyArray { + throw new Error("Not supported on the shim."); + } getEmitOutput(fileName: string): ts.EmitOutput { return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 3ad653403cf7e..1e93686a7cc05 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -70,6 +70,7 @@ "../services/navigateTo.ts", "../services/navigationBar.ts", "../services/organizeImports.ts", + "../services/renameFile.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", "../services/preProcess.ts", diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index a892d28808cf8..828a691d9cd22 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -263,6 +263,8 @@ namespace ts.server { CommandNames.GetEditsForRefactorFull, CommandNames.OrganizeImports, CommandNames.OrganizeImportsFull, + CommandNames.RenameFile, + CommandNames.RenameFileFull, ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/src/server/client.ts b/src/server/client.ts index 2238734e90c2c..af6fba62b5345 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -632,6 +632,10 @@ namespace ts.server { return notImplemented(); } + renameFile() { + return notImplemented(); + } + private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { return edits.map(edit => { const fileName = edit.fileName; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 7a258a904c202..d1db9f74cfb60 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -121,6 +121,9 @@ namespace ts.server.protocol { OrganizeImports = "organizeImports", /* @internal */ OrganizeImportsFull = "organizeImports-full", + RenameFile = "renameFile", + /* @internal */ + RenameFileFull = "renameFile-full", // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. } @@ -610,6 +613,22 @@ namespace ts.server.protocol { edits: ReadonlyArray; } + export interface RenameFileRequest extends Request { + command: CommandTypes.RenameFile; + arguments: RenameFileRequestArgs; + } + + // 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 RenameFileRequestArgs extends FileRequestArgs { + readonly oldFilePath: string; + readonly newFilePath: string; + } + + export interface RenameFileResponse extends Response { + edits: ReadonlyArray; + } + /** * Request for the available codefixes at a specific position. */ diff --git a/src/server/session.ts b/src/server/session.ts index 27044ec369dd1..42df7fcc7feb7 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1664,6 +1664,12 @@ namespace ts.server { } } + private renameFile(args: protocol.RenameFileRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { + const { file, project } = this.getFileAndProject(args); + const changes = project.getLanguageService().renameFile(args.oldFilePath, args.newFilePath, this.getFormatOptions(file)); + return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; + } + private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { if (args.errorCodes.length === 0) { return undefined; @@ -2117,7 +2123,13 @@ namespace ts.server { }, [CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => { return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false)); - } + }, + [CommandNames.RenameFile]: (request: protocol.RenameFileRequest) => { + return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.RenameFileFull]: (request: protocol.RenameFileRequest) => { + return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ false)); + }, }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index dc732604ab6c4..90c4ff0bb7b99 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -66,6 +66,7 @@ "../services/navigateTo.ts", "../services/navigationBar.ts", "../services/organizeImports.ts", + "../services/renameFile.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", "../services/preProcess.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index dd0196fab766d..bafbdf3c3e91b 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -72,6 +72,7 @@ "../services/navigateTo.ts", "../services/navigationBar.ts", "../services/organizeImports.ts", + "../services/renameFile.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", "../services/preProcess.ts", diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index ddd530e4246f1..5a10f2f48a739 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -39,7 +39,6 @@ namespace ts.codefix { } function convertToImportCodeFixContext(context: CodeFixContext, symbolToken: Node, symbolName: string): ImportCodeFixContext { - const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false; const { program } = context; const checker = program.getTypeChecker(); @@ -51,7 +50,7 @@ namespace ts.codefix { checker, compilerOptions: program.getCompilerOptions(), cachedImportDeclarations: [], - getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames), + getCanonicalFileName: createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(context.host)), symbolName, symbolToken, preferences: context.preferences, @@ -547,11 +546,6 @@ namespace ts.codefix { return startsWith(path, ".."); } - function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath; - } - function getCodeActionsForAddImport( exportInfos: ReadonlyArray, ctx: ImportCodeFixContext, diff --git a/src/services/renameFile.ts b/src/services/renameFile.ts new file mode 100644 index 0000000000000..e6b34fdad8330 --- /dev/null +++ b/src/services/renameFile.ts @@ -0,0 +1,45 @@ +/* @internal */ +namespace ts { + export function renameFile(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { + const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host); + return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { + const importsToUpdate = getImportsToUpdate(program, oldFilePath); + for (const importToUpdate of importsToUpdate) { + const newPath = pathUpdater(importToUpdate.text); + if (newPath !== undefined) { + changeTracker.replaceNode(importToUpdate.getSourceFile(), importToUpdate, updateStringLiteralLike(importToUpdate, newPath)); + } + } + }); + } + + function getImportsToUpdate(program: Program, oldFilePath: string): ReadonlyArray { + const checker = program.getTypeChecker(); + const result: StringLiteralLike[] = []; + for (const file of program.getSourceFiles()) { + for (const importStringLiteral of file.imports) { + // If it resolved to something already, ignore. + if (checker.getSymbolAtLocation(importStringLiteral)) continue; + + const resolved = program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, file.fileName); + if (contains(resolved.failedLookupLocations, oldFilePath)) { + result.push(importStringLiteral); + } + } + } + return result; + } + + function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { + // Get the relative path from old to new location, and append it on to the end of imports and normalize. + const rel = removeFileExtension(getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)))); + return oldPath => { + if (!pathIsRelative(oldPath)) return; + return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); + }; + } + + function updateStringLiteralLike(old: StringLiteralLike, newText: string): StringLiteralLike { + return old.kind === SyntaxKind.StringLiteral ? createLiteral(newText, /*isSingleQuote*/ old.singleQuote) : createNoSubstitutionTemplateLiteral(newText); + } +} diff --git a/src/services/services.ts b/src/services/services.ts index a192231405ae9..4aae43a39ca81 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1128,7 +1128,6 @@ namespace ts { let lastProjectVersion: string; let lastTypesRootVersion = 0; - const useCaseSensitivefileNames = host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(); const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken()); const currentDirectory = host.getCurrentDirectory(); @@ -1145,7 +1144,8 @@ namespace ts { } } - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitivefileNames); + const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); function getValidSourceFile(fileName: string): SourceFile { const sourceFile = program.getSourceFile(fileName); @@ -1202,7 +1202,7 @@ namespace ts { getSourceFileByPath: getOrCreateSourceFileByPath, getCancellationToken: () => cancellationToken, getCanonicalFileName, - useCaseSensitiveFileNames: () => useCaseSensitivefileNames, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)), getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: noop, @@ -1950,6 +1950,10 @@ namespace ts { return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); } + function renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray { + return ts.renameFile(getProgram(), oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions)); + } + function applyCodeActionCommand(action: CodeActionCommand): Promise; function applyCodeActionCommand(action: CodeActionCommand[]): Promise; function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; @@ -2250,6 +2254,7 @@ namespace ts { getCombinedCodeFix, applyCodeActionCommand, organizeImports, + renameFile, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 31b35a44271ad..3cfcbcb161867 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -63,6 +63,7 @@ "navigateTo.ts", "navigationBar.ts", "organizeImports.ts", + "../services/renameFile.ts", "outliningElementsCollector.ts", "patternMatcher.ts", "preProcess.ts", diff --git a/src/services/types.ts b/src/services/types.ts index 01b8b5ad4d461..4ff3fd153b68f 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -334,6 +334,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; + renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 631706782025c..cd87824e7d682 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1213,6 +1213,14 @@ namespace ts { ? isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined : getTextOfIdentifierOrLiteral(name); } + + export function hostUsesCaseSensitiveFileNames(host: LanguageServiceHost): boolean { + return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false; + } + + export function hostGetCanonicalFileName(host: LanguageServiceHost): GetCanonicalFileName { + return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); + } } // Display-part writer helpers diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 697d2d725553c..8d5851986a41c 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -344,6 +344,11 @@ declare namespace FourSlashInterface { getSuggestionDiagnostics(expected: ReadonlyArray): void; ProjectInfo(expected: string[]): void; allRangesAppearInImplementationList(markerName: string): void; + renameFile(options: { + oldPath: string; + newPath: string; + newFileContents: { [fileName: string]: string }; + }); } class edit { backspace(count?: number): void; diff --git a/tests/cases/fourslash/renameFile.ts b/tests/cases/fourslash/renameFile.ts new file mode 100644 index 0000000000000..507b84c900ccf --- /dev/null +++ b/tests/cases/fourslash/renameFile.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////import old from "./src/old"; + +// @Filename: /src/a.ts +////import old from "./old"; + +// @Filename: /src/foo/a.ts +////import old from "../old"; + +verify.renameFile({ + oldPath: "/src/old.ts", + newPath: "/src/new.ts", + newFileContents: { + "/a.ts": 'import old from "./src/new";', + "/src/a.ts": 'import old from "./new";', + "/src/foo/a.ts": 'import old from "../new";', + }, +}); From 9b94a8fe50a651d81d799948e0712c7ee0f2c0a9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 20 Apr 2018 12:45:53 -0700 Subject: [PATCH 2/3] renameFile -> getEditsForFileRename --- src/harness/fourslash.ts | 10 +++++----- src/harness/harnessLanguageService.ts | 2 +- src/harness/tsconfig.json | 2 +- src/harness/unittests/session.ts | 4 ++-- src/server/client.ts | 2 +- src/server/protocol.ts | 14 +++++++------- src/server/session.ts | 12 ++++++------ src/server/tsconfig.json | 2 +- src/server/tsconfig.library.json | 2 +- .../{renameFile.ts => getEditsForFileRename.ts} | 2 +- src/services/services.ts | 6 +++--- src/services/tsconfig.json | 2 +- src/services/types.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 17 ++++++++++++++++- tests/baselines/reference/api/typescript.d.ts | 2 ++ tests/cases/fourslash/fourslash.ts | 2 +- .../{renameFile.ts => getEditsForFileRename.ts} | 2 +- 17 files changed, 51 insertions(+), 34 deletions(-) rename src/services/{renameFile.ts => getEditsForFileRename.ts} (90%) rename tests/cases/fourslash/{renameFile.ts => getEditsForFileRename.ts} (93%) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 69a4966bab058..c9770be898323 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3280,8 +3280,8 @@ Actual: ${stringify(fullActual)}`); return a && b && a.start === b.start && a.length === b.length; } - public renameFile(options: FourSlashInterface.RenameFileOptions): void { - const changes = this.languageService.renameFile(options.oldPath, options.newPath, this.formatCodeSettings); + public getEditsForFileRename(options: FourSlashInterface.GetEditsForFileRenameOptions): void { + const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings); this.applyChanges(changes); for (const fileName in options.newFileContents) { this.openFile(fileName); @@ -4380,8 +4380,8 @@ namespace FourSlashInterface { this.state.verifyRangesInImplementationList(markerName); } - public renameFile(options: RenameFileOptions) { - this.state.renameFile(options); + public getEditsForFileRename(options: GetEditsForFileRenameOptions) { + this.state.getEditsForFileRename(options); } } @@ -4724,7 +4724,7 @@ namespace FourSlashInterface { code: number; } - export interface RenameFileOptions { + export interface GetEditsForFileRenameOptions { readonly oldPath: string; readonly newPath: string; readonly newFileContents: { readonly [fileName: string]: string }; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 34b819cc2cf2f..0063a4730f1c0 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -528,7 +528,7 @@ namespace Harness.LanguageService { organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray { throw new Error("Not supported on the shim."); } - renameFile(): ReadonlyArray { + getEditsForFileRename(): ReadonlyArray { throw new Error("Not supported on the shim."); } getEmitOutput(fileName: string): ts.EmitOutput { diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 1e93686a7cc05..a4ad8cb37975f 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -70,7 +70,7 @@ "../services/navigateTo.ts", "../services/navigationBar.ts", "../services/organizeImports.ts", - "../services/renameFile.ts", + "../services/getEditsForFileRename.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", "../services/preProcess.ts", diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 828a691d9cd22..25df6f72ea623 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -263,8 +263,8 @@ namespace ts.server { CommandNames.GetEditsForRefactorFull, CommandNames.OrganizeImports, CommandNames.OrganizeImportsFull, - CommandNames.RenameFile, - CommandNames.RenameFileFull, + CommandNames.GetEditsForFileRename, + CommandNames.GetEditsForFileRenameFull, ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/src/server/client.ts b/src/server/client.ts index af6fba62b5345..d7a83ee532094 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -632,7 +632,7 @@ namespace ts.server { return notImplemented(); } - renameFile() { + getEditsForFileRename() { return notImplemented(); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index d1db9f74cfb60..efc2e1bb23edc 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -121,9 +121,9 @@ namespace ts.server.protocol { OrganizeImports = "organizeImports", /* @internal */ OrganizeImportsFull = "organizeImports-full", - RenameFile = "renameFile", + GetEditsForFileRename = "getEditsForFileRename", /* @internal */ - RenameFileFull = "renameFile-full", + GetEditsForFileRenameFull = "getEditsForFileRename-full", // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. } @@ -613,19 +613,19 @@ namespace ts.server.protocol { edits: ReadonlyArray; } - export interface RenameFileRequest extends Request { - command: CommandTypes.RenameFile; - arguments: RenameFileRequestArgs; + export interface GetEditsForFileRenameRequest extends Request { + command: CommandTypes.GetEditsForFileRename; + 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 RenameFileRequestArgs extends FileRequestArgs { + export interface GetEditsForFileRenameRequestArgs extends FileRequestArgs { readonly oldFilePath: string; readonly newFilePath: string; } - export interface RenameFileResponse extends Response { + export interface GetEditsForFileRenameResponse extends Response { edits: ReadonlyArray; } diff --git a/src/server/session.ts b/src/server/session.ts index 42df7fcc7feb7..67d00aa8b097d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1664,9 +1664,9 @@ namespace ts.server { } } - private renameFile(args: protocol.RenameFileRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { + private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const changes = project.getLanguageService().renameFile(args.oldFilePath, args.newFilePath, this.getFormatOptions(file)); + const changes = project.getLanguageService().getEditsForFileRename(args.oldFilePath, args.newFilePath, this.getFormatOptions(file)); return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; } @@ -2124,11 +2124,11 @@ namespace ts.server { [CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => { return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.RenameFile]: (request: protocol.RenameFileRequest) => { - return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ true)); + [CommandNames.GetEditsForFileRename]: (request: protocol.GetEditsForFileRenameRequest) => { + return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ true)); }, - [CommandNames.RenameFileFull]: (request: protocol.RenameFileRequest) => { - return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ false)); + [CommandNames.GetEditsForFileRenameFull]: (request: protocol.GetEditsForFileRenameRequest) => { + return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ false)); }, }); diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 90c4ff0bb7b99..e6768f4edeed1 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -66,7 +66,7 @@ "../services/navigateTo.ts", "../services/navigationBar.ts", "../services/organizeImports.ts", - "../services/renameFile.ts", + "../services/getEditsForFileRename.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", "../services/preProcess.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index bafbdf3c3e91b..a167d3b39a2ff 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -72,7 +72,7 @@ "../services/navigateTo.ts", "../services/navigationBar.ts", "../services/organizeImports.ts", - "../services/renameFile.ts", + "../services/getEditsForFileRename.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", "../services/preProcess.ts", diff --git a/src/services/renameFile.ts b/src/services/getEditsForFileRename.ts similarity index 90% rename from src/services/renameFile.ts rename to src/services/getEditsForFileRename.ts index e6b34fdad8330..9b9d9e5f57cab 100644 --- a/src/services/renameFile.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,6 +1,6 @@ /* @internal */ namespace ts { - export function renameFile(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { + export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { const importsToUpdate = getImportsToUpdate(program, oldFilePath); diff --git a/src/services/services.ts b/src/services/services.ts index 4aae43a39ca81..ab3c1597a63f3 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1950,8 +1950,8 @@ namespace ts { return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); } - function renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray { - return ts.renameFile(getProgram(), oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions)); + function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray { + return ts.getEditsForFileRename(getProgram(), oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions)); } function applyCodeActionCommand(action: CodeActionCommand): Promise; @@ -2254,7 +2254,7 @@ namespace ts { getCombinedCodeFix, applyCodeActionCommand, organizeImports, - renameFile, + getEditsForFileRename, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 3cfcbcb161867..75b46a5c49bbe 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -63,7 +63,7 @@ "navigateTo.ts", "navigationBar.ts", "organizeImports.ts", - "../services/renameFile.ts", + "getEditsForFileRename.ts", "outliningElementsCollector.ts", "patternMatcher.ts", "preProcess.ts", diff --git a/src/services/types.ts b/src/services/types.ts index 4ff3fd153b68f..bab4d0884367e 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -334,7 +334,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index b9cf50f14d90b..7dea2455291d0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3402,6 +3402,7 @@ declare namespace ts { set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void; } function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): ModuleResolutionCache; + function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined; function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache): ResolvedModuleWithFailedLookupLocations; @@ -4446,6 +4447,7 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; dispose(): void; @@ -5409,7 +5411,8 @@ declare namespace ts.server.protocol { GetSupportedCodeFixes = "getSupportedCodeFixes", GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", - OrganizeImports = "organizeImports" + OrganizeImports = "organizeImports", + GetEditsForFileRename = "getEditsForFileRename" } /** * A TypeScript Server message @@ -5805,6 +5808,17 @@ declare namespace ts.server.protocol { interface OrganizeImportsResponse extends Response { edits: ReadonlyArray; } + interface GetEditsForFileRenameRequest extends Request { + command: CommandTypes.GetEditsForFileRename; + arguments: GetEditsForFileRenameRequestArgs; + } + interface GetEditsForFileRenameRequestArgs extends FileRequestArgs { + readonly oldFilePath: string; + readonly newFilePath: string; + } + interface GetEditsForFileRenameResponse extends Response { + edits: ReadonlyArray; + } /** * Request for the available codefixes at a specific position. */ @@ -8320,6 +8334,7 @@ declare namespace ts.server { private getApplicableRefactors; private getEditsForRefactor; private organizeImports; + private getEditsForFileRename; private getCodeFixes; private getCombinedCodeFix; private applyCodeActionCommand; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index f5114e2397c11..8e082a624fba2 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3402,6 +3402,7 @@ declare namespace ts { set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void; } function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): ModuleResolutionCache; + function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined; function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache): ResolvedModuleWithFailedLookupLocations; @@ -4446,6 +4447,7 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; dispose(): void; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 8d5851986a41c..5fc295e21bc54 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -344,7 +344,7 @@ declare namespace FourSlashInterface { getSuggestionDiagnostics(expected: ReadonlyArray): void; ProjectInfo(expected: string[]): void; allRangesAppearInImplementationList(markerName: string): void; - renameFile(options: { + getEditsForFileRename(options: { oldPath: string; newPath: string; newFileContents: { [fileName: string]: string }; diff --git a/tests/cases/fourslash/renameFile.ts b/tests/cases/fourslash/getEditsForFileRename.ts similarity index 93% rename from tests/cases/fourslash/renameFile.ts rename to tests/cases/fourslash/getEditsForFileRename.ts index 507b84c900ccf..802114414c388 100644 --- a/tests/cases/fourslash/renameFile.ts +++ b/tests/cases/fourslash/getEditsForFileRename.ts @@ -9,7 +9,7 @@ // @Filename: /src/foo/a.ts ////import old from "../old"; -verify.renameFile({ +verify.getEditsForFileRename({ oldPath: "/src/old.ts", newPath: "/src/new.ts", newFileContents: { From 81b36bc4859965fb2882dc2cabe21dec398cb567 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 20 Apr 2018 13:15:22 -0700 Subject: [PATCH 3/3] Support `` directives --- src/services/getEditsForFileRename.ts | 40 ++++++++++++------- src/services/textChanges.ts | 6 ++- .../cases/fourslash/getEditsForFileRename.ts | 9 +++-- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 9b9d9e5f57cab..60fbd5b70f3cc 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -3,27 +3,41 @@ namespace ts { export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { - const importsToUpdate = getImportsToUpdate(program, oldFilePath); - for (const importToUpdate of importsToUpdate) { - const newPath = pathUpdater(importToUpdate.text); + for (const { sourceFile, toUpdate } of getImportsToUpdate(program, oldFilePath)) { + const newPath = pathUpdater(isRef(toUpdate) ? toUpdate.fileName : toUpdate.text); if (newPath !== undefined) { - changeTracker.replaceNode(importToUpdate.getSourceFile(), importToUpdate, updateStringLiteralLike(importToUpdate, newPath)); + const range = isRef(toUpdate) ? toUpdate : createTextRange(toUpdate.getStart(sourceFile) + 1, toUpdate.end - 1); + changeTracker.replaceRangeWithText(sourceFile, range, isRef(toUpdate) ? newPath : removeFileExtension(newPath)); } } }); } - function getImportsToUpdate(program: Program, oldFilePath: string): ReadonlyArray { + interface ToUpdate { + readonly sourceFile: SourceFile; + readonly toUpdate: StringLiteralLike | FileReference; + } + function isRef(toUpdate: StringLiteralLike | FileReference): toUpdate is FileReference { + return "fileName" in toUpdate; + } + + function getImportsToUpdate(program: Program, oldFilePath: string): ReadonlyArray { const checker = program.getTypeChecker(); - const result: StringLiteralLike[] = []; - for (const file of program.getSourceFiles()) { - for (const importStringLiteral of file.imports) { + const result: ToUpdate[] = []; + for (const sourceFile of program.getSourceFiles()) { + for (const ref of sourceFile.referencedFiles) { + if (!program.getSourceFileFromReference(sourceFile, ref) && resolveTripleslashReference(ref.fileName, sourceFile.fileName) === oldFilePath) { + result.push({ sourceFile, toUpdate: ref }); + } + } + + for (const importStringLiteral of sourceFile.imports) { // If it resolved to something already, ignore. if (checker.getSymbolAtLocation(importStringLiteral)) continue; - const resolved = program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, file.fileName); + const resolved = program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName); if (contains(resolved.failedLookupLocations, oldFilePath)) { - result.push(importStringLiteral); + result.push({ sourceFile, toUpdate: importStringLiteral }); } } } @@ -32,14 +46,10 @@ namespace ts { function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { // Get the relative path from old to new location, and append it on to the end of imports and normalize. - const rel = removeFileExtension(getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)))); + const rel = getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); return oldPath => { if (!pathIsRelative(oldPath)) return; return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); }; } - - function updateStringLiteralLike(old: StringLiteralLike, newText: string): StringLiteralLike { - return old.kind === SyntaxKind.StringLiteral ? createLiteral(newText, /*isSingleQuote*/ old.singleQuote) : createNoSubstitutionTemplateLiteral(newText); - } } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index b54caf402b15b..4f431e28e0e11 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -365,8 +365,12 @@ namespace ts.textChanges { this.insertText(sourceFile, token.getStart(sourceFile), text); } + public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string) { + this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text }); + } + private insertText(sourceFile: SourceFile, pos: number, text: string): void { - this.changes.push({ kind: ChangeKind.Text, sourceFile, range: { pos, end: pos }, text }); + this.replaceRangeWithText(sourceFile, createTextRange(pos), text); } /** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */ diff --git a/tests/cases/fourslash/getEditsForFileRename.ts b/tests/cases/fourslash/getEditsForFileRename.ts index 802114414c388..b000ce28dd872 100644 --- a/tests/cases/fourslash/getEditsForFileRename.ts +++ b/tests/cases/fourslash/getEditsForFileRename.ts @@ -1,20 +1,23 @@ /// // @Filename: /a.ts +/////// ////import old from "./src/old"; // @Filename: /src/a.ts +/////// ////import old from "./old"; // @Filename: /src/foo/a.ts +/////// ////import old from "../old"; verify.getEditsForFileRename({ oldPath: "/src/old.ts", newPath: "/src/new.ts", newFileContents: { - "/a.ts": 'import old from "./src/new";', - "/src/a.ts": 'import old from "./new";', - "/src/foo/a.ts": 'import old from "../new";', + "/a.ts": '/// \nimport old from "./src/new";', + "/src/a.ts": '/// \nimport old from "./new";', + "/src/foo/a.ts": '/// \nimport old from "../new";', }, });