Skip to content

Commit d640313

Browse files
author
Jesse Trinity
authored
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
1 parent 5cdc870 commit d640313

File tree

4 files changed

+113
-68
lines changed

4 files changed

+113
-68
lines changed

src/services/refactors/extractSymbol.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ namespace ts.refactor.extractSymbol {
7070
let i = 0;
7171
for (const { functionExtraction, constantExtraction } of extractions) {
7272
const description = functionExtraction.description;
73-
if(refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)){
73+
if (refactorKindBeginsWith(extractFunctionAction.kind, requestedRefactor)) {
7474
if (functionExtraction.errors.length === 0) {
7575
// Don't issue refactorings with duplicated names.
7676
// Scopes come back in "innermost first" order, so extractions will
@@ -94,8 +94,7 @@ namespace ts.refactor.extractSymbol {
9494
}
9595
}
9696

97-
// Skip these since we don't have a way to report errors yet
98-
if(refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
97+
if (refactorKindBeginsWith(extractConstantAction.kind, requestedRefactor)) {
9998
if (constantExtraction.errors.length === 0) {
10099
// Don't issue refactorings with duplicated names.
101100
// Scopes come back in "innermost first" order, so extractions will
@@ -265,24 +264,30 @@ namespace ts.refactor.extractSymbol {
265264
/**
266265
* getRangeToExtract takes a span inside a text file and returns either an expression or an array
267266
* of statements representing the minimum set of nodes needed to extract the entire span. This
268-
* process may fail, in which case a set of errors is returned instead (these are currently
269-
* not shown to the user, but can be used by us diagnostically)
267+
* process may fail, in which case a set of errors is returned instead. These errors are shown to
268+
* users if they have the provideRefactorNotApplicableReason option set.
270269
*/
271270
// exported only for tests
272-
export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan, considerEmptySpans = true): RangeToExtract {
271+
export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan, invoked = true): RangeToExtract {
273272
const { length } = span;
274-
if (length === 0 && !considerEmptySpans) {
273+
if (length === 0 && !invoked) {
275274
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] };
276275
}
277-
const cursorRequest = length === 0 && considerEmptySpans;
276+
const cursorRequest = length === 0 && invoked;
277+
278+
const startToken = getTokenAtPosition(sourceFile, span.start);
279+
const endToken = findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span));
280+
/* If the refactoring command is invoked through a keyboard action it's safe to assume that the user is actively looking for
281+
refactoring actions at the span location. As they may not know the exact range that will trigger a refactoring, we expand the
282+
searched span to cover a real node range making it more likely that something useful will show up. */
283+
const adjustedSpan = startToken && endToken && invoked ? getAdjustedSpanFromNodes(startToken, endToken, sourceFile) : span;
278284

279285
// Walk up starting from the the start position until we find a non-SourceFile node that subsumes the selected span.
280286
// This may fail (e.g. you select two statements in the root of a source file)
281-
const startToken = getTokenAtPosition(sourceFile, span.start);
282-
const start = cursorRequest ? getExtractableParent(startToken): getParentNodeInSpan(startToken, sourceFile, span);
287+
const start = cursorRequest ? getExtractableParent(startToken): getParentNodeInSpan(startToken, sourceFile, adjustedSpan);
288+
283289
// Do the same for the ending position
284-
const endToken = findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span));
285-
const end = cursorRequest ? start : getParentNodeInSpan(endToken, sourceFile, span);
290+
const end = cursorRequest ? start : getParentNodeInSpan(endToken, sourceFile, adjustedSpan);
286291

287292
const declarations: Symbol[] = [];
288293

@@ -295,6 +300,10 @@ namespace ts.refactor.extractSymbol {
295300
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
296301
}
297302

303+
if (isJSDoc(start)) {
304+
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] };
305+
}
306+
298307
if (start.parent !== end.parent) {
299308
// start and end nodes belong to different subtrees
300309
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
@@ -332,10 +341,6 @@ namespace ts.refactor.extractSymbol {
332341
return { targetRange: { range: statements, facts: rangeFacts, declarations } };
333342
}
334343

335-
if (isJSDoc(start)) {
336-
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] };
337-
}
338-
339344
if (isReturnStatement(start) && !start.expression) {
340345
// Makes no sense to extract an expression-less return statement.
341346
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
@@ -605,6 +610,19 @@ namespace ts.refactor.extractSymbol {
605610
}
606611
}
607612

