Skip to content

Commit 0b4c9ba

Browse files
author
Andy Hanson
committed
Add 'move to new file' refactor
1 parent 222f35d commit 0b4c9ba

28 files changed

+722
-45
lines changed

src/compiler/core.ts

Lines changed: 2 additions & 2 deletions
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 = 0): number {
315+
for (let i = startIndex; i < array.length; i++) {
316316
if (predicate(array[i], i)) {
317317
return i;
318318
}

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4165,5 +4165,9 @@
41654165
"Generate 'get' and 'set' accessors": {
41664166
"category": "Message",
41674167
"code": 95046
4168+
},
4169+
"Move to new file": {
4170+
"category": "Message",
4171+
"code": 95047
41684172
}
41694173
}

src/harness/fourslash.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2950,9 +2950,7 @@ Actual: ${stringify(fullActual)}`);
29502950
}
29512951

29522952
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
2953-
const marker = this.getMarkerByName(markerName);
2954-
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position, ts.defaultPreferences);
2955-
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
2953+
const isAvailable = this.getApplicableRefactors(this.getMarkerByName(markerName).position).length > 0;
29562954
if (negative && isAvailable) {
29572955
this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`);
29582956
}
@@ -2969,9 +2967,7 @@ Actual: ${stringify(fullActual)}`);
29692967
}
29702968

29712969
public verifyRefactorAvailable(negative: boolean, name: string, actionName?: string) {
2972-
const selection = this.getSelection();
2973-
2974-
let refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || [];
2970+
let refactors = this.getApplicableRefactors(this.getSelection());
29752971
refactors = refactors.filter(r => r.name === name && (actionName === undefined || r.actions.some(a => a.name === actionName)));
29762972
const isAvailable = refactors.length > 0;
29772973

@@ -2991,10 +2987,7 @@ Actual: ${stringify(fullActual)}`);
29912987
}
29922988

29932989
public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) {
2994-
const selection = this.getSelection();
2995-
2996-
const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || ts.emptyArray)
2997-
.filter(r => r.name === name && r.actions.some(a => a.name === actionName));
2990+
const actualRefactors = this.getApplicableRefactors(this.getSelection()).filter(r => r.name === name && r.actions.some(a => a.name === actionName));
29982991
this.assertObjectsEqual(actualRefactors, refactors);
29992992
}
30002993

@@ -3004,8 +2997,7 @@ Actual: ${stringify(fullActual)}`);
30042997
throw new Error("Exactly one refactor range is allowed per test.");
30052998
}
30062999

3007-
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, ts.first(ranges), ts.defaultPreferences);
3008-
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
3000+
const isAvailable = this.getApplicableRefactors(ts.first(ranges)).length > 0;
30093001
if (negative && isAvailable) {
30103002
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
30113003
}
@@ -3016,7 +3008,7 @@ Actual: ${stringify(fullActual)}`);
30163008

30173009
public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker }: FourSlashInterface.ApplyRefactorOptions) {
30183010
const range = this.getSelection();
3019-
const refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, range, ts.defaultPreferences);
3011+
const refactors = this.getApplicableRefactors(range);
30203012
const refactorsWithName = refactors.filter(r => r.name === refactorName);
30213013
if (refactorsWithName.length === 0) {
30223014
this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`);
@@ -3062,7 +3054,38 @@ Actual: ${stringify(fullActual)}`);
30623054
return { renamePosition, newContent };
30633055
}
30643056
}
3057+
}
3058+
3059+
public moveToNewFile(options: FourSlashInterface.MoveToNewFileOptions): void {
3060+
assert(this.getRanges().length === 1);
3061+
const range = this.getRanges()[0];
3062+
const refactor = ts.find(this.getApplicableRefactors(range), r => r.name === "Move to new file");
3063+
assert(refactor.actions.length === 1);
3064+
const action = ts.first(refactor.actions);
3065+
assert(action.name === "Move to new file" && action.description === "Move to new file");
3066+
3067+
const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, ts.defaultPreferences);
3068+
for (const edit of editInfo.edits) {
3069+
const newContent = options.newFileContents[edit.fileName];
3070+
if (newContent === undefined) {
3071+
this.raiseError(`There was an edit in ${edit.fileName} but new content was not specified.`);
3072+
}
3073+
if (this.testData.files.some(f => f.fileName === edit.fileName)) {
3074+
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
3075+
this.openFile(edit.fileName);
3076+
this.verifyCurrentFileContent(newContent);
3077+
}
3078+
else {
3079+
assert(edit.textChanges.length === 1);
3080+
const change = ts.first(edit.textChanges);
3081+
assert.deepEqual(change.span, ts.createTextSpan(0, 0));
3082+
assert.equal(change.newText, newContent, `Content for ${edit.fileName}`);
3083+
}
3084+
}
30653085

