mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-15 03:23:08 -06:00
Hierarchical refactorings (#41975)
* add hierarchical refactoring strings * fourslash tests * extractSymbol filters returned actions * move refactorKind check to utilities * rename parameters * messaging for addOrRemoveBracesToArrowFunction * fix up inferFunctionReturnType * fix up convertArrowFunctionOrFunctionExpression * add preferences to fourslash method * fix up convert string * fix up moveToNewFile * fix lint errors * remove extra arrow braces diagnostics * break out tests * add refactor helpers * refactor refactors * keep list of actions * address PR comments * response protocol * address more comments
This commit is contained in:
parent
d1ac4515c8
commit
8bbef818ed
@ -6143,6 +6143,34 @@
|
||||
"category": "Message",
|
||||
"code": 95148
|
||||
},
|
||||
"Return type must be inferred from a function": {
|
||||
"category": "Message",
|
||||
"code": 95149
|
||||
},
|
||||
"Could not determine function return type": {
|
||||
"category": "Message",
|
||||
"code": 95150
|
||||
},
|
||||
"Could not convert to arrow function": {
|
||||
"category": "Message",
|
||||
"code": 95151
|
||||
},
|
||||
"Could not convert to named function": {
|
||||
"category": "Message",
|
||||
"code": 95152
|
||||
},
|
||||
"Could not convert to anonymous function": {
|
||||
"category": "Message",
|
||||
"code": 95153
|
||||
},
|
||||
"Can only convert string concatenation": {
|
||||
"category": "Message",
|
||||
"code": 95154
|
||||
},
|
||||
"Selection is not a valid statement or statements": {
|
||||
"category": "Message",
|
||||
"code": 95155
|
||||
},
|
||||
|
||||
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
|
||||
"category": "Error",
|
||||
|
||||
@ -3420,6 +3420,12 @@ namespace FourSlash {
|
||||
}
|
||||
}
|
||||
|
||||
public verifyRefactorKindsAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
|
||||
const refactors = this.getApplicableRefactorsAtSelection("invoked", kind, preferences);
|
||||
const availableKinds = ts.flatMap(refactors, refactor => refactor.actions).map(action => action.kind);
|
||||
assert.deepEqual(availableKinds.sort(), expected.sort(), `Expected kinds to be equal`);
|
||||
}
|
||||
|
||||
public verifyRefactorsAvailable(names: readonly string[]): void {
|
||||
assert.deepEqual(unique(this.getApplicableRefactorsAtSelection(), r => r.name), names);
|
||||
}
|
||||
@ -3833,14 +3839,14 @@ namespace FourSlash {
|
||||
test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved");
|
||||
}
|
||||
|
||||
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit") {
|
||||
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, ts.emptyOptions, triggerReason);
|
||||
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) {
|
||||
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind);
|
||||
}
|
||||
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit"): readonly ts.ApplicableRefactorInfo[] {
|
||||
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason); // eslint-disable-line no-in-operator
|
||||
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] {
|
||||
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line no-in-operator
|
||||
}
|
||||
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason): readonly ts.ApplicableRefactorInfo[] {
|
||||
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason) || ts.emptyArray;
|
||||
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] {
|
||||
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray;
|
||||
}
|
||||
|
||||
public configurePlugin(pluginName: string, configuration: any): void {
|
||||
|
||||
@ -215,6 +215,10 @@ namespace FourSlashInterface {
|
||||
this.state.verifyRefactorAvailable(this.negative, triggerReason, name, actionName);
|
||||
}
|
||||
|
||||
public refactorKindAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
|
||||
this.state.verifyRefactorKindsAvailable(kind, expected, preferences);
|
||||
}
|
||||
|
||||
public toggleLineComment(newFileContent: string) {
|
||||
this.state.toggleLineComment(newFileContent);
|
||||
}
|
||||
|
||||
@ -566,7 +566,8 @@ namespace ts.server.protocol {
|
||||
arguments: GetApplicableRefactorsRequestArgs;
|
||||
}
|
||||
export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & {
|
||||
triggerReason?: RefactorTriggerReason
|
||||
triggerReason?: RefactorTriggerReason;
|
||||
kind?: string;
|
||||
};
|
||||
|
||||
export type RefactorTriggerReason = "implicit" | "invoked";
|
||||
@ -626,6 +627,11 @@ namespace ts.server.protocol {
|
||||
* the current context.
|
||||
*/
|
||||
notApplicableReason?: string;
|
||||
|
||||
/**
|
||||
* The hierarchical dotted name of the refactor action.
|
||||
*/
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface GetEditsForRefactorRequest extends Request {
|
||||
|
||||
@ -2129,7 +2129,7 @@ namespace ts.server {
|
||||
private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] {
|
||||
const { file, project } = this.getFileAndProject(args);
|
||||
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
|
||||
return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason);
|
||||
return project.getLanguageService().getApplicableRefactors(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind);
|
||||
}
|
||||
|
||||
private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo {
|
||||
|
||||
@ -4,7 +4,8 @@ namespace ts.codefix {
|
||||
type AcceptedNameType = Identifier | StringLiteral;
|
||||
type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression;
|
||||
|
||||
interface Info {
|
||||
type Info = AccessorInfo | refactor.RefactorErrorInfo;
|
||||
interface AccessorInfo {
|
||||
readonly container: ContainerDeclaration;
|
||||
readonly isStatic: boolean;
|
||||
readonly isReadonly: boolean;
|
||||
@ -16,20 +17,12 @@ namespace ts.codefix {
|
||||
readonly renameAccessor: boolean;
|
||||
}
|
||||
|
||||
type InfoOrError = {
|
||||
info: Info,
|
||||
error?: never
|
||||
} | {
|
||||
info?: never,
|
||||
error: string
|
||||
};
|
||||
|
||||
export function generateAccessorFromProperty(file: SourceFile, program: Program, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined {
|
||||
const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, program, start, end);
|
||||
if (!fieldInfo || !fieldInfo.info) return undefined;
|
||||
if (!fieldInfo || refactor.isRefactorErrorInfo(fieldInfo)) return undefined;
|
||||
|
||||
const changeTracker = textChanges.ChangeTracker.fromContext(context);
|
||||
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo.info;
|
||||
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo;
|
||||
|
||||
suppressLeadingAndTrailingTrivia(fieldName);
|
||||
suppressLeadingAndTrailingTrivia(accessorName);
|
||||
@ -112,7 +105,7 @@ namespace ts.codefix {
|
||||
return modifierFlags;
|
||||
}
|
||||
|
||||
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): InfoOrError | undefined {
|
||||
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): Info | undefined {
|
||||
const node = getTokenAtPosition(file, start);
|
||||
const cursorRequest = start === end && considerEmptySpans;
|
||||
const declaration = findAncestor(node.parent, isAcceptedDeclaration);
|
||||
@ -142,17 +135,15 @@ namespace ts.codefix {
|
||||
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
|
||||
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
|
||||
return {
|
||||
info: {
|
||||
isStatic: hasStaticModifier(declaration),
|
||||
isReadonly: hasEffectiveReadonlyModifier(declaration),
|
||||
type: getDeclarationType(declaration, program),
|
||||
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
|
||||
originalName: (<AcceptedNameType>declaration.name).text,
|
||||
declaration,
|
||||
fieldName,
|
||||
accessorName,
|
||||
renameAccessor: startWithUnderscore
|
||||
}
|
||||
isStatic: hasStaticModifier(declaration),
|
||||
isReadonly: hasEffectiveReadonlyModifier(declaration),
|
||||
type: getDeclarationType(declaration, program),
|
||||
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
|
||||
originalName: (<AcceptedNameType>declaration.name).text,
|
||||
declaration,
|
||||
fieldName,
|
||||
accessorName,
|
||||
renameAccessor: startWithUnderscore
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@ namespace ts.refactor {
|
||||
|
||||
export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] {
|
||||
return arrayFrom(flatMapIterator(refactors.values(), refactor =>
|
||||
context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context)));
|
||||
context.cancellationToken && context.cancellationToken.isCancellationRequested() ||
|
||||
!refactor.kinds?.some(kind => refactorKindBeginsWith(kind, context.kind)) ? undefined :
|
||||
refactor.getAvailableActions(context)));
|
||||
}
|
||||
|
||||
export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {
|
||||
|
||||
@ -2,45 +2,40 @@
|
||||
namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
const refactorName = "Add or remove braces in an arrow function";
|
||||
const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message;
|
||||
const addBracesActionName = "Add braces to arrow function";
|
||||
const removeBracesActionName = "Remove braces from arrow function";
|
||||
const addBracesActionDescription = Diagnostics.Add_braces_to_arrow_function.message;
|
||||
const removeBracesActionDescription = Diagnostics.Remove_braces_from_arrow_function.message;
|
||||
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
|
||||
|
||||
interface Info {
|
||||
const addBracesAction = {
|
||||
name: "Add braces to arrow function",
|
||||
description: Diagnostics.Add_braces_to_arrow_function.message,
|
||||
kind: "refactor.rewrite.arrow.braces.add",
|
||||
};
|
||||
const removeBracesAction = {
|
||||
name: "Remove braces from arrow function",
|
||||
description: Diagnostics.Remove_braces_from_arrow_function.message,
|
||||
kind: "refactor.rewrite.arrow.braces.remove"
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [removeBracesAction.kind],
|
||||
getEditsForAction,
|
||||
getAvailableActions });
|
||||
|
||||
interface FunctionBracesInfo {
|
||||
func: ArrowFunction;
|
||||
expression: Expression | undefined;
|
||||
returnStatement?: ReturnStatement;
|
||||
addBraces: boolean;
|
||||
}
|
||||
|
||||
type InfoOrError = {
|
||||
info: Info,
|
||||
error?: never
|
||||
} | {
|
||||
info?: never,
|
||||
error: string
|
||||
};
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const { file, startPosition, triggerReason } = context;
|
||||
const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked");
|
||||
if (!info) return emptyArray;
|
||||
|
||||
if (info.error === undefined) {
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [
|
||||
info.info.addBraces ?
|
||||
{
|
||||
name: addBracesActionName,
|
||||
description: addBracesActionDescription
|
||||
} : {
|
||||
name: removeBracesActionName,
|
||||
description: removeBracesActionDescription
|
||||
}
|
||||
info.addBraces ? addBracesAction : removeBracesAction
|
||||
]
|
||||
}];
|
||||
}
|
||||
@ -49,15 +44,10 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [{
|
||||
name: addBracesActionName,
|
||||
description: addBracesActionDescription,
|
||||
notApplicableReason: info.error
|
||||
}, {
|
||||
name: removeBracesActionName,
|
||||
description: removeBracesActionDescription,
|
||||
notApplicableReason: info.error
|
||||
}]
|
||||
actions: [
|
||||
{ ...addBracesAction, notApplicableReason: info.error },
|
||||
{ ...removeBracesAction, notApplicableReason: info.error },
|
||||
]
|
||||
}];
|
||||
}
|
||||
|
||||
@ -67,19 +57,19 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
|
||||
const { file, startPosition } = context;
|
||||
const info = getConvertibleArrowFunctionAtPosition(file, startPosition);
|
||||
if (!info || !info.info) return undefined;
|
||||
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
|
||||
|
||||
const { expression, returnStatement, func } = info.info;
|
||||
const { expression, returnStatement, func } = info;
|
||||
|
||||
let body: ConciseBody;
|
||||
|
||||
if (actionName === addBracesActionName) {
|
||||
if (actionName === addBracesAction.name) {
|
||||
const returnStatement = factory.createReturnStatement(expression);
|
||||
body = factory.createBlock([returnStatement], /* multiLine */ true);
|
||||
suppressLeadingAndTrailingTrivia(body);
|
||||
copyLeadingComments(expression!, returnStatement, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true);
|
||||
}
|
||||
else if (actionName === removeBracesActionName && returnStatement) {
|
||||
else if (actionName === removeBracesAction.name && returnStatement) {
|
||||
const actualExpression = expression || factory.createVoidZero();
|
||||
body = needsParentheses(actualExpression) ? factory.createParenthesizedExpression(actualExpression) : actualExpression;
|
||||
suppressLeadingAndTrailingTrivia(body);
|
||||
@ -98,7 +88,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
return { renameFilename: undefined, renameLocation: undefined, edits };
|
||||
}
|
||||
|
||||
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): InfoOrError | undefined {
|
||||
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true, kind?: string): FunctionBracesInfo | RefactorErrorInfo | undefined {
|
||||
const node = getTokenAtPosition(file, startPosition);
|
||||
const func = getContainingFunction(node);
|
||||
|
||||
@ -118,26 +108,13 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isExpression(func.body)) {
|
||||
return {
|
||||
info: {
|
||||
func,
|
||||
addBraces: true,
|
||||
expression: func.body
|
||||
}
|
||||
};
|
||||
if (refactorKindBeginsWith(addBracesAction.kind, kind) && isExpression(func.body)) {
|
||||
return { func, addBraces: true, expression: func.body };
|
||||
}
|
||||
else if (func.body.statements.length === 1) {
|
||||
else if (refactorKindBeginsWith(removeBracesAction.kind, kind) && isBlock(func.body) && func.body.statements.length === 1) {
|
||||
const firstStatement = first(func.body.statements);
|
||||
if (isReturnStatement(firstStatement)) {
|
||||
return {
|
||||
info: {
|
||||
func,
|
||||
addBraces: false,
|
||||
expression: firstStatement.expression,
|
||||
returnStatement: firstStatement
|
||||
}
|
||||
};
|
||||
return { func, addBraces: false, expression: firstStatement.expression, returnStatement: firstStatement };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@ -3,15 +3,30 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
|
||||
const refactorName = "Convert arrow function or function expression";
|
||||
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_arrow_function_or_function_expression);
|
||||
|
||||
const toAnonymousFunctionActionName = "Convert to anonymous function";
|
||||
const toNamedFunctionActionName = "Convert to named function";
|
||||
const toArrowFunctionActionName = "Convert to arrow function";
|
||||
|
||||
const toAnonymousFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function);
|
||||
const toNamedFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_named_function);
|
||||
const toArrowFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function);
|
||||
|
||||
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
|
||||
const toAnonymousFunctionAction = {
|
||||
name: "Convert to anonymous function",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function),
|
||||
kind: "refactor.rewrite.function.anonymous",
|
||||
};
|
||||
const toNamedFunctionAction = {
|
||||
name: "Convert to named function",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Convert_to_named_function),
|
||||
kind: "refactor.rewrite.function.named",
|
||||
};
|
||||
const toArrowFunctionAction = {
|
||||
name: "Convert to arrow function",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function),
|
||||
kind: "refactor.rewrite.function.arrow",
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [
|
||||
toAnonymousFunctionAction.kind,
|
||||
toNamedFunctionAction.kind,
|
||||
toArrowFunctionAction.kind
|
||||
],
|
||||
getEditsForAction,
|
||||
getAvailableActions
|
||||
});
|
||||
|
||||
interface FunctionInfo {
|
||||
readonly selectedVariableDeclaration: boolean;
|
||||
@ -26,38 +41,50 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
|
||||
}
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const { file, startPosition, program } = context;
|
||||
const { file, startPosition, program, kind } = context;
|
||||
const info = getFunctionInfo(file, startPosition, program);
|
||||
|
||||
if (!info) return emptyArray;
|
||||
const { selectedVariableDeclaration, func } = info;
|
||||
const possibleActions: RefactorActionInfo[] = [];
|
||||
|
||||
if (selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent))) {
|
||||
possibleActions.push({
|
||||
name: toNamedFunctionActionName,
|
||||
description: toNamedFunctionActionDescription
|
||||
});
|
||||
const errors: RefactorActionInfo[] = [];
|
||||
if (refactorKindBeginsWith(toNamedFunctionAction.kind, kind)) {
|
||||
const error = selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent)) ?
|
||||
undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_named_function);
|
||||
if (error) {
|
||||
errors.push({ ...toNamedFunctionAction, notApplicableReason: error });
|
||||
}
|
||||
else {
|
||||
possibleActions.push(toNamedFunctionAction);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedVariableDeclaration && isArrowFunction(func)) {
|
||||
possibleActions.push({
|
||||
name: toAnonymousFunctionActionName,
|
||||
description: toAnonymousFunctionActionDescription
|
||||
});
|
||||
if (refactorKindBeginsWith(toAnonymousFunctionAction.kind, kind)) {
|
||||
const error = !selectedVariableDeclaration && isArrowFunction(func) ?
|
||||
undefined: getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_anonymous_function);
|
||||
if (error) {
|
||||
errors.push({ ...toAnonymousFunctionAction, notApplicableReason: error });
|
||||
}
|
||||
else {
|
||||
possibleActions.push(toAnonymousFunctionAction);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFunctionExpression(func)) {
|
||||
possibleActions.push({
|
||||
name: toArrowFunctionActionName,
|
||||
description: toArrowFunctionActionDescription
|
||||
});
|
||||
if (refactorKindBeginsWith(toArrowFunctionAction.kind, kind)) {
|
||||
const error = isFunctionExpression(func) ? undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_arrow_function);
|
||||
if (error) {
|
||||
errors.push({ ...toArrowFunctionAction, notApplicableReason: error });
|
||||
}
|
||||
else {
|
||||
possibleActions.push(toArrowFunctionAction);
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: possibleActions
|
||||
actions: possibleActions.length === 0 && context.preferences.provideRefactorNotApplicableReason ?
|
||||
errors : possibleActions
|
||||
}];
|
||||
}
|
||||
|
||||
@ -70,18 +97,18 @@ namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
|
||||
const edits: FileTextChanges[] = [];
|
||||
|
||||
switch (actionName) {
|
||||
case toAnonymousFunctionActionName:
|
||||
case toAnonymousFunctionAction.name:
|
||||
edits.push(...getEditInfoForConvertToAnonymousFunction(context, func));
|
||||
break;
|
||||
|
||||
case toNamedFunctionActionName:
|
||||
case toNamedFunctionAction.name:
|
||||
const variableInfo = getVariableInfo(func);
|
||||
if (!variableInfo) return undefined;
|
||||
|
||||
edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo));
|
||||
break;
|
||||
|
||||
case toArrowFunctionActionName:
|
||||
case toArrowFunctionAction.name:
|
||||
if (!isFunctionExpression(func)) return undefined;
|
||||
edits.push(...getEditInfoForConvertToArrowFunction(context, func));
|
||||
break;
|
||||
|
||||
@ -1,54 +1,62 @@
|
||||
/* @internal */
|
||||
namespace ts.refactor {
|
||||
const refactorName = "Convert export";
|
||||
const actionNameDefaultToNamed = "Convert default export to named export";
|
||||
const actionNameNamedToDefault = "Convert named export to default export";
|
||||
|
||||
const defaultToNamedAction = {
|
||||
name: "Convert default export to named export",
|
||||
description: Diagnostics.Convert_default_export_to_named_export.message,
|
||||
kind: "refactor.rewrite.export.named"
|
||||
};
|
||||
const namedToDefaultAction = {
|
||||
name: "Convert named export to default export",
|
||||
description: Diagnostics.Convert_named_export_to_default_export.message,
|
||||
kind: "refactor.rewrite.export.default"
|
||||
};
|
||||
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [
|
||||
defaultToNamedAction.kind,
|
||||
namedToDefaultAction.kind
|
||||
],
|
||||
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
|
||||
const info = getInfo(context, context.triggerReason === "invoked");
|
||||
if (!info) return emptyArray;
|
||||
|
||||
if (info.error === undefined) {
|
||||
const description = info.info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message;
|
||||
const actionName = info.info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault;
|
||||
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
const action = info.wasDefault ? defaultToNamedAction : namedToDefaultAction;
|
||||
return [{ name: refactorName, description: action.description, actions: [action] }];
|
||||
}
|
||||
|
||||
if (context.preferences.provideRefactorNotApplicableReason) {
|
||||
return [
|
||||
{ name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [{ name: actionNameDefaultToNamed, description: Diagnostics.Convert_default_export_to_named_export.message, notApplicableReason: info.error }] },
|
||||
{ name: refactorName, description: Diagnostics.Convert_named_export_to_default_export.message, actions: [{ name: actionNameNamedToDefault, description: Diagnostics.Convert_named_export_to_default_export.message, notApplicableReason: info.error }] },
|
||||
{ name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [
|
||||
{ ...defaultToNamedAction, notApplicableReason: info.error },
|
||||
{ ...namedToDefaultAction, notApplicableReason: info.error },
|
||||
]}
|
||||
];
|
||||
}
|
||||
|
||||
return emptyArray;
|
||||
},
|
||||
getEditsForAction(context, actionName): RefactorEditInfo {
|
||||
Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name");
|
||||
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context)?.info, "context must have info"), t, context.cancellationToken));
|
||||
Debug.assert(actionName === defaultToNamedAction.name || actionName === namedToDefaultAction.name, "Unexpected action name");
|
||||
const info = getInfo(context);
|
||||
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
|
||||
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, info, t, context.cancellationToken));
|
||||
return { edits, renameFilename: undefined, renameLocation: undefined };
|
||||
},
|
||||
});
|
||||
|
||||
// If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name.
|
||||
type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement;
|
||||
interface Info {
|
||||
interface ExportInfo {
|
||||
readonly exportNode: ExportToConvert;
|
||||
readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s.
|
||||
readonly wasDefault: boolean;
|
||||
readonly exportingModuleSymbol: Symbol;
|
||||
}
|
||||
|
||||
type InfoOrError = {
|
||||
info: Info,
|
||||
error?: never
|
||||
} | {
|
||||
info?: never,
|
||||
error: string
|
||||
};
|
||||
|
||||
function getInfo(context: RefactorContext, considerPartialSpans = true): InfoOrError | undefined {
|
||||
function getInfo(context: RefactorContext, considerPartialSpans = true): ExportInfo | RefactorErrorInfo | undefined {
|
||||
const { file } = context;
|
||||
const span = getRefactorContextSpan(context);
|
||||
const token = getTokenAtPosition(file, span.start);
|
||||
@ -74,7 +82,7 @@ namespace ts.refactor {
|
||||
case SyntaxKind.TypeAliasDeclaration:
|
||||
case SyntaxKind.ModuleDeclaration: {
|
||||
const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration;
|
||||
return node.name && isIdentifier(node.name) ? { info: { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } } : undefined;
|
||||
return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined;
|
||||
}
|
||||
case SyntaxKind.VariableStatement: {
|
||||
const vs = exportNode as VariableStatement;
|
||||
@ -85,19 +93,19 @@ namespace ts.refactor {
|
||||
const decl = first(vs.declarationList.declarations);
|
||||
if (!decl.initializer) return undefined;
|
||||
Debug.assert(!wasDefault, "Can't have a default flag here");
|
||||
return isIdentifier(decl.name) ? { info: { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } } : undefined;
|
||||
return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
|
||||
function doChange(exportingSourceFile: SourceFile, program: Program, info: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
|
||||
changeExport(exportingSourceFile, info, changes, program.getTypeChecker());
|
||||
changeImports(program, info, changes, cancellationToken);
|
||||
}
|
||||
|
||||
function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: textChanges.ChangeTracker, checker: TypeChecker): void {
|
||||
function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: ExportInfo, changes: textChanges.ChangeTracker, checker: TypeChecker): void {
|
||||
if (wasDefault) {
|
||||
changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list"));
|
||||
}
|
||||
@ -131,7 +139,7 @@ namespace ts.refactor {
|
||||
}
|
||||
}
|
||||
|
||||
function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
|
||||
function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: ExportInfo, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void {
|
||||
const checker = program.getTypeChecker();
|
||||
const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol");
|
||||
FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => {
|
||||
|
||||
@ -1,46 +1,55 @@
|
||||
/* @internal */
|
||||
namespace ts.refactor {
|
||||
const refactorName = "Convert import";
|
||||
const actionNameNamespaceToNamed = "Convert namespace import to named imports";
|
||||
const actionNameNamedToNamespace = "Convert named imports to namespace import";
|
||||
|
||||
type NamedImportBindingsOrError = {
|
||||
info: NamedImportBindings,
|
||||
error?: never
|
||||
} | {
|
||||
info?: never,
|
||||
error: string
|
||||
const namespaceToNamedAction = {
|
||||
name: "Convert namespace import to named imports",
|
||||
description: Diagnostics.Convert_namespace_import_to_named_imports.message,
|
||||
kind: "refactor.rewrite.import.named",
|
||||
};
|
||||
const namedToNamespaceAction = {
|
||||
name: "Convert named imports to namespace import",
|
||||
description: Diagnostics.Convert_named_imports_to_namespace_import.message,
|
||||
kind: "refactor.rewrite.import.namespace",
|
||||
};
|
||||
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [
|
||||
namespaceToNamedAction.kind,
|
||||
namedToNamespaceAction.kind
|
||||
],
|
||||
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
|
||||
const i = getImportToConvert(context, context.triggerReason === "invoked");
|
||||
if (!i) return emptyArray;
|
||||
const info = getImportToConvert(context, context.triggerReason === "invoked");
|
||||
if (!info) return emptyArray;
|
||||
|
||||
if (i.error === undefined) {
|
||||
const description = i.info.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message;
|
||||
const actionName = i.info.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace;
|
||||
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
const namespaceImport = info.kind === SyntaxKind.NamespaceImport;
|
||||
const action = namespaceImport ? namespaceToNamedAction : namedToNamespaceAction;
|
||||
return [{ name: refactorName, description: action.description, actions: [action] }];
|
||||
}
|
||||
|
||||
if (context.preferences.provideRefactorNotApplicableReason) {
|
||||
return [
|
||||
{ name: refactorName, description: Diagnostics.Convert_namespace_import_to_named_imports.message, actions: [{ name: actionNameNamespaceToNamed, description: Diagnostics.Convert_namespace_import_to_named_imports.message, notApplicableReason: i.error }] },
|
||||
{ name: refactorName, description: Diagnostics.Convert_named_imports_to_namespace_import.message, actions: [{ name: actionNameNamedToNamespace, description: Diagnostics.Convert_named_imports_to_namespace_import.message, notApplicableReason: i.error }] }
|
||||
{ name: refactorName, description: namespaceToNamedAction.description,
|
||||
actions: [{ ...namespaceToNamedAction, notApplicableReason: info.error }] },
|
||||
{ name: refactorName, description: namedToNamespaceAction.description,
|
||||
actions: [{ ...namedToNamespaceAction, notApplicableReason: info.error }] }
|
||||
];
|
||||
}
|
||||
|
||||
return emptyArray;
|
||||
},
|
||||
getEditsForAction(context, actionName): RefactorEditInfo {
|
||||
Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace, "Unexpected action name");
|
||||
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context)?.info, "Context must provide an import to convert")));
|
||||
Debug.assert(actionName === namespaceToNamedAction.name || actionName === namedToNamespaceAction.name, "Unexpected action name");
|
||||
const info = getImportToConvert(context);
|
||||
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
|
||||
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, info));
|
||||
return { edits, renameFilename: undefined, renameLocation: undefined };
|
||||
}
|
||||
});
|
||||
|
||||
// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`.
|
||||
function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindingsOrError | undefined {
|
||||
function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | RefactorErrorInfo | undefined {
|
||||
const { file } = context;
|
||||
const span = getRefactorContextSpan(context);
|
||||
const token = getTokenAtPosition(file, span.start);
|
||||
@ -57,7 +66,7 @@ namespace ts.refactor {
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) };
|
||||
}
|
||||
|
||||
return { info: importClause.namedBindings };
|
||||
return importClause.namedBindings;
|
||||
}
|
||||
|
||||
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {
|
||||
|
||||
@ -2,8 +2,17 @@
|
||||
namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
const refactorName = "Convert overload list to single signature";
|
||||
const refactorDescription = Diagnostics.Convert_overload_list_to_single_signature.message;
|
||||
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
|
||||
|
||||
const functionOverloadAction = {
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
kind: "refactor.rewrite.function.overloadList",
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [functionOverloadAction.kind],
|
||||
getEditsForAction,
|
||||
getAvailableActions
|
||||
});
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const { file, startPosition, program } = context;
|
||||
@ -13,10 +22,7 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [{
|
||||
name: refactorName,
|
||||
description: refactorDescription
|
||||
}]
|
||||
actions: [functionOverloadAction]
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,18 @@
|
||||
namespace ts.refactor.convertParamsToDestructuredObject {
|
||||
const refactorName = "Convert parameters to destructured object";
|
||||
const minimumParameterLength = 2;
|
||||
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
|
||||
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object);
|
||||
|
||||
const toDestructuredAction = {
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
kind: "refactor.rewrite.parameters.toDestructured"
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [toDestructuredAction.kind],
|
||||
getEditsForAction,
|
||||
getAvailableActions
|
||||
});
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const { file, startPosition } = context;
|
||||
@ -12,14 +22,10 @@ namespace ts.refactor.convertParamsToDestructuredObject {
|
||||
const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker());
|
||||
if (!functionDeclaration) return emptyArray;
|
||||
|
||||
const description = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object);
|
||||
return [{
|
||||
name: refactorName,
|
||||
description,
|
||||
actions: [{
|
||||
name: refactorName,
|
||||
description
|
||||
}]
|
||||
description: refactorDescription,
|
||||
actions: [toDestructuredAction]
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,16 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
|
||||
const refactorName = "Convert to template string";
|
||||
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string);
|
||||
|
||||
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
|
||||
const convertStringAction = {
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
kind: "refactor.rewrite.string"
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [convertStringAction.kind],
|
||||
getEditsForAction,
|
||||
getAvailableActions
|
||||
});
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const { file, startPosition } = context;
|
||||
@ -12,7 +21,13 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
|
||||
const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] };
|
||||
|
||||
if (isBinaryExpression(maybeBinary) && isStringConcatenationValid(maybeBinary)) {
|
||||
refactorInfo.actions.push({ name: refactorName, description: refactorDescription });
|
||||
refactorInfo.actions.push(convertStringAction);
|
||||
return [refactorInfo];
|
||||
}
|
||||
else if (context.preferences.provideRefactorNotApplicableReason) {
|
||||
refactorInfo.actions.push({ ...convertStringAction,
|
||||
notApplicableReason: getLocaleSpecificMessage(Diagnostics.Can_only_convert_string_concatenation)
|
||||
});
|
||||
return [refactorInfo];
|
||||
}
|
||||
return emptyArray;
|
||||
|
||||
@ -3,20 +3,26 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
const refactorName = "Convert to optional chain expression";
|
||||
const convertToOptionalChainExpressionMessage = getLocaleSpecificMessage(Diagnostics.Convert_to_optional_chain_expression);
|
||||
|
||||
registerRefactor(refactorName, { getAvailableActions, getEditsForAction });
|
||||
const toOptionalChainAction = {
|
||||
name: refactorName,
|
||||
description: convertToOptionalChainExpressionMessage,
|
||||
kind: "refactor.rewrite.expression.optionalChain",
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [toOptionalChainAction.kind],
|
||||
getAvailableActions,
|
||||
getEditsForAction
|
||||
});
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const info = getInfo(context, context.triggerReason === "invoked");
|
||||
if (!info) return emptyArray;
|
||||
|
||||
if (!info.error) {
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: convertToOptionalChainExpressionMessage,
|
||||
actions: [{
|
||||
name: refactorName,
|
||||
description: convertToOptionalChainExpressionMessage
|
||||
}]
|
||||
actions: [toOptionalChainAction],
|
||||
}];
|
||||
}
|
||||
|
||||
@ -24,11 +30,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: convertToOptionalChainExpressionMessage,
|
||||
actions: [{
|
||||
name: refactorName,
|
||||
description: convertToOptionalChainExpressionMessage,
|
||||
notApplicableReason: info.error
|
||||
}]
|
||||
actions: [{ ...toOptionalChainAction, notApplicableReason: info.error }],
|
||||
}];
|
||||
}
|
||||
return emptyArray;
|
||||
@ -36,24 +38,16 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
|
||||
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
|
||||
const info = getInfo(context);
|
||||
if (!info || !info.info) return undefined;
|
||||
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
|
||||
const edits = textChanges.ChangeTracker.with(context, t =>
|
||||
doChange(context.file, context.program.getTypeChecker(), t, Debug.checkDefined(info.info, "context must have info"), actionName)
|
||||
doChange(context.file, context.program.getTypeChecker(), t, info, actionName)
|
||||
);
|
||||
return { edits, renameFilename: undefined, renameLocation: undefined };
|
||||
}
|
||||
|
||||
type InfoOrError = {
|
||||
info: Info,
|
||||
error?: never;
|
||||
} | {
|
||||
info?: never,
|
||||
error: string;
|
||||
};
|
||||
|
||||
type Occurrence = PropertyAccessExpression | ElementAccessExpression | Identifier;
|
||||
|
||||
interface Info {
|
||||
interface OptionalChainInfo {
|
||||
finalExpression: PropertyAccessExpression | ElementAccessExpression | CallExpression,
|
||||
occurrences: Occurrence[],
|
||||
expression: ValidExpression,
|
||||
@ -83,7 +77,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
return isValidExpression(node) || isValidStatement(node);
|
||||
}
|
||||
|
||||
function getInfo(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined {
|
||||
function getInfo(context: RefactorContext, considerEmptySpans = true): OptionalChainInfo | RefactorErrorInfo | undefined {
|
||||
const { file, program } = context;
|
||||
const span = getRefactorContextSpan(context);
|
||||
|
||||
@ -103,7 +97,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
return isConditionalExpression(expression) ? getConditionalInfo(expression, checker) : getBinaryInfo(expression);
|
||||
}
|
||||
|
||||
function getConditionalInfo(expression: ConditionalExpression, checker: TypeChecker): InfoOrError | undefined {
|
||||
function getConditionalInfo(expression: ConditionalExpression, checker: TypeChecker): OptionalChainInfo | RefactorErrorInfo | undefined {
|
||||
const condition = expression.condition;
|
||||
const finalExpression = getFinalExpressionInChain(expression.whenTrue);
|
||||
|
||||
@ -113,16 +107,16 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
|
||||
if ((isPropertyAccessExpression(condition) || isIdentifier(condition))
|
||||
&& getMatchingStart(condition, finalExpression.expression)) {
|
||||
return { info: { finalExpression, occurrences: [condition], expression } };
|
||||
return { finalExpression, occurrences: [condition], expression };
|
||||
}
|
||||
else if (isBinaryExpression(condition)) {
|
||||
const occurrences = getOccurrencesInExpression(finalExpression.expression, condition);
|
||||
return occurrences ? { info: { finalExpression, occurrences, expression } } :
|
||||
return occurrences ? { finalExpression, occurrences, expression } :
|
||||
{ error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) };
|
||||
}
|
||||
}
|
||||
|
||||
function getBinaryInfo(expression: BinaryExpression): InfoOrError | undefined {
|
||||
function getBinaryInfo(expression: BinaryExpression): OptionalChainInfo | RefactorErrorInfo | undefined {
|
||||
if (expression.operatorToken.kind !== SyntaxKind.AmpersandAmpersandToken) {
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_logical_AND_access_chains) };
|
||||
};
|
||||
@ -131,7 +125,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
if (!finalExpression) return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_convertible_access_expression) };
|
||||
|
||||
const occurrences = getOccurrencesInExpression(finalExpression.expression, expression.left);
|
||||
return occurrences ? { info: { finalExpression, occurrences, expression } } :
|
||||
return occurrences ? { finalExpression, occurrences, expression } :
|
||||
{ error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) };
|
||||
}
|
||||
|
||||
@ -288,7 +282,7 @@ namespace ts.refactor.convertToOptionalChainExpression {
|
||||
return toConvert;
|
||||
}
|
||||
|
||||
function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: Info, _actionName: string): void {
|
||||
function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: OptionalChainInfo, _actionName: string): void {
|
||||
const { finalExpression, occurrences, expression } = info;
|
||||
const firstOccurrence = occurrences[occurrences.length - 1];
|
||||
const convertedChain = convertOccurrences(checker, finalExpression, occurrences);
|
||||
|
||||
@ -1,39 +1,56 @@
|
||||
/* @internal */
|
||||
namespace ts.refactor.extractSymbol {
|
||||
const refactorName = "Extract Symbol";
|
||||
registerRefactor(refactorName, { getAvailableActions, getEditsForAction });
|
||||
|
||||
const extractConstantAction = {
|
||||
name: "Extract Constant",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_constant),
|
||||
kind: "refactor.extract.constant",
|
||||
};
|
||||
const extractFunctionAction = {
|
||||
name: "Extract Function",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
|
||||
kind: "refactor.extract.function",
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [
|
||||
extractConstantAction.kind,
|
||||
extractFunctionAction.kind
|
||||
],
|
||||
getAvailableActions,
|
||||
getEditsForAction
|
||||
});
|
||||
|
||||
/**
|
||||
* Compute the associated code actions
|
||||
* Exported for tests.
|
||||
*/
|
||||
export function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const requestedRefactor = context.kind;
|
||||
const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context), context.triggerReason === "invoked");
|
||||
|
||||
const targetRange = rangeToExtract.targetRange;
|
||||
|
||||
if (targetRange === undefined) {
|
||||
if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
|
||||
actions: [{
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
|
||||
name: "function_extract_error",
|
||||
notApplicableReason: getStringError(rangeToExtract.errors)
|
||||
}]
|
||||
},
|
||||
{
|
||||
name: refactorName,
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_constant),
|
||||
actions: [{
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_constant),
|
||||
name: "constant_extract_error",
|
||||
notApplicableReason: getStringError(rangeToExtract.errors)
|
||||
}]
|
||||
}];
|
||||
const errors = [];
|
||||
if (refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) {
|
||||
errors.push({
|
||||
name: refactorName,
|
||||
description: extractFunctionAction.description,
|
||||
actions: [{ ...extractFunctionAction, notApplicableReason: getStringError(rangeToExtract.errors) }]
|
||||
});
|
||||
}
|
||||
if (refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
|
||||
errors.push({
|
||||
name: refactorName,
|
||||
description: extractConstantAction.description,
|
||||
actions: [{ ...extractConstantAction, notApplicableReason: getStringError(rangeToExtract.errors) }]
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
const extractions = getPossibleExtractions(targetRange, context);
|
||||
@ -53,46 +70,54 @@ namespace ts.refactor.extractSymbol {
|
||||
let i = 0;
|
||||
for (const { functionExtraction, constantExtraction } of extractions) {
|
||||
const description = functionExtraction.description;
|
||||
if (functionExtraction.errors.length === 0) {
|
||||
// Don't issue refactorings with duplicated names.
|
||||
// Scopes come back in "innermost first" order, so extractions will
|
||||
// preferentially go into nearer scopes
|
||||
if (!usedFunctionNames.has(description)) {
|
||||
usedFunctionNames.set(description, true);
|
||||
functionActions.push({
|
||||
description,
|
||||
name: `function_scope_${i}`
|
||||
});
|
||||
if(refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)){
|
||||
if (functionExtraction.errors.length === 0) {
|
||||
// Don't issue refactorings with duplicated names.
|
||||
// Scopes come back in "innermost first" order, so extractions will
|
||||
// preferentially go into nearer scopes
|
||||
if (!usedFunctionNames.has(description)) {
|
||||
usedFunctionNames.set(description, true);
|
||||
functionActions.push({
|
||||
description,
|
||||
name: `function_scope_${i}`,
|
||||
kind: extractFunctionAction.kind
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!innermostErrorFunctionAction) {
|
||||
innermostErrorFunctionAction = {
|
||||
description,
|
||||
name: `function_scope_${i}`,
|
||||
notApplicableReason: getStringError(functionExtraction.errors),
|
||||
kind: extractFunctionAction.kind
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (!innermostErrorFunctionAction) {
|
||||
innermostErrorFunctionAction = {
|
||||
description,
|
||||
name: `function_scope_${i}`,
|
||||
notApplicableReason: getStringError(functionExtraction.errors)
|
||||
};
|
||||
}
|
||||
|
||||
// Skip these since we don't have a way to report errors yet
|
||||
if (constantExtraction.errors.length === 0) {
|
||||
// Don't issue refactorings with duplicated names.
|
||||
// Scopes come back in "innermost first" order, so extractions will
|
||||
// preferentially go into nearer scopes
|
||||
const description = constantExtraction.description;
|
||||
if (!usedConstantNames.has(description)) {
|
||||
usedConstantNames.set(description, true);
|
||||
constantActions.push({
|
||||
description,
|
||||
name: `constant_scope_${i}`
|
||||
});
|
||||
if(refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
|
||||
if (constantExtraction.errors.length === 0) {
|
||||
// Don't issue refactorings with duplicated names.
|
||||
// Scopes come back in "innermost first" order, so extractions will
|
||||
// preferentially go into nearer scopes
|
||||
const description = constantExtraction.description;
|
||||
if (!usedConstantNames.has(description)) {
|
||||
usedConstantNames.set(description, true);
|
||||
constantActions.push({
|
||||
description,
|
||||
name: `constant_scope_${i}`,
|
||||
kind: extractConstantAction.kind
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!innermostErrorConstantAction) {
|
||||
innermostErrorConstantAction = {
|
||||
description,
|
||||
name: `constant_scope_${i}`,
|
||||
notApplicableReason: getStringError(constantExtraction.errors),
|
||||
kind: extractConstantAction.kind
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (!innermostErrorConstantAction) {
|
||||
innermostErrorConstantAction = {
|
||||
description,
|
||||
name: `constant_scope_${i}`,
|
||||
notApplicableReason: getStringError(constantExtraction.errors)
|
||||
};
|
||||
}
|
||||
|
||||
// *do* increment i anyway because we'll look for the i-th scope
|
||||
@ -106,7 +131,7 @@ namespace ts.refactor.extractSymbol {
|
||||
infos.push({
|
||||
name: refactorName,
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_function),
|
||||
actions: functionActions
|
||||
actions: functionActions,
|
||||
});
|
||||
}
|
||||
else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) {
|
||||
|
||||
@ -1,25 +1,39 @@
|
||||
/* @internal */
|
||||
namespace ts.refactor {
|
||||
const refactorName = "Extract type";
|
||||
const extractToTypeAlias = "Extract to type alias";
|
||||
const extractToInterface = "Extract to interface";
|
||||
const extractToTypeDef = "Extract to typedef";
|
||||
|
||||
const extractToTypeAliasAction = {
|
||||
name: "Extract to type alias",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias),
|
||||
kind: "refactor.extract.type",
|
||||
};
|
||||
const extractToInterfaceAction = {
|
||||
name: "Extract to interface",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface),
|
||||
kind: "refactor.extract.interface",
|
||||
};
|
||||
const extractToTypeDefAction = {
|
||||
name: "Extract to typedef",
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef),
|
||||
kind: "refactor.extract.typedef"
|
||||
};
|
||||
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [
|
||||
extractToTypeAliasAction.kind,
|
||||
extractToInterfaceAction.kind,
|
||||
extractToTypeDefAction.kind
|
||||
],
|
||||
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
|
||||
const info = getRangeToExtract(context, context.triggerReason === "invoked");
|
||||
if (!info) return emptyArray;
|
||||
|
||||
if (info.error === undefined) {
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
|
||||
actions: info.info.isJS ? [{
|
||||
name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef)
|
||||
}] : append([{
|
||||
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
|
||||
}], info.info.typeElements && {
|
||||
name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface)
|
||||
})
|
||||
actions: info.isJS ?
|
||||
[extractToTypeDefAction] : append([extractToTypeAliasAction], info.typeElements && extractToInterfaceAction)
|
||||
}];
|
||||
}
|
||||
|
||||
@ -28,9 +42,9 @@ namespace ts.refactor {
|
||||
name: refactorName,
|
||||
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
|
||||
actions: [
|
||||
{ name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef), notApplicableReason: info.error },
|
||||
{ name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias), notApplicableReason: info.error },
|
||||
{ name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface), notApplicableReason: info.error },
|
||||
{ ...extractToTypeDefAction, notApplicableReason: info.error },
|
||||
{ ...extractToTypeAliasAction, notApplicableReason: info.error },
|
||||
{ ...extractToInterfaceAction, notApplicableReason: info.error },
|
||||
]
|
||||
}];
|
||||
}
|
||||
@ -39,18 +53,19 @@ namespace ts.refactor {
|
||||
},
|
||||
getEditsForAction(context, actionName): RefactorEditInfo {
|
||||
const { file, } = context;
|
||||
const info = Debug.checkDefined(getRangeToExtract(context)?.info, "Expected to find a range to extract");
|
||||
const info = getRangeToExtract(context);
|
||||
Debug.assert(info && !isRefactorErrorInfo(info), "Expected to find a range to extract");
|
||||
|
||||
const name = getUniqueName("NewType", file);
|
||||
const edits = textChanges.ChangeTracker.with(context, changes => {
|
||||
switch (actionName) {
|
||||
case extractToTypeAlias:
|
||||
case extractToTypeAliasAction.name:
|
||||
Debug.assert(!info.isJS, "Invalid actionName/JS combo");
|
||||
return doTypeAliasChange(changes, file, name, info);
|
||||
case extractToTypeDef:
|
||||
case extractToTypeDefAction.name:
|
||||
Debug.assert(info.isJS, "Invalid actionName/JS combo");
|
||||
return doTypedefChange(changes, file, name, info);
|
||||
case extractToInterface:
|
||||
case extractToInterfaceAction.name:
|
||||
Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo");
|
||||
return doInterfaceChange(changes, file, name, info as InterfaceInfo);
|
||||
default:
|
||||
@ -72,16 +87,9 @@ namespace ts.refactor {
|
||||
isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements: readonly TypeElement[];
|
||||
}
|
||||
|
||||
type Info = TypeAliasInfo | InterfaceInfo;
|
||||
type InfoOrError = {
|
||||
info: Info,
|
||||
error?: never
|
||||
} | {
|
||||
info?: never,
|
||||
error: string
|
||||
};
|
||||
type ExtractInfo = TypeAliasInfo | InterfaceInfo;
|
||||
|
||||
function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined {
|
||||
function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): ExtractInfo | RefactorErrorInfo | undefined {
|
||||
const { file, startPosition } = context;
|
||||
const isJS = isSourceFileJS(file);
|
||||
const current = getTokenAtPosition(file, startPosition);
|
||||
@ -98,7 +106,7 @@ namespace ts.refactor {
|
||||
if (!typeParameters) return { error: getLocaleSpecificMessage(Diagnostics.No_type_could_be_extracted_from_this_type_node) };
|
||||
|
||||
const typeElements = flattenTypeLiteralNodeReference(checker, selection);
|
||||
return { info: { isJS, selection, firstStatement, typeParameters, typeElements } };
|
||||
return { isJS, selection, firstStatement, typeParameters, typeElements };
|
||||
}
|
||||
|
||||
function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined {
|
||||
@ -209,7 +217,7 @@ namespace ts.refactor {
|
||||
changes.replaceNode(file, selection, factory.createTypeReferenceNode(name, typeParameters.map(id => factory.createTypeReferenceNode(id.name, /* typeArguments */ undefined))), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.ExcludeWhitespace });
|
||||
}
|
||||
|
||||
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: Info) {
|
||||
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: ExtractInfo) {
|
||||
const { firstStatement, selection, typeParameters } = info;
|
||||
|
||||
const node = factory.createJSDocTypedefTag(
|
||||
|
||||
@ -2,18 +2,25 @@
|
||||
namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
const actionName = "Generate 'get' and 'set' accessors";
|
||||
const actionDescription = Diagnostics.Generate_get_and_set_accessors.message;
|
||||
|
||||
const generateGetSetAction = {
|
||||
name: actionName,
|
||||
description: actionDescription,
|
||||
kind: "refactor.rewrite.property.generateAccessors",
|
||||
};
|
||||
registerRefactor(actionName, {
|
||||
kinds: [generateGetSetAction.kind],
|
||||
getEditsForAction(context, actionName) {
|
||||
if (!context.endPosition) return undefined;
|
||||
const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition);
|
||||
if (!info || !info.info) return undefined;
|
||||
Debug.assert(info && !isRefactorErrorInfo(info), "Expected applicable refactor info");
|
||||
const edits = codefix.generateAccessorFromProperty(context.file, context.program, context.startPosition, context.endPosition, context, actionName);
|
||||
if (!edits) return undefined;
|
||||
|
||||
const renameFilename = context.file.fileName;
|
||||
const nameNeedRename = info.info.renameAccessor ? info.info.accessorName : info.info.fieldName;
|
||||
const nameNeedRename = info.renameAccessor ? info.accessorName : info.fieldName;
|
||||
const renameLocationOffset = isIdentifier(nameNeedRename) ? 0 : -1;
|
||||
const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.info.declaration));
|
||||
const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.declaration));
|
||||
|
||||
return { renameFilename, renameLocation, edits };
|
||||
},
|
||||
@ -22,16 +29,11 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.program, context.startPosition, context.endPosition, context.triggerReason === "invoked");
|
||||
if (!info) return emptyArray;
|
||||
|
||||
if (!info.error) {
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
return [{
|
||||
name: actionName,
|
||||
description: actionDescription,
|
||||
actions: [
|
||||
{
|
||||
name: actionName,
|
||||
description: actionDescription
|
||||
}
|
||||
]
|
||||
actions: [generateGetSetAction],
|
||||
}];
|
||||
}
|
||||
|
||||
@ -39,11 +41,7 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
|
||||
return [{
|
||||
name: actionName,
|
||||
description: actionDescription,
|
||||
actions: [{
|
||||
name: actionName,
|
||||
description: actionDescription,
|
||||
notApplicableReason: info.error
|
||||
}]
|
||||
actions: [{ ...generateGetSetAction, notApplicableReason: info.error }],
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
25
src/services/refactors/helpers.ts
Normal file
25
src/services/refactors/helpers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/* @internal */
|
||||
namespace ts.refactor {
|
||||
/**
|
||||
* Returned by refactor funtions when some error message needs to be surfaced to users.
|
||||
*/
|
||||
export interface RefactorErrorInfo {
|
||||
error: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if some refactor info has refactor error info.
|
||||
*/
|
||||
export function isRefactorErrorInfo(info: unknown): info is RefactorErrorInfo {
|
||||
return (info as RefactorErrorInfo).error !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string "known" begins with string "requested".
|
||||
* Used to match requested kinds with a known kind.
|
||||
*/
|
||||
export function refactorKindBeginsWith(known: string, requested: string | undefined): boolean {
|
||||
if(!requested) return true;
|
||||
return known.substr(0, requested.length) === requested;
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,21 @@
|
||||
namespace ts.refactor.inferFunctionReturnType {
|
||||
const refactorName = "Infer function return type";
|
||||
const refactorDescription = Diagnostics.Infer_function_return_type.message;
|
||||
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
|
||||
|
||||
const inferReturnTypeAction = {
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
kind: "refactor.rewrite.function.returnType"
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [inferReturnTypeAction.kind],
|
||||
getEditsForAction,
|
||||
getAvailableActions
|
||||
});
|
||||
|
||||
function getEditsForAction(context: RefactorContext): RefactorEditInfo | undefined {
|
||||
const info = getInfo(context);
|
||||
if (info) {
|
||||
if (info && !isRefactorErrorInfo(info)) {
|
||||
const edits = textChanges.ChangeTracker.with(context, t =>
|
||||
t.tryInsertTypeAnnotation(context.file, info.declaration, info.returnTypeNode));
|
||||
return { renameFilename: undefined, renameLocation: undefined, edits };
|
||||
@ -16,14 +26,19 @@ namespace ts.refactor.inferFunctionReturnType {
|
||||
|
||||
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
|
||||
const info = getInfo(context);
|
||||
if (info) {
|
||||
if (!info) return emptyArray;
|
||||
if (!isRefactorErrorInfo(info)) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [{
|
||||
name: refactorName,
|
||||
description: refactorDescription
|
||||
}]
|
||||
actions: [inferReturnTypeAction]
|
||||
}];
|
||||
}
|
||||
if (context.preferences.provideRefactorNotApplicableReason) {
|
||||
return [{
|
||||
name: refactorName,
|
||||
description: refactorDescription,
|
||||
actions: [{ ...inferReturnTypeAction, notApplicableReason: info.error }]
|
||||
}];
|
||||
}
|
||||
return emptyArray;
|
||||
@ -35,21 +50,25 @@ namespace ts.refactor.inferFunctionReturnType {
|
||||
| ArrowFunction
|
||||
| MethodDeclaration;
|
||||
|
||||
interface Info {
|
||||
interface FunctionInfo {
|
||||
declaration: ConvertibleDeclaration;
|
||||
returnTypeNode: TypeNode;
|
||||
}
|
||||
|
||||
function getInfo(context: RefactorContext): Info | undefined {
|
||||
if (isInJSFile(context.file)) return;
|
||||
function getInfo(context: RefactorContext): FunctionInfo | RefactorErrorInfo | undefined {
|
||||
if (isInJSFile(context.file) || !refactorKindBeginsWith(inferReturnTypeAction.kind, context.kind)) return;
|
||||
|
||||
const token = getTokenAtPosition(context.file, context.startPosition);
|
||||
const declaration = findAncestor(token, isConvertibleDeclaration);
|
||||
if (!declaration || !declaration.body || declaration.type) return;
|
||||
if (!declaration || !declaration.body || declaration.type) {
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Return_type_must_be_inferred_from_a_function) };
|
||||
}
|
||||
|
||||
const typeChecker = context.program.getTypeChecker();
|
||||
const returnType = tryGetReturnType(typeChecker, declaration);
|
||||
if (!returnType) return;
|
||||
if (!returnType) {
|
||||
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_determine_function_return_type) };
|
||||
};
|
||||
|
||||
const returnTypeNode = typeChecker.typeToTypeNode(returnType, declaration, NodeBuilderFlags.NoTruncation);
|
||||
if (returnTypeNode) {
|
||||
|
||||
@ -1,11 +1,26 @@
|
||||
/* @internal */
|
||||
namespace ts.refactor {
|
||||
const refactorName = "Move to a new file";
|
||||
const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file);
|
||||
|
||||
const moveToNewFileAction = {
|
||||
name: refactorName,
|
||||
description,
|
||||
kind: "refactor.move.newFile",
|
||||
};
|
||||
registerRefactor(refactorName, {
|
||||
kinds: [moveToNewFileAction.kind],
|
||||
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
|
||||
if (!context.preferences.allowTextChangesInNewFiles || getStatementsToMove(context) === undefined) return emptyArray;
|
||||
const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file);
|
||||
return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }];
|
||||
const statements = getStatementsToMove(context);
|
||||
if (context.preferences.allowTextChangesInNewFiles && statements) {
|
||||
return [{ name: refactorName, description, actions: [moveToNewFileAction] }];
|
||||
}
|
||||
if (context.preferences.provideRefactorNotApplicableReason) {
|
||||
return [{ name: refactorName, description, actions:
|
||||
[{ ...moveToNewFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }]
|
||||
}];
|
||||
}
|
||||
return emptyArray;
|
||||
},
|
||||
getEditsForAction(context, actionName): RefactorEditInfo {
|
||||
Debug.assert(actionName === refactorName, "Wrong refactor invoked");
|
||||
|
||||
@ -2451,7 +2451,7 @@ namespace ts {
|
||||
return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options);
|
||||
}
|
||||
|
||||
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason): RefactorContext {
|
||||
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason, kind?: string): RefactorContext {
|
||||
const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end];
|
||||
return {
|
||||
file,
|
||||
@ -2463,6 +2463,7 @@ namespace ts {
|
||||
cancellationToken,
|
||||
preferences,
|
||||
triggerReason,
|
||||
kind
|
||||
};
|
||||
}
|
||||
|
||||
@ -2470,10 +2471,10 @@ namespace ts {
|
||||
return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName));
|
||||
}
|
||||
|
||||
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason): ApplicableRefactorInfo[] {
|
||||
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): ApplicableRefactorInfo[] {
|
||||
synchronizeHostData();
|
||||
const file = getValidSourceFile(fileName);
|
||||
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason));
|
||||
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind));
|
||||
}
|
||||
|
||||
function getEditsForRefactor(
|
||||
|
||||
@ -116,6 +116,7 @@
|
||||
"refactors/extractSymbol.ts",
|
||||
"refactors/extractType.ts",
|
||||
"refactors/generateGetAccessorAndSetAccessor.ts",
|
||||
"refactors/helpers.ts",
|
||||
"refactors/moveToNewFile.ts",
|
||||
"refactors/addOrRemoveBracesToArrowFunction.ts",
|
||||
"refactors/convertParamsToDestructuredObject.ts",
|
||||
|
||||
@ -520,7 +520,7 @@ namespace ts {
|
||||
/** @deprecated `fileName` will be ignored */
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
|
||||
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[];
|
||||
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
|
||||
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
|
||||
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
|
||||
@ -792,6 +792,11 @@ namespace ts {
|
||||
* the current context.
|
||||
*/
|
||||
notApplicableReason?: string;
|
||||
|
||||
/**
|
||||
* The hierarchical dotted name of the refactor action.
|
||||
*/
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1464,6 +1469,10 @@ namespace ts {
|
||||
|
||||
/** @internal */
|
||||
export interface Refactor {
|
||||
/** List of action kinds a refactor can provide.
|
||||
* Used to skip unnecessary calculation when specific refactors are requested. */
|
||||
kinds?: string[];
|
||||
|
||||
/** Compute the associated code actions */
|
||||
getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined;
|
||||
|
||||
@ -1480,5 +1489,6 @@ namespace ts {
|
||||
cancellationToken?: CancellationToken;
|
||||
preferences: UserPreferences;
|
||||
triggerReason?: RefactorTriggerReason;
|
||||
kind?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5570,7 +5570,7 @@ declare namespace ts {
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
|
||||
/** @deprecated `fileName` will be ignored */
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[];
|
||||
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
|
||||
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
|
||||
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
|
||||
@ -5796,6 +5796,10 @@ declare namespace ts {
|
||||
* the current context.
|
||||
*/
|
||||
notApplicableReason?: string;
|
||||
/**
|
||||
* The hierarchical dotted name of the refactor action.
|
||||
*/
|
||||
kind?: string;
|
||||
}
|
||||
/**
|
||||
* A set of edits to make in response to a refactor action, plus an optional
|
||||
@ -6926,6 +6930,7 @@ declare namespace ts.server.protocol {
|
||||
}
|
||||
type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs & {
|
||||
triggerReason?: RefactorTriggerReason;
|
||||
kind?: string;
|
||||
};
|
||||
type RefactorTriggerReason = "implicit" | "invoked";
|
||||
/**
|
||||
@ -6978,6 +6983,10 @@ declare namespace ts.server.protocol {
|
||||
* the current context.
|
||||
*/
|
||||
notApplicableReason?: string;
|
||||
/**
|
||||
* The hierarchical dotted name of the refactor action.
|
||||
*/
|
||||
kind?: string;
|
||||
}
|
||||
interface GetEditsForRefactorRequest extends Request {
|
||||
command: CommandTypes.GetEditsForRefactor;
|
||||
|
||||
@ -5570,7 +5570,7 @@ declare namespace ts {
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
|
||||
/** @deprecated `fileName` will be ignored */
|
||||
applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
||||
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason): ApplicableRefactorInfo[];
|
||||
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[];
|
||||
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
|
||||
organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
|
||||
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
|
||||
@ -5796,6 +5796,10 @@ declare namespace ts {
|
||||
* the current context.
|
||||
*/
|
||||
notApplicableReason?: string;
|
||||
/**
|
||||
* The hierarchical dotted name of the refactor action.
|
||||
*/
|
||||
kind?: string;
|
||||
}
|
||||
/**
|
||||
* A set of edits to make in response to a refactor action, plus an optional
|
||||
|
||||
@ -244,6 +244,7 @@ declare namespace FourSlashInterface {
|
||||
|
||||
refactorAvailable(name: string, actionName?: string): void;
|
||||
refactorAvailableForTriggerReason(triggerReason: RefactorTriggerReason, name: string, action?: string): void;
|
||||
refactorKindAvailable(refactorKind: string, expected: string[], preferences?: {}): void;
|
||||
}
|
||||
class verify extends verifyNegatable {
|
||||
assertHasRanges(ranges: Range[]): void;
|
||||
|
||||
16
tests/cases/fourslash/refactorKind_extract.ts
Normal file
16
tests/cases/fourslash/refactorKind_extract.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// const foo: /*a*/string/*b*/ = /*c*/1/*d*/;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.extract",
|
||||
[
|
||||
"refactor.extract.type"
|
||||
]);
|
||||
|
||||
goTo.select("c", "d");
|
||||
verify.refactorKindAvailable("refactor.extract",
|
||||
[
|
||||
"refactor.extract.constant",
|
||||
"refactor.extract.function"
|
||||
]);
|
||||
@ -0,0 +1,11 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// class A {
|
||||
//// /*a*/public a: string;/*b*/
|
||||
//// }
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite.property",
|
||||
[
|
||||
"refactor.rewrite.property.generateAccessors"
|
||||
]);
|
||||
12
tests/cases/fourslash/refactorKind_moveToNewFile.ts
Normal file
12
tests/cases/fourslash/refactorKind_moveToNewFile.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// /*a*/const moveMe = 1;/*b*/
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.move",
|
||||
[
|
||||
"refactor.move.newFile"
|
||||
],
|
||||
{
|
||||
allowTextChangesInNewFiles: true
|
||||
});
|
||||
9
tests/cases/fourslash/refactorKind_rewriteExport.ts
Normal file
9
tests/cases/fourslash/refactorKind_rewriteExport.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// /*a*/export function f() {}/*b*/
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite.export",
|
||||
[
|
||||
"refactor.rewrite.export.default"
|
||||
]);
|
||||
12
tests/cases/fourslash/refactorKind_rewriteFunction.ts
Normal file
12
tests/cases/fourslash/refactorKind_rewriteFunction.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// const arrow = () /*a*/=>/*b*/ 1;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite",
|
||||
[
|
||||
"refactor.rewrite.arrow.braces.add",
|
||||
"refactor.rewrite.function.named",
|
||||
"refactor.rewrite.function.anonymous",
|
||||
"refactor.rewrite.function.returnType"
|
||||
]);
|
||||
@ -0,0 +1,10 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// /*a*/declare function foo(): void;
|
||||
//// declare function foo(a: string): void;/*b*/
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite",
|
||||
[
|
||||
"refactor.rewrite.function.overloadList"
|
||||
]);
|
||||
9
tests/cases/fourslash/refactorKind_rewriteImport.ts
Normal file
9
tests/cases/fourslash/refactorKind_rewriteImport.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// /*a*/import * as m from "m";/*b*/
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite",
|
||||
[
|
||||
"refactor.rewrite.import.named"
|
||||
]);
|
||||
10
tests/cases/fourslash/refactorKind_rewriteOptionalChain.ts
Normal file
10
tests/cases/fourslash/refactorKind_rewriteOptionalChain.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// /*a*/foo && foo.bar/*b*/
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite",
|
||||
[
|
||||
"refactor.rewrite.expression.optionalChain"
|
||||
]);
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// function(/*a*/a: number, b: number/*b*/): number {}
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite",
|
||||
[
|
||||
"refactor.rewrite.parameters.toDestructured"
|
||||
]);
|
||||
9
tests/cases/fourslash/refactorKind_rewriteString.ts
Normal file
9
tests/cases/fourslash/refactorKind_rewriteString.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
//// const foo = /*a*/"a" + bar/*b*/;
|
||||
|
||||
goTo.select("a", "b");
|
||||
verify.refactorKindAvailable("refactor.rewrite",
|
||||
[
|
||||
"refactor.rewrite.string"
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user