Skip to content

Commit d3bbef3

Browse files
authored
'Move to file' refactor (#53542)
1 parent 09b1c55 commit d3bbef3

File tree

48 files changed

+3437
-910
lines changed

Some content is hidden

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

48 files changed

+3437
-910
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7600,6 +7600,10 @@
76007600
"category": "Message",
76017601
"code": 95177
76027602
},
7603+
"Move to file": {
7604+
"category": "Message",
7605+
"code": 95178
7606+
},
76037607

76047608
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
76057609
"category": "Error",

src/compiler/moduleSpecifiers.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,18 @@ function getPreferences(
151151
? [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index]
152152
: [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
153153
}
154+
const allowImportingTsExtension = shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.fileName);
154155
switch (preferredEnding) {
155-
case ModuleSpecifierEnding.JsExtension: return [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
156+
case ModuleSpecifierEnding.JsExtension: return allowImportingTsExtension
157+
? [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index]
158+
: [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
156159
case ModuleSpecifierEnding.TsExtension: return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index];
157-
case ModuleSpecifierEnding.Index: return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension];
158-
case ModuleSpecifierEnding.Minimal: return [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
160+
case ModuleSpecifierEnding.Index: return allowImportingTsExtension
161+
? [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension]
162+
: [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension];
163+
case ModuleSpecifierEnding.Minimal: return allowImportingTsExtension
164+
? [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension]
165+
: [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
159166
default: Debug.assertNever(preferredEnding);
160167
}
161168
},
@@ -1046,7 +1053,12 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
10461053
return fileName;
10471054
}
10481055

1049-
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
1056+
const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
1057+
const tsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.TsExtension);
1058+
if (fileExtensionIsOneOf(fileName, [Extension.Mts, Extension.Cts]) && tsPriority !== -1 && tsPriority < jsPriority) {
1059+
return fileName;
1060+
}
1061+
else if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
10501062
return noExtension + getJSExtensionForFile(fileName, options);
10511063
}
10521064
else if (!fileExtensionIsOneOf(fileName, [Extension.Dts]) && fileExtensionIsOneOf(fileName, [Extension.Ts]) && stringContains(fileName, ".d.")) {
@@ -1072,7 +1084,6 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
10721084
// know if a .d.ts extension is valid, so use no extension or a .js extension
10731085
if (isDeclarationFileName(fileName)) {
10741086
const extensionlessPriority = allowedEndings.findIndex(e => e === ModuleSpecifierEnding.Minimal || e === ModuleSpecifierEnding.Index);
1075-
const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
10761087
return extensionlessPriority !== -1 && extensionlessPriority < jsPriority
10771088
? noExtension
10781089
: noExtension + getJSExtensionForFile(fileName, options);

src/compiler/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4239,7 +4239,6 @@ export interface SourceFileLike {
42394239
getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number;
42404240
}
42414241

4242-
42434242
/** @internal */
42444243
export interface RedirectInfo {
42454244
/** Source file this redirects to. */

src/harness/client.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,14 @@ export class SessionClient implements LanguageService {
795795
return response.body!; // TODO: GH#18217
796796
}
797797

798+
getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFileName: string; files: string[]; } {
799+
const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName);
800+
801+
const request = this.processRequest<protocol.GetMoveToRefactoringFileSuggestionsRequest>(protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, args);
802+
const response = this.processResponse<protocol.GetMoveToRefactoringFileSuggestions>(request);
803+
return { newFileName: response.body?.newFileName, files:response.body?.files }!;
804+
}
805+
798806
getEditsForRefactor(
799807
fileName: string,
800808
_formatOptions: FormatCodeSettings,
@@ -818,7 +826,7 @@ export class SessionClient implements LanguageService {
818826
const renameFilename: string | undefined = response.body.renameFilename;
819827
let renameLocation: number | undefined;
820828
if (renameFilename !== undefined) {
821-
renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217
829+
renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!);
822830
}
823831

824832
return {

src/harness/fourslashImpl.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3299,7 +3299,6 @@ export class TestState {
32993299
ts.Debug.fail(`Did not expect a change in ${change.fileName}`);
33003300
}
33013301
const oldText = this.tryGetFileContent(change.fileName);
3302-
ts.Debug.assert(!!change.isNewFile === (oldText === undefined));
33033302
const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges);
33043303
this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent);
33053304
}
@@ -3912,6 +3911,18 @@ export class TestState {
39123911
this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);
39133912
}
39143913

