Skip to content

Commit 5c94bef

Browse files
author
Andy
authored
Add 'renameFile' command to services (#23573)
* Add 'renameFile' command to services * renameFile -> getEditsForFileRename * Support `<reference path>` directives
1 parent e65681a commit 5c94bef

24 files changed

+211
-17
lines changed

src/compiler/core.ts

+9
Original file line numberDiff line numberDiff line change
@@ -2212,6 +2212,15 @@ namespace ts {
22122212
return absolutePath;
22132213
}
22142214

2215+
export function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) {
2216+
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
2217+
return ensurePathIsRelative(relativePath);
2218+
}
2219+
2220+
export function ensurePathIsRelative(path: string): string {
2221+
return !pathIsRelative(path) ? "./" + path : path;
2222+
}
2223+
22152224
export function getBaseFileName(path: string) {
22162225
if (path === undefined) {
22172226
return undefined;

src/compiler/moduleNameResolver.ts

+6
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ namespace ts {
444444
}
445445
}
446446

447+
export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined {
448+
const containingDirectory = getDirectoryPath(containingFile);
449+
const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory);
450+
return perFolderCache && perFolderCache.get(moduleName);
451+
}
452+
447453
export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
448454
const traceEnabled = isTraceEnabled(compilerOptions, host);
449455
if (traceEnabled) {

src/compiler/program.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -622,9 +622,6 @@ namespace ts {
622622

623623
Debug.assert(!!missingFilePaths);
624624

625-
// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
626-
moduleResolutionCache = undefined;
627-
628625
// Release any files we have acquired in the old program but are
629626
// not part of the new program.
630627
if (oldProgram && host.onReleaseOldSourceFile) {
@@ -670,7 +667,8 @@ namespace ts {
670667
sourceFileToPackageName,
671668
redirectTargetsSet,
672669
isEmittedFile,
673-
getConfigFileParsingDiagnostics
670+
getConfigFileParsingDiagnostics,
671+
getResolvedModuleWithFailedLookupLocationsFromCache,
674672
};
675673

676674
verifyCompilerOptions();
@@ -679,6 +677,10 @@ namespace ts {
679677

680678
return program;
681679

680+
function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations {
681+
return moduleResolutionCache && resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache);
682+
}
683+
682684
function toPath(fileName: string): Path {
683685
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
684686
}

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2725,6 +2725,8 @@ namespace ts {
27252725
/* @internal */ redirectTargetsSet: Map<true>;
27262726
/** Is the file emitted file */
27272727
/* @internal */ isEmittedFile(file: string): boolean;
2728+
2729+
/* @internal */ getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined;
27282730
}
27292731

27302732
/* @internal */

src/harness/fourslash.ts

+19
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,15 @@ Actual: ${stringify(fullActual)}`);
32803280
private static textSpansEqual(a: ts.TextSpan, b: ts.TextSpan) {
32813281
return a && b && a.start === b.start && a.length === b.length;
32823282
}
3283+
3284+
public getEditsForFileRename(options: FourSlashInterface.GetEditsForFileRenameOptions): void {
3285+
const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings);
3286+
this.applyChanges(changes);
3287+
for (const fileName in options.newFileContents) {
3288+
this.openFile(fileName);
3289+
this.verifyCurrentFileContent(options.newFileContents[fileName]);
3290+
}
3291+
}
32833292
}
32843293

32853294
export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
@@ -4371,6 +4380,10 @@ namespace FourSlashInterface {
43714380
public allRangesAppearInImplementationList(markerName: string) {
43724381
this.state.verifyRangesInImplementationList(markerName);
43734382
}
4383+
4384+
public getEditsForFileRename(options: GetEditsForFileRenameOptions) {
4385+
this.state.getEditsForFileRename(options);
4386+
}
43744387
}
43754388

43764389
export class Edit {
@@ -4713,4 +4726,10 @@ namespace FourSlashInterface {
47134726
range?: FourSlash.Range;
47144727
code: number;
47154728
}
4729+
4730+
export interface GetEditsForFileRenameOptions {
4731+
readonly oldPath: string;
4732+
readonly newPath: string;
4733+
readonly newFileContents: { readonly [fileName: string]: string };
4734+
}
47164735
}

src/harness/harnessLanguageService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,9 @@ namespace Harness.LanguageService {
528528
organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray<ts.FileTextChanges> {
529529
throw new Error("Not supported on the shim.");
530530
}
531+
getEditsForFileRename(): ReadonlyArray<ts.FileTextChanges> {
532+
throw new Error("Not supported on the shim.");
533+
}
531534
getEmitOutput(fileName: string): ts.EmitOutput {
532535
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
533536
}

src/harness/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"../services/navigateTo.ts",
7171
"../services/navigationBar.ts",
7272
"../services/organizeImports.ts",
73+
"../services/getEditsForFileRename.ts",
7374
"../services/outliningElementsCollector.ts",
7475
"../services/patternMatcher.ts",
7576
"../services/preProcess.ts",

src/harness/unittests/session.ts

+2
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ namespace ts.server {
263263
CommandNames.GetEditsForRefactorFull,
264264
CommandNames.OrganizeImports,
265265
CommandNames.OrganizeImportsFull,
266+
CommandNames.GetEditsForFileRename,
267+
CommandNames.GetEditsForFileRenameFull,
266268
];
267269

268270
it("should not throw when commands are executed with invalid arguments", () => {

src/server/client.ts

+4
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,10 @@ namespace ts.server {
632632
return notImplemented();
633633
}
634634

635+
getEditsForFileRename() {
636+
return notImplemented();
637+
}
638+
635639
private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
636640
return edits.map(edit => {
637641
const fileName = edit.fileName;

src/server/protocol.ts

+19
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ namespace ts.server.protocol {
121121
OrganizeImports = "organizeImports",
122122
/* @internal */
123123
OrganizeImportsFull = "organizeImports-full",
124+
GetEditsForFileRename = "getEditsForFileRename",
125+
/* @internal */
126+
GetEditsForFileRenameFull = "getEditsForFileRename-full",
124127

125128
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
126129
}
@@ -610,6 +613,22 @@ namespace ts.server.protocol {
610613
edits: ReadonlyArray<FileCodeEdits>;
611614
}
612615

616+
export interface GetEditsForFileRenameRequest extends Request {
617+
command: CommandTypes.GetEditsForFileRename;
618+
arguments: GetEditsForFileRenameRequestArgs;
619+
}
620+
621+
// Note: The file from FileRequestArgs is just any file in the project.
622+
// We will generate code changes for every file in that project, so the choice is arbitrary.
623+
export interface GetEditsForFileRenameRequestArgs extends FileRequestArgs {
624+
readonly oldFilePath: string;
625+
readonly newFilePath: string;
626+
}
627+
628+
export interface GetEditsForFileRenameResponse extends Response {
629+
edits: ReadonlyArray<FileCodeEdits>;
630+
}
631+
613632
/**
614633
* Request for the available codefixes at a specific position.
615634
*/

src/server/session.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,12 @@ namespace ts.server {
16651665
}
16661666
}
16671667

1668+
private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
1669+
const { file, project } = this.getFileAndProject(args);
1670+
const changes = project.getLanguageService().getEditsForFileRename(args.oldFilePath, args.newFilePath, this.getFormatOptions(file));
1671+
return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes;
1672+
}
1673+
16681674
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeFixAction> | ReadonlyArray<CodeFixAction> {
16691675
if (args.errorCodes.length === 0) {
16701676
return undefined;
@@ -2118,7 +2124,13 @@ namespace ts.server {
21182124
},
21192125
[CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => {
21202126
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false));
2121-
}
2127+
},
2128+
[CommandNames.GetEditsForFileRename]: (request: protocol.GetEditsForFileRenameRequest) => {
2129+
return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ true));
2130+
},
2131+
[CommandNames.GetEditsForFileRenameFull]: (request: protocol.GetEditsForFileRenameRequest) => {
2132+
return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ false));
2133+
},
21222134
});
21232135

21242136
public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {

src/server/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"../services/navigateTo.ts",
6767
"../services/navigationBar.ts",
6868
"../services/organizeImports.ts",
69+
"../services/getEditsForFileRename.ts",
6970
"../services/outliningElementsCollector.ts",
7071
"../services/patternMatcher.ts",
7172
"../services/preProcess.ts",

src/server/tsconfig.library.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"../services/navigateTo.ts",
7373
"../services/navigationBar.ts",
7474
"../services/organizeImports.ts",
75+
"../services/getEditsForFileRename.ts",
7576
"../services/outliningElementsCollector.ts",
7677
"../services/patternMatcher.ts",
7778
"../services/preProcess.ts",

src/services/codefixes/importFixes.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ namespace ts.codefix {
3939
}
4040

4141
function convertToImportCodeFixContext(context: CodeFixContext, symbolToken: Node, symbolName: string): ImportCodeFixContext {
42-
const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false;
4342
const { program } = context;
4443
const checker = program.getTypeChecker();
4544

@@ -51,7 +50,7 @@ namespace ts.codefix {
5150
checker,
5251
compilerOptions: program.getCompilerOptions(),
5352
cachedImportDeclarations: [],
54-
getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames),
53+
getCanonicalFileName: createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(context.host)),
5554
symbolName,
5655
symbolToken,
5756
preferences: context.preferences,
@@ -547,11 +546,6 @@ namespace ts.codefix {
547546
return startsWith(path, "..");
548547
}
549548

550-
function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) {
551-
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
552-
return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath;
553-
}
554-
555549
function getCodeActionsForAddImport(
556550
exportInfos: ReadonlyArray<SymbolExportInfo>,
557551
ctx: ImportCodeFixContext,

src/services/getEditsForFileRename.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/* @internal */
2+
namespace ts {
3+
export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray<FileTextChanges> {
4+
const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host);
5+
return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => {
6+
for (const { sourceFile, toUpdate } of getImportsToUpdate(program, oldFilePath)) {
7+
const newPath = pathUpdater(isRef(toUpdate) ? toUpdate.fileName : toUpdate.text);
8+
if (newPath !== undefined) {
9+
const range = isRef(toUpdate) ? toUpdate : createTextRange(toUpdate.getStart(sourceFile) + 1, toUpdate.end - 1);
10+
changeTracker.replaceRangeWithText(sourceFile, range, isRef(toUpdate) ? newPath : removeFileExtension(newPath));
11+
}
12+
}
13+
});
14+
}
15+
16+
interface ToUpdate {
17+
readonly sourceFile: SourceFile;
18+
readonly toUpdate: StringLiteralLike | FileReference;
19+
}
20+
function isRef(toUpdate: StringLiteralLike | FileReference): toUpdate is FileReference {
21+
return "fileName" in toUpdate;
22+
}
23+
24+
function getImportsToUpdate(program: Program, oldFilePath: string): ReadonlyArray<ToUpdate> {
25+
const checker = program.getTypeChecker();
26+
const result: ToUpdate[] = [];
27+
for (const sourceFile of program.getSourceFiles()) {
28+
for (const ref of sourceFile.referencedFiles) {
29+
if (!program.getSourceFileFromReference(sourceFile, ref) && resolveTripleslashReference(ref.fileName, sourceFile.fileName) === oldFilePath) {
30+
result.push({ sourceFile, toUpdate: ref });
31+
}
32+
}
33+
34+
for (const importStringLiteral of sourceFile.imports) {
35+
// If it resolved to something already, ignore.
36+
if (checker.getSymbolAtLocation(importStringLiteral)) continue;
37+
38+
const resolved = program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName);
39+
if (contains(resolved.failedLookupLocations, oldFilePath)) {
40+
result.push({ sourceFile, toUpdate: importStringLiteral });
41+
}
42+
}
43+
}
44+
return result;
45+
}
46+
47+
function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined {
48+
// Get the relative path from old to new location, and append it on to the end of imports and normalize.
49+
const rel = getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)));
50+
return oldPath => {
51+
if (!pathIsRelative(oldPath)) return;
52+
return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
53+
};
54+
}
55+
}

src/services/services.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,6 @@ namespace ts {
11281128
let lastProjectVersion: string;
11291129
let lastTypesRootVersion = 0;
11301130

1131-
const useCaseSensitivefileNames = host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames();
11321131
const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken());
11331132

11341133
const currentDirectory = host.getCurrentDirectory();
@@ -1145,7 +1144,8 @@ namespace ts {
11451144
}
11461145
}
11471146

1148-
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitivefileNames);
1147+
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
1148+
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
11491149

11501150
function getValidSourceFile(fileName: string): SourceFile {
11511151
const sourceFile = program.getSourceFile(fileName);
@@ -1202,7 +1202,7 @@ namespace ts {
12021202
getSourceFileByPath: getOrCreateSourceFileByPath,
12031203
getCancellationToken: () => cancellationToken,
12041204
getCanonicalFileName,
1205-
useCaseSensitiveFileNames: () => useCaseSensitivefileNames,
1205+
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
12061206
getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)),
12071207
getDefaultLibFileName: (options) => host.getDefaultLibFileName(options),
12081208
writeFile: noop,
@@ -1951,6 +1951,10 @@ namespace ts {
19511951
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences);
19521952
}
19531953

1954+
function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
1955+
return ts.getEditsForFileRename(getProgram(), oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions));
1956+
}
1957+
19541958
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
19551959
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
19561960
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
@@ -2251,6 +2255,7 @@ namespace ts {
22512255
getCombinedCodeFix,
22522256
applyCodeActionCommand,
22532257
organizeImports,
2258+
getEditsForFileRename,
22542259
getEmitOutput,
22552260
getNonBoundSourceFile,
22562261
getSourceFile,

src/services/textChanges.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,12 @@ namespace ts.textChanges {
365365
this.insertText(sourceFile, token.getStart(sourceFile), text);
366366
}
367367

368+
public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string) {
369+
this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text });
370+
}
371+
368372
private insertText(sourceFile: SourceFile, pos: number, text: string): void {
369-
this.changes.push({ kind: ChangeKind.Text, sourceFile, range: { pos, end: pos }, text });
373+
this.replaceRangeWithText(sourceFile, createTextRange(pos), text);
370374
}
371375

372376
/** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"navigateTo.ts",
6464
"navigationBar.ts",
6565
"organizeImports.ts",
66+
"getEditsForFileRename.ts",
6667
"outliningElementsCollector.ts",
6768
"patternMatcher.ts",
6869
"preProcess.ts",

src/services/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ namespace ts {
334334
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[];
335335
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
336336
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray<FileTextChanges>;
337+
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;
337338

338339
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
339340

src/services/utilities.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,14 @@ namespace ts {
12131213
? isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined
12141214
: getTextOfIdentifierOrLiteral(name);
12151215
}
1216+
1217+
export function hostUsesCaseSensitiveFileNames(host: LanguageServiceHost): boolean {
1218+
return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false;
1219+
}
1220+
1221+
export function hostGetCanonicalFileName(host: LanguageServiceHost): GetCanonicalFileName {
1222+
return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host));
1223+
}
12161224
}
12171225

12181226
// Display-part writer helpers

0 commit comments

Comments
 (0)