Skip to content

Commit 7271ec1

Browse files
author
Andy
authored
Add 'move to new file' refactor (#23726)
* Add 'move to new file' refactor * Code review, and support commonjs * Compute movedSymbols completely before using, and support `export import` * Fix assertion error: sort empty change before non-empty change * Remove extra newline * Add allowTextChangesInNewFiles preference * Add the new file to 'files' in tsconfig * Avoid parameter initializer * Update API baselines * Use path relative to tsconfig.json * Code review * Fix error where node in tsconfig file was missing a source file
1 parent 6149b41 commit 7271ec1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1254
-116
lines changed

Diff for: src/compiler/core.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,8 @@ namespace ts {
311311
}
312312

313313
/** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */
314-
export function findIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean): number {
315-
for (let i = 0; i < array.length; i++) {
314+
export function findIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
315+
for (let i = startIndex || 0; i < array.length; i++) {
316316
if (predicate(array[i], i)) {
317317
return i;
318318
}
@@ -2294,20 +2294,24 @@ namespace ts {
22942294
return ["", ...relative, ...components];
22952295
}
22962296

2297+
export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) {
2298+
return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName));
2299+
}
2300+
22972301
/**
22982302
* Gets a relative path that can be used to traverse between `from` and `to`.
22992303
*/
2300-
export function getRelativePath(from: string, to: string, ignoreCase: boolean): string;
2304+
export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string;
23012305
/**
23022306
* Gets a relative path that can be used to traverse between `from` and `to`.
23032307
*/
23042308
// tslint:disable-next-line:unified-signatures
2305-
export function getRelativePath(from: string, to: string, getCanonicalFileName: GetCanonicalFileName): string;
2306-
export function getRelativePath(from: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) {
2307-
Debug.assert((getRootLength(from) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative");
2309+
export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string;
2310+
export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) {
2311+
Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative");
23082312
const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity;
23092313
const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false;
2310-
const pathComponents = getPathComponentsRelativeTo(from, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName);
2314+
const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName);
23112315
return getPathFromPathComponents(pathComponents);
23122316
}
23132317

Diff for: src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -4245,5 +4245,9 @@
42454245
"Convert all 'require' to 'import'": {
42464246
"category": "Message",
42474247
"code": 95048
4248+
},
4249+
"Move to a new file": {
4250+
"category": "Message",
4251+
"code": 95049
42484252
}
42494253
}

Diff for: src/compiler/factory.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,13 @@ namespace ts {
15051505
return block;
15061506
}
15071507

1508+
/* @internal */
1509+
export function createExpressionStatement(expression: Expression): ExpressionStatement {
1510+
const node = <ExpressionStatement>createSynthesizedNode(SyntaxKind.ExpressionStatement);
1511+
node.expression = expression;
1512+
return node;
1513+
}
1514+
15081515
export function updateBlock(node: Block, statements: ReadonlyArray<Statement>) {
15091516
return node.statements !== statements
15101517
? updateNode(createBlock(statements, node.multiLine), node)
@@ -1531,9 +1538,7 @@ namespace ts {
15311538
}
15321539

15331540
export function createStatement(expression: Expression) {
1534-
const node = <ExpressionStatement>createSynthesizedNode(SyntaxKind.ExpressionStatement);
1535-
node.expression = parenthesizeExpressionForExpressionStatement(expression);
1536-
return node;
1541+
return createExpressionStatement(parenthesizeExpressionForExpressionStatement(expression));
15371542
}
15381543

15391544
export function updateStatement(node: ExpressionStatement, expression: Expression) {

Diff for: src/harness/fourslash.ts

+61-13
Original file line numberDiff line numberDiff line change
@@ -3005,9 +3005,7 @@ Actual: ${stringify(fullActual)}`);
30053005
}
30063006

30073007
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
3008-
const marker = this.getMarkerByName(markerName);
3009-
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position, ts.defaultPreferences);
3010-
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
3008+
const isAvailable = this.getApplicableRefactors(this.getMarkerByName(markerName).position).length > 0;
30113009
if (negative && isAvailable) {
30123010
this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`);
30133011
}
@@ -3024,9 +3022,7 @@ Actual: ${stringify(fullActual)}`);
30243022
}
30253023

30263024
public verifyRefactorAvailable(negative: boolean, name: string, actionName?: string) {
3027-
const selection = this.getSelection();
3028-
3029-
let refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || [];
3025+
let refactors = this.getApplicableRefactors(this.getSelection());
30303026
refactors = refactors.filter(r => r.name === name && (actionName === undefined || r.actions.some(a => a.name === actionName)));
30313027
const isAvailable = refactors.length > 0;
30323028

@@ -3046,10 +3042,7 @@ Actual: ${stringify(fullActual)}`);
30463042
}
30473043

