Expand extractSymbol ranges (#42770)

* allow partial selections of node ranges

* separate tests for better failure investigation

* gate span expansion behind invoked command

* add invoked test

* comment wording

* for test
This commit is contained in:
Jesse Trinity
2021-02-17 12:55:12 -08:00
committed by GitHub
parent 5cdc87032b
commit d640313ff2
4 changed files with 113 additions and 68 deletions

View File

@@ -70,7 +70,7 @@ namespace ts.refactor.extractSymbol {
let i = 0;
for (const { functionExtraction, constantExtraction } of extractions) {
const description = functionExtraction.description;
if(refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)){
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
@@ -94,8 +94,7 @@ namespace ts.refactor.extractSymbol {
}
}
// Skip these since we don't have a way to report errors yet
if(refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
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
@@ -265,24 +264,30 @@ namespace ts.refactor.extractSymbol {
/**
* getRangeToExtract takes a span inside a text file and returns either an expression or an array
* of statements representing the minimum set of nodes needed to extract the entire span. This
* process may fail, in which case a set of errors is returned instead (these are currently
* not shown to the user, but can be used by us diagnostically)
* process may fail, in which case a set of errors is returned instead. These errors are shown to
* users if they have the provideRefactorNotApplicableReason option set.
*/
// exported only for tests
export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan, considerEmptySpans = true): RangeToExtract {
export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan, invoked = true): RangeToExtract {
const { length } = span;
if (length === 0 && !considerEmptySpans) {
if (length === 0 && !invoked) {
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] };
}
const cursorRequest = length === 0 && considerEmptySpans;
const cursorRequest = length === 0 && invoked;
const startToken = getTokenAtPosition(sourceFile, span.start);
const endToken = findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span));
/* If the refactoring command is invoked through a keyboard action it's safe to assume that the user is actively looking for
refactoring actions at the span location. As they may not know the exact range that will trigger a refactoring, we expand the
searched span to cover a real node range making it more likely that something useful will show up. */
const adjustedSpan = startToken && endToken && invoked ? getAdjustedSpanFromNodes(startToken, endToken, sourceFile) : span;
// Walk up starting from the the start position until we find a non-SourceFile node that subsumes the selected span.
// This may fail (e.g. you select two statements in the root of a source file)
const startToken = getTokenAtPosition(sourceFile, span.start);
const start = cursorRequest ? getExtractableParent(startToken): getParentNodeInSpan(startToken, sourceFile, span);
const start = cursorRequest ? getExtractableParent(startToken): getParentNodeInSpan(startToken, sourceFile, adjustedSpan);
// Do the same for the ending position
const endToken = findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span));
const end = cursorRequest ? start : getParentNodeInSpan(endToken, sourceFile, span);
const end = cursorRequest ? start : getParentNodeInSpan(endToken, sourceFile, adjustedSpan);
const declarations: Symbol[] = [];
@@ -295,6 +300,10 @@ namespace ts.refactor.extractSymbol {
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
}
if (isJSDoc(start)) {
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] };
}
if (start.parent !== end.parent) {
// start and end nodes belong to different subtrees
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
@@ -332,10 +341,6 @@ namespace ts.refactor.extractSymbol {
return { targetRange: { range: statements, facts: rangeFacts, declarations } };
}
if (isJSDoc(start)) {
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] };
}
if (isReturnStatement(start) && !start.expression) {
// Makes no sense to extract an expression-less return statement.
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
@@ -605,6 +610,19 @@ namespace ts.refactor.extractSymbol {
}
}
/**
* Includes the final semicolon so that the span covers statements in cases where it would otherwise
* only cover the declaration list.
*/
function getAdjustedSpanFromNodes(startNode: Node, endNode: Node, sourceFile: SourceFile): TextSpan {
const start = startNode.getStart(sourceFile);
let end = endNode.getEnd();
if (sourceFile.text.charCodeAt(end) === CharacterCodes.semicolon) {
end++;
}
return { start, length: end - start };
}
function getStatementOrExpressionRange(node: Node): Statement[] | Expression | undefined {
if (isStatement(node)) {
return [node];