3914+
public moveToFile(options: FourSlashInterface.MoveToFileOptions): void {
3915+
assert(this.getRanges().length === 1, "Must have exactly one fourslash range (source enclosed between '[|' and '|]' delimiters) in the source file");
3916+
const range = this.getRanges()[0];
3917+
const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }, /*triggerReason*/ undefined, /*kind*/ undefined, /*includeInteractiveActions*/ true), r => r.name === "Move to file")!;
3918+
assert(refactor.actions.length === 1);
3919+
const action = ts.first(refactor.actions);
3920+
assert(action.name === "Move to file" && action.description === "Move to file");
3921+
3922+
const editInfo = this.languageService.getEditsForRefactor(range.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions, options.interactiveRefactorArguments)!;
3923+
this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);
3924+
}
3925+
39153926
private testNewFileContents(edits: readonly ts.FileTextChanges[], newFileContents: { [fileName: string]: string }, description: string): void {
39163927
for (const { fileName, textChanges } of edits) {
39173928
const newContent = newFileContents[fileName];
@@ -4217,11 +4228,11 @@ export class TestState {
42174228
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) {
42184229
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind);
42194230
}
4220-
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] {
4221-
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line local/no-in-operator
4231+
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, includeInteractiveActions?: boolean): readonly ts.ApplicableRefactorInfo[] {
4232+
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind, includeInteractiveActions); // eslint-disable-line local/no-in-operator
42224233
}
4223-
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] {
4224-
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray;
4234+
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): readonly ts.ApplicableRefactorInfo[] {
4235+
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind, includeInteractiveActions) || ts.emptyArray;
42254236
}
42264237