30483044
public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) {
3049-
const selection = this.getSelection();
3050-
3051-
const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || ts.emptyArray)
3052-
.filter(r => r.name === name && r.actions.some(a => a.name === actionName));
3045+
const actualRefactors = this.getApplicableRefactors(this.getSelection()).filter(r => r.name === name && r.actions.some(a => a.name === actionName));
30533046
this.assertObjectsEqual(actualRefactors, refactors);
30543047
}
30553048

@@ -3059,8 +3052,7 @@ Actual: ${stringify(fullActual)}`);
30593052
throw new Error("Exactly one refactor range is allowed per test.");
30603053
}
30613054

3062-
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, ts.first(ranges), ts.defaultPreferences);
3063-
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
3055+
const isAvailable = this.getApplicableRefactors(ts.first(ranges)).length > 0;
30643056
if (negative && isAvailable) {
30653057
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
30663058
}
@@ -3071,7 +3063,7 @@ Actual: ${stringify(fullActual)}`);
30713063

30723064
public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker }: FourSlashInterface.ApplyRefactorOptions) {
30733065
const range = this.getSelection();
3074-
const refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, range, ts.defaultPreferences);
3066+
const refactors = this.getApplicableRefactors(range);
30753067
const refactorsWithName = refactors.filter(r => r.name === refactorName);
30763068
if (refactorsWithName.length === 0) {
30773069
this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`);
@@ -3117,7 +3109,48 @@ Actual: ${stringify(fullActual)}`);
31173109
return { renamePosition, newContent };
31183110
}
31193111
}
3112+
}
31203113

3114+
public noMoveToNewFile() {
3115+
for (const range of this.getRanges()) {
3116+
for (const refactor of this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true })) {
3117+
if (refactor.name === "Move to a new file") {
3118+
ts.Debug.fail("Did not expect to get 'move to a new file' refactor");
3119+
}
3120+
}
3121+
}
3122+
}
3123+
3124+
public moveToNewFile(options: FourSlashInterface.MoveToNewFileOptions): void {
3125+
assert(this.getRanges().length === 1);
3126+
const range = this.getRanges()[0];
3127+
const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }), r => r.name === "Move to a new file");
3128+
assert(refactor.actions.length === 1);
3129+
const action = ts.first(refactor.actions);
3130+
assert(action.name === "Move to a new file" && action.description === "Move to a new file");
3131+
3132+
const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, ts.defaultPreferences);
3133+
for (const edit of editInfo.edits) {
3134+
const newContent = options.newFileContents[edit.fileName];
3135+
if (newContent === undefined) {
3136+
this.raiseError(`There was an edit in ${edit.fileName} but new content was not specified.`);
3137+
}
3138+
if (this.testData.files.some(f => f.fileName === edit.fileName)) {
3139+
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
3140+
this.openFile(edit.fileName);
3141+
this.verifyCurrentFileContent(newContent);
3142+
}
3143+
else {
3144+
assert(edit.textChanges.length === 1);
3145+
const change = ts.first(edit.textChanges);
3146+
assert.deepEqual(change.span, ts.createTextSpan(0, 0));
3147+
assert.equal(change.newText, newContent, `Content for ${edit.fileName}`);
3148+
}
3149+
}
3150+
3151+
for (const fileName in options.newFileContents) {
3152+
assert(editInfo.edits.some(e => e.fileName === fileName));
3153+
}
31213154
}
31223155

