Skip to content

Commit 505189f

Browse files
author
Andy Hanson
committed
Add 'renameFile' command to services
1 parent 18c3f5f commit 505189f

21 files changed

+175
-15
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
@@ -3279,6 +3279,15 @@ Actual: ${stringify(fullActual)}`);
32793279
private static textSpansEqual(a: ts.TextSpan, b: ts.TextSpan) {
32803280
return a && b && a.start === b.start && a.length === b.length;
32813281
}
3282+
3283+
public renameFile(options: FourSlashInterface.RenameFileOptions): void {
3284+
const changes = this.languageService.renameFile(options.oldPath, options.newPath, this.formatCodeSettings);
3285+
this.applyChanges(changes);
3286+
for (const fileName in options.newFileContents) {
3287+
this.openFile(fileName);
3288+
this.verifyCurrentFileContent(options.newFileContents[fileName]);
3289+
}
3290+
}
32823291
}
32833292

32843293
export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
@@ -4370,6 +4379,10 @@ namespace FourSlashInterface {
43704379
public allRangesAppearInImplementationList(markerName: string) {
43714380
this.state.verifyRangesInImplementationList(markerName);
43724381
}
4382+
4383+
public renameFile(options: RenameFileOptions) {
4384+
this.state.renameFile(options);
4385+
}
43734386
}
43744387

43754388
export class Edit {
@@ -4710,4 +4723,10 @@ namespace FourSlashInterface {
47104723
range?: FourSlash.Range;
47114724
code: number;
47124725
}
4726+
4727+
export interface RenameFileOptions {
4728+
readonly oldPath: string;
4729+
readonly newPath: string;
4730+
readonly newFileContents: { readonly [fileName: string]: string };
4731+
}
47134732
}

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+
renameFile(): 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/renameFile.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.RenameFile,
267+
CommandNames.RenameFileFull,
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+
renameFile() {
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+
RenameFile = "renameFile",
125+
/* @internal */
126+
RenameFileFull = "renameFile-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 RenameFileRequest extends Request {
617+
command: CommandTypes.RenameFile;
618+
arguments: RenameFileRequestArgs;
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 RenameFileRequestArgs extends FileRequestArgs {
624+
readonly oldFilePath: string;
625+
readonly newFilePath: string;
626+
}
627+
628+
export interface RenameFileResponse 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
@@ -1664,6 +1664,12 @@ namespace ts.server {
16641664
}
16651665
}
16661666

1667+
private renameFile(args: protocol.RenameFileRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
1668+
const { file, project } = this.getFileAndProject(args);
1669+
const changes = project.getLanguageService().renameFile(args.oldFilePath, args.newFilePath, this.getFormatOptions(file));
1670+
return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes;
1671+
}
1672+
16671673
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeFixAction> | ReadonlyArray<CodeFixAction> {
16681674
if (args.errorCodes.length === 0) {
16691675
return undefined;
@@ -2117,7 +2123,13 @@ namespace ts.server {
21172123
},
21182124
[CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => {
21192125
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false));
2120-
}
2126+
},
2127+
[CommandNames.RenameFile]: (request: protocol.RenameFileRequest) => {
2128+
return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ true));
2129+
},
2130+
[CommandNames.RenameFileFull]: (request: protocol.RenameFileRequest) => {
2131+
return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ false));
2132+
},
21212133
});
21222134

21232135
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/renameFile.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/renameFile.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/renameFile.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* @internal */
2+
namespace ts {
3+
export function renameFile(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+
const importsToUpdate = getImportsToUpdate(program, oldFilePath);
7+
for (const importToUpdate of importsToUpdate) {
8+
const newPath = pathUpdater(importToUpdate.text);
9+
if (newPath !== undefined) {
10+
changeTracker.replaceNode(importToUpdate.getSourceFile(), importToUpdate, updateStringLiteralLike(importToUpdate, newPath));
11+
}
12+
}
13+
});
14+
}
15+
16+
function getImportsToUpdate(program: Program, oldFilePath: string): ReadonlyArray<StringLiteralLike> {
17+
const checker = program.getTypeChecker();
18+
const result: StringLiteralLike[] = [];
19+
for (const file of program.getSourceFiles()) {
20+
for (const importStringLiteral of file.imports) {
21+
// If it resolved to something already, ignore.
22+
if (checker.getSymbolAtLocation(importStringLiteral)) continue;
23+
24+
const resolved = program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, file.fileName);
25+
if (contains(resolved.failedLookupLocations, oldFilePath)) {
26+
result.push(importStringLiteral);
27+
}
28+
}
29+
}
30+
return result;
31+
}
32+
33+
function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined {
34+
// Get the relative path from old to new location, and append it on to the end of imports and normalize.
35+
const rel = removeFileExtension(getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))));
36+
return oldPath => {
37+
if (!pathIsRelative(oldPath)) return;
38+
return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
39+
};
40+
}
41+
42+
function updateStringLiteralLike(old: StringLiteralLike, newText: string): StringLiteralLike {
43+
return old.kind === SyntaxKind.StringLiteral ? createLiteral(newText, /*isSingleQuote*/ old.singleQuote) : createNoSubstitutionTemplateLiteral(newText);
44+
}
45+
}

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,
@@ -1950,6 +1950,10 @@ namespace ts {
19501950
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences);
19511951
}
19521952

1953+
function renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
1954+
return ts.renameFile(getProgram(), oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions));
1955+
}
1956+
19531957
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
19541958
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
19551959
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
@@ -2250,6 +2254,7 @@ namespace ts {
22502254
getCombinedCodeFix,
22512255
applyCodeActionCommand,
22522256
organizeImports,
2257+
renameFile,
22532258
getEmitOutput,
22542259
getNonBoundSourceFile,
22552260
getSourceFile,

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+
"../services/renameFile.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+
renameFile(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

tests/cases/fourslash/fourslash.ts

+5
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,11 @@ declare namespace FourSlashInterface {
344344
getSuggestionDiagnostics(expected: ReadonlyArray<Diagnostic>): void;
345345
ProjectInfo(expected: string[]): void;
346346
allRangesAppearInImplementationList(markerName: string): void;
347+
renameFile(options: {
348+
oldPath: string;
349+
newPath: string;
350+
newFileContents: { [fileName: string]: string };
351+
});
347352
}
348353
class edit {
349354
backspace(count?: number): void;

0 commit comments

Comments
 (0)