613+
/**
614+
* Includes the final semicolon so that the span covers statements in cases where it would otherwise
615+
* only cover the declaration list.
616+
*/
617+
function getAdjustedSpanFromNodes(startNode: Node, endNode: Node, sourceFile: SourceFile): TextSpan {
618+
const start = startNode.getStart(sourceFile);
619+
let end = endNode.getEnd();
620+
if (sourceFile.text.charCodeAt(end) === CharacterCodes.semicolon) {
621+
end++;
622+
}
623+
return { start, length: end - start };
624+
}
625+
608626
function getStatementOrExpressionRange(node: Node): Statement[] | Expression | undefined {
609627
if (isStatement(node)) {
610628
return [node];

src/testRunner/unittests/services/extract/ranges.ts

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,85 +14,100 @@ namespace ts {
1414
});
1515
}
1616

17-
function testExtractRange(s: string): void {
18-
const t = extractTest(s);
19-
const f = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true);
20-
const selectionRange = t.ranges.get("selection");
21-
if (!selectionRange) {
22-
throw new Error(`Test ${s} does not specify selection range`);
23-
}
24-
const result = refactor.extractSymbol.getRangeToExtract(f, createTextSpanFromRange(selectionRange));
25-
const expectedRange = t.ranges.get("extracted");
26-
if (expectedRange) {
27-
let pos: number, end: number;
28-
const targetRange = result.targetRange!;
29-
if (isArray(targetRange.range)) {
30-
pos = targetRange.range[0].getStart(f);
31-
end = last(targetRange.range).getEnd();
17+
function testExtractRange(caption: string, s: string) {
18+
return it(caption, () => {
19+
const t = extractTest(s);
20+
const f = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true);
21+
const selectionRange = t.ranges.get("selection");
22+
if (!selectionRange) {
23+
throw new Error(`Test ${s} does not specify selection range`);
24+
}
25+
const result = refactor.extractSymbol.getRangeToExtract(f, createTextSpanFromRange(selectionRange));
26+
const expectedRange = t.ranges.get("extracted");
27+
if (expectedRange) {
28+
let pos: number, end: number;
29+
const targetRange = result.targetRange!;
30+
if (isArray(targetRange.range)) {
31+
pos = targetRange.range[0].getStart(f);
32+
end = last(targetRange.range).getEnd();
33+
}
34+
else {
35+
pos = targetRange.range.getStart(f);
36+
end = targetRange.range.getEnd();
37+
}
38+
assert.equal(pos, expectedRange.pos, "incorrect pos of range");
39+
assert.equal(end, expectedRange.end, "incorrect end of range");
3240
}
3341
else {
34-
pos = targetRange.range.getStart(f);
35-
end = targetRange.range.getEnd();
42+
assert.isTrue(!result.targetRange, `expected range to extract to be undefined`);
3643
}
37-
assert.equal(pos, expectedRange.pos, "incorrect pos of range");
38-
assert.equal(end, expectedRange.end, "incorrect end of range");
39-
}
40-
else {
41-
assert.isTrue(!result.targetRange, `expected range to extract to be undefined`);
42-
}
44+
});
4345
}
4446