31233156
public verifyFileAfterApplyingRefactorAtMarker(
@@ -3333,6 +3366,10 @@ Actual: ${stringify(fullActual)}`);
33333366
this.verifyCurrentFileContent(options.newFileContents[fileName]);
33343367
}
33353368
}
3369+
3370+
private getApplicableRefactors(positionOrRange: number | ts.TextRange, preferences = ts.defaultPreferences): ReadonlyArray<ts.ApplicableRefactorInfo> {
3371+
return this.languageService.getApplicableRefactors(this.activeFile.fileName, positionOrRange, preferences) || ts.emptyArray;
3372+
}
33363373
}
33373374

33383375
export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
@@ -4430,6 +4467,13 @@ namespace FourSlashInterface {
44304467
public getEditsForFileRename(options: GetEditsForFileRenameOptions) {
44314468
this.state.getEditsForFileRename(options);
44324469
}
4470+
4471+
public moveToNewFile(options: MoveToNewFileOptions): void {
4472+
this.state.moveToNewFile(options);
4473+
}
4474+
public noMoveToNewFile(): void {
4475+
this.state.noMoveToNewFile();
4476+
}
44334477
}
44344478

44354479
export class Edit {
@@ -4803,4 +4847,8 @@ namespace FourSlashInterface {
48034847
readonly newPath: string;
48044848
readonly newFileContents: { readonly [fileName: string]: string };
48054849
}
4850+
4851+
export interface MoveToNewFileOptions {
4852+
readonly newFileContents: { readonly [fileName: string]: string };
4853+
}
48064854
}

Diff for: src/harness/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"../services/codefixes/useDefaultImport.ts",
117117
"../services/refactors/extractSymbol.ts",
118118
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
119+
"../services/refactors/moveToNewFile.ts",
119120
"../services/sourcemaps.ts",
120121
"../services/services.ts",
121122
"../services/breakpoints.ts",

Diff for: src/harness/unittests/paths.ts

+20-20
Original file line numberDiff line numberDiff line change
@@ -268,25 +268,25 @@ describe("core paths", () => {
268268
assert.strictEqual(ts.resolvePath("a", "b", "../c"), "a/c");
269269
});
270270
it("getPathRelativeTo", () => {
271-
assert.strictEqual(ts.getRelativePath("/", "/", /*ignoreCase*/ false), "");
272-
assert.strictEqual(ts.getRelativePath("/a", "/a", /*ignoreCase*/ false), "");
273-
assert.strictEqual(ts.getRelativePath("/a/", "/a", /*ignoreCase*/ false), "");
274-
assert.strictEqual(ts.getRelativePath("/a", "/", /*ignoreCase*/ false), "..");
275-
assert.strictEqual(ts.getRelativePath("/a", "/b", /*ignoreCase*/ false), "../b");
276-
assert.strictEqual(ts.getRelativePath("/a/b", "/b", /*ignoreCase*/ false), "../../b");
277-
assert.strictEqual(ts.getRelativePath("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b");
278-
assert.strictEqual(ts.getRelativePath("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c");
279-
assert.strictEqual(ts.getRelativePath("/a/b/c", "/a/b", /*ignoreCase*/ false), "..");
280-
assert.strictEqual(ts.getRelativePath("c:", "d:", /*ignoreCase*/ false), "d:/");
281-
assert.strictEqual(ts.getRelativePath("file:///", "file:///", /*ignoreCase*/ false), "");
282-
assert.strictEqual(ts.getRelativePath("file:///a", "file:///a", /*ignoreCase*/ false), "");
283-
assert.strictEqual(ts.getRelativePath("file:///a/", "file:///a", /*ignoreCase*/ false), "");
284-
assert.strictEqual(ts.getRelativePath("file:///a", "file:///", /*ignoreCase*/ false), "..");
285-
assert.strictEqual(ts.getRelativePath("file:///a", "file:///b", /*ignoreCase*/ false), "../b");
286-
assert.strictEqual(ts.getRelativePath("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b");
287-
assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b");
288-
assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c");
289-
assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), "..");
290-
assert.strictEqual(ts.getRelativePath("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/");
271+
assert.strictEqual(ts.getRelativePathFromDirectory("/", "/", /*ignoreCase*/ false), "");
272+
assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/a", /*ignoreCase*/ false), "");
273+
assert.strictEqual(ts.getRelativePathFromDirectory("/a/", "/a", /*ignoreCase*/ false), "");
274+
assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/", /*ignoreCase*/ false), "..");
275+
assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/b", /*ignoreCase*/ false), "../b");
276+
assert.strictEqual(ts.getRelativePathFromDirectory("/a/b", "/b", /*ignoreCase*/ false), "../../b");
277+
assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b");
278+
assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c");
279+
assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/a/b", /*ignoreCase*/ false), "..");
280+
assert.strictEqual(ts.getRelativePathFromDirectory("c:", "d:", /*ignoreCase*/ false), "d:/");
281+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///", "file:///", /*ignoreCase*/ false), "");
282+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///a", /*ignoreCase*/ false), "");
283+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/", "file:///a", /*ignoreCase*/ false), "");
284+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///", /*ignoreCase*/ false), "..");
285+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///b", /*ignoreCase*/ false), "../b");
286+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b");
287+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b");
288+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c");
289+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), "..");
290+
assert.strictEqual(ts.getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/");
291291
});
292292
});

Diff for: src/harness/vpath.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace vpath {
1818
export import dirname = ts.getDirectoryPath;
1919
export import basename = ts.getBaseFileName;
2020
export import extname = ts.getAnyExtensionFromPath;
21-
export import relative = ts.getRelativePath;
21+
export import relative = ts.getRelativePathFromDirectory;
2222
export import beneath = ts.containsPath;
2323
export import changeExtension = ts.changeAnyExtension;
2424
export import isTypeScript = ts.hasTypeScriptFileExtension;

Diff for: src/server/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"../services/codefixes/useDefaultImport.ts",
113113
"../services/refactors/extractSymbol.ts",
114114
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
115+
"../services/refactors/moveToNewFile.ts",
115116
"../services/sourcemaps.ts",
116117
"../services/services.ts",
117118
"../services/breakpoints.ts",

Diff for: src/server/tsconfig.library.json

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"../services/codefixes/useDefaultImport.ts",
119119
"../services/refactors/extractSymbol.ts",
120120
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
121+
"../services/refactors/moveToNewFile.ts",
121122
"../services/sourcemaps.ts",
122123
"../services/services.ts",
123124
"../services/breakpoints.ts",

Diff for: src/services/breakpoints.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ namespace ts.BreakpointResolver {
4141
}
4242

4343
function textSpanEndingAtNextToken(startNode: Node, previousTokenToFindNextEndToken: Node): TextSpan {
44-
return textSpan(startNode, findNextToken(previousTokenToFindNextEndToken, previousTokenToFindNextEndToken.parent));
44+
return textSpan(startNode, findNextToken(previousTokenToFindNextEndToken, previousTokenToFindNextEndToken.parent, sourceFile));
4545
}
4646

4747
function spanInNodeIfStartsOnSameLine(node: Node, otherwiseOnNode?: Node): TextSpan {
@@ -60,7 +60,7 @@ namespace ts.BreakpointResolver {
6060
}
6161

6262
function spanInNextNode(node: Node): TextSpan {
63-
return spanInNode(findNextToken(node, node.parent));
63+
return spanInNode(findNextToken(node, node.parent, sourceFile));
6464
}
6565

6666
function spanInNode(node: Node): TextSpan {

Diff for: src/services/codefixes/convertToEs6Module.ts

-9
Original file line numberDiff line numberDiff line change
@@ -487,15 +487,6 @@ namespace ts.codefix {
487487
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier);
488488
}
489489

490-
function makeImport(name: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: StringLiteralLike): ImportDeclaration {
491-
return makeImportDeclaration(name, namedImports, moduleSpecifier);
492-
}
493-
494-
export function makeImportDeclaration(name: Identifier, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: Expression) {
495-
const importClause = (name || namedImports) && createImportClause(name, namedImports && createNamedImports(namedImports));
496-
return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifier);
497-
}
498-
499490
function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier {
500491
return createImportSpecifier(propertyName !== undefined && propertyName !== name ? createIdentifier(propertyName) : undefined, createIdentifier(name));
501492
}

0 commit comments

Comments
 (0)