Skip to content

Commit 7c5a0ec

Browse files
author
Andy
authored
Clean up outliningElementsCollector (#20143)
* Clean up outliningElementsCollector * Use depthRemaining instead of maxDepth
1 parent 761c739 commit 7c5a0ec

File tree

4 files changed

+149
-210
lines changed

4 files changed

+149
-210
lines changed
Lines changed: 134 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -1,227 +1,166 @@
11
/* @internal */
22
namespace ts.OutliningElementsCollector {
3-
const collapseText = "...";
4-
const maxDepth = 40;
5-
const defaultLabel = "#region";
6-
const regionMatch = new RegExp("^\\s*//\\s*#(end)?region(?:\\s+(.*))?$");
7-
83
export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] {
9-
const elements: OutliningSpan[] = [];
10-
let depth = 0;
11-
const regions: OutliningSpan[] = [];
4+
const res: OutliningSpan[] = [];
5+
addNodeOutliningSpans(sourceFile, cancellationToken, res);
6+
addRegionOutliningSpans(sourceFile, res);
7+
return res.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start);
8+
}
129

13-
walk(sourceFile);
14-
gatherRegions();
15-
return elements.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start);
10+
function addNodeOutliningSpans(sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push<OutliningSpan>): void {
11+
let depthRemaining = 40;
12+
sourceFile.forEachChild(function walk(n) {
13+
if (depthRemaining === 0) return;
14+
cancellationToken.throwIfCancellationRequested();
1615

17-
/** If useFullStart is true, then the collapsing span includes leading whitespace, including linebreaks. */
18-
function addOutliningSpan(hintSpanNode: Node, startElement: Node, endElement: Node, autoCollapse: boolean, useFullStart: boolean) {
19-
if (hintSpanNode && startElement && endElement) {
20-
const span: OutliningSpan = {
21-
textSpan: createTextSpanFromBounds(useFullStart ? startElement.getFullStart() : startElement.getStart(), endElement.getEnd()),
22-
hintSpan: createTextSpanFromNode(hintSpanNode, sourceFile),
23-
bannerText: collapseText,
24-
autoCollapse,
25-
};
26-
elements.push(span);
16+
if (isDeclaration(n)) {
17+
addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out);
2718
}
28-
}
2919

30-
function addOutliningSpanComments(commentSpan: CommentRange, autoCollapse: boolean) {
31-
if (commentSpan) {
32-
const span: OutliningSpan = {
33-
textSpan: createTextSpanFromBounds(commentSpan.pos, commentSpan.end),
34-
hintSpan: createTextSpanFromBounds(commentSpan.pos, commentSpan.end),
35-
bannerText: collapseText,
36-
autoCollapse,
37-
};
38-
elements.push(span);
39-
}
40-
}
20+
const span = getOutliningSpanForNode(n, sourceFile);
21+
if (span) out.push(span);
4122

42-
function addOutliningForLeadingCommentsForNode(n: Node) {
43-
const comments = ts.getLeadingCommentRangesOfNode(n, sourceFile);
23+
depthRemaining--;
24+
n.forEachChild(walk);
25+
depthRemaining++;
26+
});
27+
}
4428

45-
if (comments) {
46-
let firstSingleLineCommentStart = -1;
47-
let lastSingleLineCommentEnd = -1;
48-
let isFirstSingleLineComment = true;
49-
let singleLineCommentCount = 0;
29+
function addRegionOutliningSpans(sourceFile: SourceFile, out: Push<OutliningSpan>): void {
30+
const regions: OutliningSpan[] = [];
31+
const lineStarts = sourceFile.getLineStarts();
32+
for (let i = 0; i < lineStarts.length; i++) {
33+
const currentLineStart = lineStarts[i];
34+
const lineEnd = i + 1 === lineStarts.length ? sourceFile.getEnd() : lineStarts[i + 1] - 1;
35+
const lineText = sourceFile.text.substring(currentLineStart, lineEnd);
36+
const result = lineText.match(/^\s*\/\/\s*#(end)?region(?:\s+(.*))?$/);
37+
if (!result || isInComment(sourceFile, currentLineStart)) {
38+
continue;
39+
}
5040

51-
for (const currentComment of comments) {
52-
cancellationToken.throwIfCancellationRequested();
41+
if (!result[1]) {
42+
const span = createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd);
43+
regions.push(createOutliningSpan(span, span, /*autoCollapse*/ false, result[2] || "#region"));
44+
}
45+
else {
46+
const region = regions.pop();
47+
if (region) {
48+
region.textSpan.length = lineEnd - region.textSpan.start;
49+
region.hintSpan.length = lineEnd - region.textSpan.start;
50+
out.push(region);
51+
}
52+
}
53+
}
54+
}
5355

56+
function addOutliningForLeadingCommentsForNode(n: Node, sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push<OutliningSpan>): void {
57+
const comments = getLeadingCommentRangesOfNode(n, sourceFile);
58+
if (!comments) return;
59+
let firstSingleLineCommentStart = -1;
60+
let lastSingleLineCommentEnd = -1;
61+
let singleLineCommentCount = 0;
62+
for (const { kind, pos, end } of comments) {
63+
cancellationToken.throwIfCancellationRequested();
64+
switch (kind) {
65+
case SyntaxKind.SingleLineCommentTrivia:
5466
// For single line comments, combine consecutive ones (2 or more) into
5567
// a single span from the start of the first till the end of the last
56-
if (currentComment.kind === SyntaxKind.SingleLineCommentTrivia) {
57-
if (isFirstSingleLineComment) {
58-
firstSingleLineCommentStart = currentComment.pos;
59-
}
60-
isFirstSingleLineComment = false;
61-
lastSingleLineCommentEnd = currentComment.end;
62-
singleLineCommentCount++;
63-
}
64-
else if (currentComment.kind === SyntaxKind.MultiLineCommentTrivia) {
65-
combineAndAddMultipleSingleLineComments(singleLineCommentCount, firstSingleLineCommentStart, lastSingleLineCommentEnd);
66-
addOutliningSpanComments(currentComment, /*autoCollapse*/ false);
67-
68-
singleLineCommentCount = 0;
69-
lastSingleLineCommentEnd = -1;
70-
isFirstSingleLineComment = true;
68+
if (singleLineCommentCount === 0) {
69+
firstSingleLineCommentStart = pos;
7170
}
72-
}
73-
74-
combineAndAddMultipleSingleLineComments(singleLineCommentCount, firstSingleLineCommentStart, lastSingleLineCommentEnd);
71+
lastSingleLineCommentEnd = end;
72+
singleLineCommentCount++;
73+
break;
74+
case SyntaxKind.MultiLineCommentTrivia:
75+
combineAndAddMultipleSingleLineComments();
76+
out.push(createOutliningSpanFromBounds(pos, end));
77+
singleLineCommentCount = 0;
78+
break;
79+
default:
80+
Debug.assertNever(kind);
7581
}
7682
}
83+
combineAndAddMultipleSingleLineComments();
7784

78-
function combineAndAddMultipleSingleLineComments(count: number, start: number, end: number) {
79-
85+
function combineAndAddMultipleSingleLineComments(): void {
8086
// Only outline spans of two or more consecutive single line comments
81-
if (count > 1) {
82-
const multipleSingleLineComments: CommentRange = {
83-
kind: SyntaxKind.SingleLineCommentTrivia,
84-
pos: start,
85-
end,
86-
};
87-
88-
addOutliningSpanComments(multipleSingleLineComments, /*autoCollapse*/ false);
87+
if (singleLineCommentCount > 1) {
88+
out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd));
8989
}
9090
}
91+
}
9192

92-
function autoCollapse(node: Node) {
93-
return isFunctionBlock(node) && node.parent.kind !== SyntaxKind.ArrowFunction;
94-
}
95-
96-
function gatherRegions(): void {
97-
const lineStarts = sourceFile.getLineStarts();
98-
99-
for (let i = 0; i < lineStarts.length; i++) {
100-
const currentLineStart = lineStarts[i];
101-
const lineEnd = lineStarts[i + 1] - 1 || sourceFile.getEnd();
102-
const comment = sourceFile.text.substring(currentLineStart, lineEnd);
103-
const result = comment.match(regionMatch);
93+
function createOutliningSpanFromBounds(pos: number, end: number): OutliningSpan {
94+
return createOutliningSpan(createTextSpanFromBounds(pos, end));
95+
}
10496

105-
if (result && !isInComment(sourceFile, currentLineStart)) {
106-
if (!result[1]) {
107-
const start = sourceFile.getFullText().indexOf("//", currentLineStart);
108-
const textSpan = createTextSpanFromBounds(start, lineEnd);
109-
const region: OutliningSpan = {
110-
textSpan,
111-
hintSpan: textSpan,
112-
bannerText: result[2] || defaultLabel,
113-
autoCollapse: false
114-
};
115-
regions.push(region);
116-
}
117-
else {
118-
const region = regions.pop();
119-
if (region) {
120-
region.textSpan.length = lineEnd - region.textSpan.start;
121-
region.hintSpan.length = lineEnd - region.textSpan.start;
122-
elements.push(region);
123-
}
124-
}
97+
function getOutliningSpanForNode(n: Node, sourceFile: SourceFile): OutliningSpan | undefined {
98+
switch (n.kind) {
99+
case SyntaxKind.Block:
100+
if (isFunctionBlock(n)) {
101+
return spanForNode(n.parent, /*autoCollapse*/ n.parent.kind !== SyntaxKind.ArrowFunction);
125102
}
126-
}
127-
}
128-
129-
function walk(n: Node): void {
130-
cancellationToken.throwIfCancellationRequested();
131-
if (depth > maxDepth) {
132-
return;
133-
}
134-
135-
if (isDeclaration(n)) {
136-
addOutliningForLeadingCommentsForNode(n);
137-
}
138-
139-
switch (n.kind) {
140-
case SyntaxKind.Block:
141-
if (!isFunctionBlock(n)) {
142-
const parent = n.parent;
143-
const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile);
144-
const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile);
145-
146-
// Check if the block is standalone, or 'attached' to some parent statement.
147-
// If the latter, we want to collapse the block, but consider its hint span
148-
// to be the entire span of the parent.
149-
if (parent.kind === SyntaxKind.DoStatement ||
150-
parent.kind === SyntaxKind.ForInStatement ||
151-
parent.kind === SyntaxKind.ForOfStatement ||
152-
parent.kind === SyntaxKind.ForStatement ||
153-
parent.kind === SyntaxKind.IfStatement ||
154-
parent.kind === SyntaxKind.WhileStatement ||
155-
parent.kind === SyntaxKind.WithStatement ||
156-
parent.kind === SyntaxKind.CatchClause) {
157-
158-
addOutliningSpan(parent, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true);
159-
break;
103+
// Check if the block is standalone, or 'attached' to some parent statement.
104+
// If the latter, we want to collapse the block, but consider its hint span
105+
// to be the entire span of the parent.
106+
switch (n.parent.kind) {
107+
case SyntaxKind.DoStatement:
108+
case SyntaxKind.ForInStatement:
109+
case SyntaxKind.ForOfStatement:
110+
case SyntaxKind.ForStatement:
111+
case SyntaxKind.IfStatement:
112+
case SyntaxKind.WhileStatement:
113+
case SyntaxKind.WithStatement:
114+
case SyntaxKind.CatchClause:
115+
return spanForNode(n.parent);
116+
case SyntaxKind.TryStatement:
117+
// Could be the try-block, or the finally-block.
118+
const tryStatement = <TryStatement>n.parent;
119+
if (tryStatement.tryBlock === n) {
120+
return spanForNode(n.parent);
160121
}
161-
162-
if (parent.kind === SyntaxKind.TryStatement) {
163-
// Could be the try-block, or the finally-block.
164-
const tryStatement = <TryStatement>parent;
165-
if (tryStatement.tryBlock === n) {
166-
addOutliningSpan(parent, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true);
167-
break;
168-
}
169-
else if (tryStatement.finallyBlock === n) {
170-
const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile);
171-
if (finallyKeyword) {
172-
addOutliningSpan(finallyKeyword, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true);
173-
break;
174-
}
175-
}
176-
177-
// fall through.
122+
else if (tryStatement.finallyBlock === n) {
123+
return spanForNode(findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile)!);
178124
}
179-
125+
// falls through
126+
default:
180127
// Block was a standalone block. In this case we want to only collapse
181128
// the span of the block, independent of any parent span.
182-
const span = createTextSpanFromNode(n);
183-
elements.push({
184-
textSpan: span,
185-
hintSpan: span,
186-
bannerText: collapseText,
187-
autoCollapse: autoCollapse(n)
188-
});
189-
break;
190-
}
191-
// falls through
192-
193-
case SyntaxKind.ModuleBlock: {
194-
const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile);
195-
const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile);
196-
addOutliningSpan(n.parent, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true);
197-
break;
198-
}
199-
case SyntaxKind.ClassDeclaration:
200-
case SyntaxKind.InterfaceDeclaration:
201-
case SyntaxKind.EnumDeclaration:
202-
case SyntaxKind.CaseBlock: {
203-
const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile);
204-
const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile);
205-
addOutliningSpan(n, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true);
206-
break;
129+
return createOutliningSpan(createTextSpanFromNode(n, sourceFile));
207130
}
208-
// If the block has no leading keywords and is inside an array literal,
209-
// we only want to collapse the span of the block.
210-
// Otherwise, the collapsed section will include the end of the previous line.
211-
case SyntaxKind.ObjectLiteralExpression:
212-
const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile);
213-
const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile);
214-
addOutliningSpan(n, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ !isArrayLiteralExpression(n.parent));
215-
break;
216-
case SyntaxKind.ArrayLiteralExpression:
217-
const openBracket = findChildOfKind(n, SyntaxKind.OpenBracketToken, sourceFile);
218-
const closeBracket = findChildOfKind(n, SyntaxKind.CloseBracketToken, sourceFile);
219-
addOutliningSpan(n, openBracket, closeBracket, autoCollapse(n), /*useFullStart*/ !isArrayLiteralExpression(n.parent));
220-
break;
131+
case SyntaxKind.ModuleBlock:
132+
return spanForNode(n.parent);
133+
case SyntaxKind.ClassDeclaration:
134+
case SyntaxKind.InterfaceDeclaration:
135+
case SyntaxKind.EnumDeclaration:
136+
case SyntaxKind.CaseBlock:
137+
return spanForNode(n);
138+
case SyntaxKind.ObjectLiteralExpression:
139+
return spanForObjectOrArrayLiteral(n);
140+
case SyntaxKind.ArrayLiteralExpression:
141+
return spanForObjectOrArrayLiteral(n, SyntaxKind.OpenBracketToken);
142+
}
143+
144+
function spanForObjectOrArrayLiteral(node: Node, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined {
145+
// If the block has no leading keywords and is inside an array literal,
146+
// we only want to collapse the span of the block.
147+
// Otherwise, the collapsed section will include the end of the previous line.
148+
return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent), open);
149+
}
150+
151+
function spanForNode(hintSpanNode: Node, autoCollapse = false, useFullStart = true, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined {
152+
const openToken = findChildOfKind(n, open, sourceFile);
153+
const close = open === SyntaxKind.OpenBraceToken ? SyntaxKind.CloseBraceToken : SyntaxKind.CloseBracketToken;
154+
const closeToken = findChildOfKind(n, close, sourceFile);
155+
if (!openToken || !closeToken) {
156+
return undefined;
221157
}
222-
depth++;
223-
forEachChild(n, walk);
224-
depth--;
158+
const textSpan = createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd());
159+
return createOutliningSpan(textSpan, createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse);
225160
}
226161
}
162+
163+
function createOutliningSpan(textSpan: TextSpan, hintSpan: TextSpan = textSpan, autoCollapse = false, bannerText = "..."): OutliningSpan {
164+
return { textSpan, hintSpan, bannerText, autoCollapse };
165+
}
227166
}

tests/cases/fourslash/getOutliningSpans.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference path="fourslash.ts"/>
22

3-
////// interface
3+
////// interface
44
////interface IFoo[| {
55
//// getDist(): number;
66
////}|]
@@ -63,9 +63,9 @@
6363
////}|])
6464
////
6565
////// trivia handeling
66-
////class ClassFooWithTrivia[| /* some comments */
66+
////class ClassFooWithTrivia[| /* some comments */
6767
//// /* more trivia */ {
68-
////
68+
////
6969
////
7070
//// /*some trailing trivia */
7171
////}|] /* even more */
@@ -85,8 +85,8 @@
8585
//// [
8686
//// [
8787
//// [
88-
//// 1,2,3
89-
//// ]
88+
//// 1,2,3
89+
//// ]
9090
//// ]
9191
//// ]
9292
//// ]

0 commit comments

Comments
 (0)