From b2c6a56e38bc8cca1573aeadf8c2bda74a67e8e9 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 5 Jan 2024 17:52:26 +0000 Subject: [PATCH] Fix "Extract to type alias" not available at end of span (#56467) Signed-off-by: Babak K. Shandiz --- src/services/refactors/extractType.ts | 28 ++++++--- .../cases/fourslash/refactorExtractType89.ts | 61 +++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 tests/cases/fourslash/refactorExtractType89.ts diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index f8ddcaedbf0..e9d43a1a3c2 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -23,6 +23,7 @@ import { getRefactorContextSpan, getRenameLocation, getTokenAtPosition, + getTouchingToken, getUniqueName, ignoreSourceNewlines, isArray, @@ -173,14 +174,9 @@ type ExtractInfo = TypeAliasInfo | InterfaceInfo; function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): ExtractInfo | RefactorErrorInfo | undefined { const { file, startPosition } = context; const isJS = isSourceFileJS(file); - const current = getTokenAtPosition(file, startPosition); const range = createTextRangeFromSpan(getRefactorContextSpan(context)); - const cursorRequest = range.pos === range.end && considerEmptySpans; - const overlappingRange = nodeOverlapsWithStartEnd(current, file, range.pos, range.end); - - const firstType = findAncestor(current, node => - node.parent && isTypeNode(node) && !rangeContainsSkipTrivia(range, node.parent, file) && - (cursorRequest || overlappingRange)); + const isCursorRequest = range.pos === range.end && considerEmptySpans; + const firstType = getFirstTypeAt(file, startPosition, range, isCursorRequest); if (!firstType || !isTypeNode(firstType)) return { error: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_type_node) }; const checker = context.program.getTypeChecker(); @@ -209,6 +205,24 @@ function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): return { isJS, selection, enclosingNode, typeParameters, typeElements }; } +function getFirstTypeAt(file: SourceFile, startPosition: number, range: TextRange, isCursorRequest: boolean): Node | undefined { + const currentNodes = [ + () => getTokenAtPosition(file, startPosition), + () => getTouchingToken(file, startPosition, () => true), + ]; + for (const f of currentNodes) { + const current = f(); + const overlappingRange = nodeOverlapsWithStartEnd(current, file, range.pos, range.end); + const firstType = findAncestor(current, node => + node.parent && isTypeNode(node) && !rangeContainsSkipTrivia(range, node.parent, file) && + (isCursorRequest || overlappingRange)); + if (firstType) { + return firstType; + } + } + return undefined; +} + function flattenTypeLiteralNodeReference(checker: TypeChecker, selection: TypeNode | TypeNode[] | undefined): readonly TypeElement[] | undefined { if (!selection) return undefined; if (isArray(selection)) { diff --git a/tests/cases/fourslash/refactorExtractType89.ts b/tests/cases/fourslash/refactorExtractType89.ts new file mode 100644 index 00000000000..bb6026cc7a6 --- /dev/null +++ b/tests/cases/fourslash/refactorExtractType89.ts @@ -0,0 +1,61 @@ +/// + +//// interface Yadda { x: T } +//// +//// export let blah: Yadda/*a*//*b*/; +//// +//// interface YaddaWithDefault { x: T/*d*/ } + +goTo.marker("a"); +verify.refactorAvailableForTriggerReason("invoked", "Extract type", "Extract to type alias") + +goTo.marker("b"); +edit.applyRefactor({ + triggerReason: "invoked", + refactorName: "Extract type", + actionName: "Extract to type alias", + actionDescription: "Extract to type alias", + newContent: `interface Yadda { x: T } + +type /*RENAME*/NewType = Yadda; + +export let blah: NewType; + +interface YaddaWithDefault { x: T }`, +}); + +goTo.marker("c"); +edit.applyRefactor({ + triggerReason: "invoked", + refactorName: "Extract type", + actionName: "Extract to type alias", + actionDescription: "Extract to type alias", + newContent: `interface Yadda { x: T } + +type NewType = Yadda; + +export let blah: NewType; + +type /*RENAME*/NewType_1 = boolean; + +interface YaddaWithDefault { x: T }` +}); + +goTo.marker("d"); +edit.applyRefactor({ + triggerReason: "invoked", + refactorName: "Extract type", + actionName: "Extract to type alias", + actionDescription: "Extract to type alias", + newContent: `interface Yadda { x: T } + +type NewType = Yadda; + +export let blah: NewType; + +type NewType_1 = boolean; + +type /*RENAME*/NewType_2 = T; + +interface YaddaWithDefault { x: NewType_2 }` +});