4547
describe("unittests:: services:: extract:: extractRanges", () => {
46-
it("get extract range from selection", () => {
47-
testExtractRange(`
48+
describe("get extract range from selection", () => {
49+
testExtractRange("extractRange1", `
4850
[#|
4951
[$|var x = 1;
5052
var y = 2;|]|]
5153
`);
52-
testExtractRange(`
53-
[#|
54-
var x = 1;
55-
var y = 2|];
54+
testExtractRange("extractRange2", `
55+
[$|[#|var x = 1;
56+
var y = 2|];|]
5657
`);
57-
testExtractRange(`
58-
[#|var x = 1|];
58+
testExtractRange("extractRange3", `
59+
[#|var x = [$|1|]|];
5960
var y = 2;
6061
`);
61-
testExtractRange(`
62+
testExtractRange("extractRange4", `
63+
var x = [$|10[#|00|]|];
64+
`);
65+
testExtractRange("extractRange5", `
66+
[$|va[#|r foo = 1;
67+
var y = 200|]0;|]
68+
`);
69+
testExtractRange("extractRange6", `
70+
var x = [$|fo[#|o.bar.baz()|]|];
71+
`);
72+
testExtractRange("extractRange7", `
6273
if ([#|[#extracted|a && b && c && d|]|]) {
6374
}
6475
`);
65-
testExtractRange(`
76+
testExtractRange("extractRange8", `
6677
if [#|(a && b && c && d|]) {
6778
}
6879
`);
69-
testExtractRange(`
80+
testExtractRange("extractRange9", `
81+
if ([$|a[#|a && b && c && d|]d|]) {
82+
}
83+
`);
84+
testExtractRange("extractRange10", `
7085
if (a && b && c && d) {
7186
[#| [$|var x = 1;
7287
console.log(x);|] |]
7388
}
7489
`);
75-
testExtractRange(`
90+
testExtractRange("extractRange11", `
7691
[#|
7792
if (a) {
7893
return 100;
7994
} |]
8095
`);
81-
testExtractRange(`
96+
testExtractRange("extractRange12", `
8297
function foo() {
8398
[#| [$|if (a) {
8499
}
85100
return 100|] |]
86101
}
87102
`);
88-
testExtractRange(`
103+
testExtractRange("extractRange13", `
89104
[#|
90105
[$|l1:
91106
if (x) {
92107
break l1;
93108
}|]|]
94109
`);
95-
testExtractRange(`
110+
testExtractRange("extractRange14", `
96111
[#|
97112
[$|l2:
98113
{
@@ -101,21 +116,21 @@ namespace ts {
101116
break l2;
102117
}|]|]
103118
`);
104-
testExtractRange(`
119+
testExtractRange("extractRange15", `
105120
while (true) {
106121
[#| if(x) {
107122
}
108123
break; |]
109124
}
110125
`);
111-
testExtractRange(`
126+
testExtractRange("extractRange16", `
112127
while (true) {
113128
[#| if(x) {
114129
}
115130
continue; |]
116131
}
117132
`);
118-
testExtractRange(`
133+
testExtractRange("extractRange17", `
119134
l3:
120135
{
121136
[#|
@@ -124,7 +139,7 @@ namespace ts {
124139
break l3; |]
125140
}
126141
`);
127-
testExtractRange(`
142+
testExtractRange("extractRange18", `
128143
function f() {
129144
while (true) {
130145
[#|
@@ -134,7 +149,7 @@ namespace ts {
134149
}
135150
}
136151
`);
137-
testExtractRange(`
152+
testExtractRange("extractRange19", `
138153
function f() {
139154
while (true) {
140155
[#|
@@ -145,13 +160,13 @@ namespace ts {
145160
}
146161
}
147162
`);
148-
testExtractRange(`
163+
testExtractRange("extractRange20", `
149164
function f() {
150165
return [#| [$|1 + 2|] |]+ 3;
151166
}
152167
}
153168
`);
154-
testExtractRange(`
169+
testExtractRange("extractRange21", `
155170
function f(x: number) {
156171
[#|[$|try {
157172
x++;
@@ -163,17 +178,21 @@ namespace ts {
163178
`);
164179

165180
// Variable statements
166-
testExtractRange(`[#|let x = [$|1|];|]`);
167-
testExtractRange(`[#|let x = [$|1|], y;|]`);
168-
testExtractRange(`[#|[$|let x = 1, y = 1;|]|]`);
181+
testExtractRange("extractRange22", `[#|let x = [$|1|];|]`);
182+
testExtractRange("extractRange23", `[#|let x = [$|1|], y;|]`);
183+
testExtractRange("extractRange24", `[#|[$|let x = 1, y = 1;|]|]`);
169184

170185
// Variable declarations
171-
testExtractRange(`let [#|x = [$|1|]|];`);
172-
testExtractRange(`let [#|x = [$|1|]|], y = 2;`);
173-
testExtractRange(`let x = 1, [#|y = [$|2|]|];`);
186+
testExtractRange("extractRange25", `let [#|x = [$|1|]|];`);
187+
testExtractRange("extractRange26", `let [#|x = [$|1|]|], y = 2;`);
188+
testExtractRange("extractRange27", `let x = 1, [#|y = [$|2|]|];`);
174189

175190
// Return statements
176-
testExtractRange(`[#|return [$|1|];|]`);
191+
testExtractRange("extractRange28", `[#|return [$|1|];|]`);
192+
193+
// For statements
194+
testExtractRange("extractRange29", `for ([#|var i = 1|]; i < 2; i++) {}`);
195+
testExtractRange("extractRange30", `for (var i = [#|[$|1|]|]; i < 2; i++) {}`);
177196
});
178197

179198
testExtractRangeFailed("extractRangeFailed1",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = ba/*a*/r + b/*b*/az;
4+
5+
// Expand selection to fit nodes if refactors are explicitly requested
6+
goTo.select("a", "b");
7+
verify.not.refactorAvailableForTriggerReason("implicit", "Extract Symbol");
8+
verify.refactorAvailableForTriggerReason("invoked", "Extract Symbol", "constant_scope_0");

0 commit comments

Comments
 (0)