3086+
for (const fileName in options.newFileContents) {
3087+
assert(editInfo.edits.some(e => e.fileName === fileName));
3088+
}
30663089
}
30673090

30683091
public verifyFileAfterApplyingRefactorAtMarker(
@@ -3278,6 +3301,10 @@ Actual: ${stringify(fullActual)}`);
32783301
this.verifyCurrentFileContent(options.newFileContents[fileName]);
32793302
}
32803303
}
3304+
3305+
private getApplicableRefactors(positionOrRange: number | ts.TextRange): ReadonlyArray<ts.ApplicableRefactorInfo> {
3306+
return this.languageService.getApplicableRefactors(this.activeFile.fileName, positionOrRange, ts.defaultPreferences) || ts.emptyArray;
3307+
}
32813308
}
32823309

32833310
export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
@@ -4373,6 +4400,10 @@ namespace FourSlashInterface {
43734400
public getEditsForFileRename(options: GetEditsForFileRenameOptions) {
43744401
this.state.getEditsForFileRename(options);
43754402
}
4403+
4404+
public moveToNewFile(options: MoveToNewFileOptions): void {
4405+
this.state.moveToNewFile(options);
4406+
}
43764407
}
43774408

43784409
export class Edit {
@@ -4721,4 +4752,8 @@ namespace FourSlashInterface {
47214752
readonly newPath: string;
47224753
readonly newFileContents: { readonly [fileName: string]: string };
47234754
}
4755+
4756+
export interface MoveToNewFileOptions {
4757+
readonly newFileContents: { readonly [fileName: string]: string };
4758+
}
47244759
}

src/harness/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"../services/codefixes/useDefaultImport.ts",
116116
"../services/refactors/extractSymbol.ts",
117117
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
118+
"../services/refactors/moveToNewFile.ts",
118119
"../services/sourcemaps.ts",
119120
"../services/services.ts",
120121
"../services/breakpoints.ts",

src/server/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"../services/codefixes/useDefaultImport.ts",
112112
"../services/refactors/extractSymbol.ts",
113113
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
114+
"../services/refactors/moveToNewFile.ts",
114115
"../services/sourcemaps.ts",
115116
"../services/services.ts",
116117
"../services/breakpoints.ts",

src/server/tsconfig.library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"../services/codefixes/useDefaultImport.ts",
118118
"../services/refactors/extractSymbol.ts",
119119
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
120+
"../services/refactors/moveToNewFile.ts",
120121
"../services/sourcemaps.ts",
121122
"../services/services.ts",
122123
"../services/breakpoints.ts",

src/services/codefixes/convertToEs6Module.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -507,15 +507,6 @@ namespace ts.codefix {
507507
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier);
508508
}
509509

510-
function makeImport(name: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: StringLiteralLike): ImportDeclaration {
511-
return makeImportDeclaration(name, namedImports, moduleSpecifier);
512-
}
513-
514-
export function makeImportDeclaration(name: Identifier, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: Expression) {
515-
const importClause = (name || namedImports) && createImportClause(name, namedImports && createNamedImports(namedImports));
516-
return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifier);
517-
}
518-
519510
function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier {
520511
return createImportSpecifier(propertyName !== undefined && propertyName !== name ? createIdentifier(propertyName) : undefined, createIdentifier(name));
521512
}

src/services/codefixes/fixInvalidImportSyntax.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ namespace ts.codefix {
2828
const variations: CodeFixAction[] = [];
2929

3030
// import Bluebird from "bluebird";
31-
variations.push(createAction(context, sourceFile, node, makeImportDeclaration(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier)));
31+
variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier)));
3232

3333
if (getEmitModuleKind(opts) === ModuleKind.CommonJS) {
3434
// import Bluebird = require("bluebird");

src/services/codefixes/useDefaultImport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ namespace ts.codefix {
3737
}
3838

3939
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info): void {
40-
changes.replaceNode(sourceFile, info.importNode, makeImportDeclaration(info.name, /*namedImports*/ undefined, info.moduleSpecifier));
40+
changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier));
4141
}
4242
}

src/services/importTracker.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ namespace ts.FindAllReferences {
248248
const { name } = importClause;
249249
// If a default import has the same name as the default export, allow to rename it.
250250
// Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that.
251-
if (name && (!isForRename || name.escapedText === symbolName(exportSymbol))) {
251+
if (name && (!isForRename || name.escapedText === symbolEscapedNameNoDefault(exportSymbol))) {
252252
const defaultImportAlias = checker.getSymbolAtLocation(name);
253253
addSearch(name, defaultImportAlias);
254254
}
@@ -534,7 +534,7 @@ namespace ts.FindAllReferences {
534534
// If the import has a different name than the export, do not continue searching.
535535
// If `importedName` is undefined, do continue searching as the export is anonymous.
536536
// (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.)
537-
const importedName = symbolName(importedSymbol);
537+
const importedName = symbolEscapedNameNoDefault(importedSymbol);
538538
if (importedName === undefined || importedName === InternalSymbolName.Default || importedName === symbol.escapedName) {
539539
return { kind: ImportExport.Import, symbol: importedSymbol, ...isImport };
540540
}
@@ -606,17 +606,6 @@ namespace ts.FindAllReferences {
606606
return isExternalModuleSymbol(exportingModuleSymbol) ? { exportingModuleSymbol, exportKind } : undefined;
607607
}
608608

609-
function symbolName(symbol: Symbol): __String | undefined {
610-
if (symbol.escapedName !== InternalSymbolName.Default) {
611-
return symbol.escapedName;
612-
}
613-
614-
return forEach(symbol.declarations, decl => {
615-
const name = getNameOfDeclaration(decl);
616-
return name && name.kind === SyntaxKind.Identifier && name.escapedText;
617-
});
618-
}
619-
620609
/** If at an export specifier, go to the symbol it refers to. */
621610
function skipExportSpecifierSymbol(symbol: Symbol, checker: TypeChecker): Symbol {
622611
// For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does.

src/services/refactorProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ namespace ts {
3838
}
3939
}
4040

41-
export function getRefactorContextLength(context: RefactorContext): number {
42-
return context.endPosition === undefined ? 0 : context.endPosition - context.startPosition;
41+
export function getRefactorContextSpan({ startPosition, endPosition }: RefactorContext): TextSpan {
42+
return createTextSpanFromBounds(startPosition, endPosition === undefined ? startPosition : endPosition);
4343
}
4444
}

src/services/refactors/extractSymbol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace ts.refactor.extractSymbol {
88
* Exported for tests.
99
*/
1010
export function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
11-
const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length: getRefactorContextLength(context) });
11+
const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context));
1212

1313
const targetRange: TargetRange = rangeToExtract.targetRange;
1414
if (targetRange === undefined) {
@@ -87,7 +87,7 @@ namespace ts.refactor.extractSymbol {
8787

8888
/* Exported for tests */
8989
export function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
90-
const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length: getRefactorContextLength(context) });
90+
const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context));
9191
const targetRange: TargetRange = rangeToExtract.targetRange;
9292

9393
const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName);

0 commit comments

Comments
 (0)