42274238
public configurePlugin(pluginName: string, configuration: any): void {

src/harness/fourslashInterfaceImpl.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,10 @@ export class Verify extends VerifyNegatable {
610610
this.state.moveToNewFile(options);
611611
}
612612

613+
public moveToFile(options: MoveToFileOptions): void {
614+
this.state.moveToFile(options);
615+
}
616+
613617
public noMoveToNewFile(): void {
614618
this.state.noMoveToNewFile();
615619
}
@@ -1896,6 +1900,12 @@ export interface MoveToNewFileOptions {
18961900
readonly preferences?: ts.UserPreferences;
18971901
}
18981902

1903+
export interface MoveToFileOptions {
1904+
readonly newFileContents: { readonly [fileName: string]: string };
1905+
readonly interactiveRefactorArguments: ts.InteractiveRefactorArguments;
1906+
readonly preferences?: ts.UserPreferences;
1907+
}
1908+
18991909
export type RenameLocationsOptions = readonly RenameLocationOptions[] | {
19001910
readonly findInStrings?: boolean;
19011911
readonly findInComments?: boolean;

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ class LanguageServiceShimProxy implements ts.LanguageService {
616616
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
617617
throw new Error("Not supported on the shim.");
618618
}
619+
getMoveToRefactoringFileSuggestions(): { newFileName: string, files: string[] } {
620+
throw new Error("Not supported on the shim.");
621+
}
619622
organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] {
620623
throw new Error("Not supported on the shim.");
621624
}

src/server/protocol.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
EndOfLineState,
55
FileExtensionInfo,
66
HighlightSpanKind,
7+
InteractiveRefactorArguments,
78
MapLike,
89
OutliningSpanKind,
910
OutputFile,
@@ -142,6 +143,7 @@ export const enum CommandTypes {
142143

143144
GetApplicableRefactors = "getApplicableRefactors",
144145
GetEditsForRefactor = "getEditsForRefactor",
146+
GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions",
145147
/** @internal */
146148
GetEditsForRefactorFull = "getEditsForRefactor-full",
147149

@@ -606,6 +608,27 @@ export interface GetApplicableRefactorsResponse extends Response {
606608
body?: ApplicableRefactorInfo[];
607609
}
608610

611+
/**
612+
* Request refactorings at a given position or selection area to move to an existing file.
613+
*/
614+
export interface GetMoveToRefactoringFileSuggestionsRequest extends Request {
615+
command: CommandTypes.GetMoveToRefactoringFileSuggestions;
616+
arguments: GetMoveToRefactoringFileSuggestionsRequestArgs;
617+
}
618+
export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & {
619+
kind?: string;
620+
};
621+
/**
622+
* Response is a list of available files.
623+
* Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring
624+
*/
625+
export interface GetMoveToRefactoringFileSuggestions extends Response {
626+
body: {
627+
newFileName: string;
628+
files: string[];
629+
};
630+
}
631+
609632
/**
610633
* A set of one or more available refactoring actions, grouped under a parent refactoring.
611634
*/
@@ -680,6 +703,8 @@ export type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & {
680703
refactor: string;
681704
/* The 'name' property from the refactoring action */
682705
action: string;
706+
/* Arguments for interactive action */
707+
interactiveRefactorArguments?: InteractiveRefactorArguments;
683708
};
684709

685710

src/server/session.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [
881881
protocol.CommandTypes.ApplyCodeActionCommand,
882882
protocol.CommandTypes.GetSupportedCodeFixes,
883883
protocol.CommandTypes.GetApplicableRefactors,
884+
protocol.CommandTypes.GetMoveToRefactoringFileSuggestions,
884885
protocol.CommandTypes.GetEditsForRefactor,
885886
protocol.CommandTypes.GetEditsForRefactorFull,
886887
protocol.CommandTypes.OrganizeImports,
@@ -2687,6 +2688,7 @@ export class Session<TMessage = string> implements EventSender {
26872688
args.refactor,
26882689
args.action,
26892690
this.getPreferences(file),
2691+
args.interactiveRefactorArguments
26902692
);
26912693

26922694
if (result === undefined) {
@@ -2702,11 +2704,19 @@ export class Session<TMessage = string> implements EventSender {
27022704
const renameScriptInfo = project.getScriptInfoForNormalizedPath(toNormalizedPath(renameFilename))!;
27032705
mappedRenameLocation = getLocationInNewDocument(getSnapshotText(renameScriptInfo.getSnapshot()), renameFilename, renameLocation, edits);
27042706
}
2705-
return { renameLocation: mappedRenameLocation, renameFilename, edits: this.mapTextChangesToCodeEdits(edits) };
2706-
}
2707-
else {
2708-
return result;
2707+
return {
2708+
renameLocation: mappedRenameLocation,
2709+
renameFilename,
2710+
edits: this.mapTextChangesToCodeEdits(edits)
2711+
};
27092712
}
2713+
return result;
2714+
}
2715+
2716+
private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFileName: string, files: string[] }{
2717+
const { file, project } = this.getFileAndProject(args);
2718+
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
2719+
return project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file));
27102720
}
27112721

27122722
private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] {
@@ -3429,6 +3439,9 @@ export class Session<TMessage = string> implements EventSender {
34293439
[protocol.CommandTypes.GetEditsForRefactor]: (request: protocol.GetEditsForRefactorRequest) => {
34303440
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ true));
34313441
},
3442+
[protocol.CommandTypes.GetMoveToRefactoringFileSuggestions]: (request: protocol.GetMoveToRefactoringFileSuggestionsRequest) => {
3443+
return this.requiredResponse(this.getMoveToRefactoringFileSuggestions(request.arguments));
3444+
},
34323445
[protocol.CommandTypes.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => {
34333446
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false));
34343447
},

src/services/_namespaces/ts.refactor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from "../refactors/convertImport";
66
export * from "../refactors/extractType";
77
export * from "../refactors/helpers";
88
export * from "../refactors/moveToNewFile";
9+
export * from "../refactors/moveToFile";
910
import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction";
1011
export { addOrRemoveBracesToArrowFunction };
1112
import * as convertArrowFunctionOrFunctionExpression from "./ts.refactor.convertArrowFunctionOrFunctionExpression";

0 commit comments

Comments
 (0)