• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.OutliningElementsCollector {
3    export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): 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    }
9
10    function addNodeOutliningSpans(sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push<OutliningSpan>): void {
11        let depthRemaining = 40;
12        let current = 0;
13        // Includes the EOF Token so that comments which aren't attached to statements are included
14        const statements = [...sourceFile.statements, sourceFile.endOfFileToken];
15        const n = statements.length;
16        while (current < n) {
17            while (current < n && !isAnyImportSyntax(statements[current])) {
18                visitNonImportNode(statements[current]);
19                current++;
20            }
21            if (current === n) break;
22            const firstImport = current;
23            while (current < n && isAnyImportSyntax(statements[current])) {
24                addOutliningForLeadingCommentsForNode(statements[current], sourceFile, cancellationToken, out);
25                current++;
26            }
27            const lastImport = current - 1;
28            if (lastImport !== firstImport) {
29                out.push(createOutliningSpanFromBounds(findChildOfKind(statements[firstImport], SyntaxKind.ImportKeyword, sourceFile)!.getStart(sourceFile), statements[lastImport].getEnd(), OutliningSpanKind.Imports));
30            }
31        }
32
33        function visitNonImportNode(n: Node) {
34            if (depthRemaining === 0) return;
35            cancellationToken.throwIfCancellationRequested();
36
37            if (isDeclaration(n) || isVariableStatement(n) || n.kind === SyntaxKind.EndOfFileToken) {
38                addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out);
39            }
40
41            if (isFunctionLike(n) && isBinaryExpression(n.parent) && isPropertyAccessExpression(n.parent.left)) {
42                addOutliningForLeadingCommentsForNode(n.parent.left, sourceFile, cancellationToken, out);
43            }
44
45            const span = getOutliningSpanForNode(n, sourceFile);
46            if (span) out.push(span);
47
48            depthRemaining--;
49            if (isCallExpression(n)) {
50                depthRemaining++;
51                visitNonImportNode(n.expression);
52                depthRemaining--;
53                n.arguments.forEach(visitNonImportNode);
54                n.typeArguments?.forEach(visitNonImportNode);
55            }
56            else if (isIfStatement(n) && n.elseStatement && isIfStatement(n.elseStatement)) {
57                // Consider an 'else if' to be on the same depth as the 'if'.
58                visitNonImportNode(n.expression);
59                visitNonImportNode(n.thenStatement);
60                depthRemaining++;
61                visitNonImportNode(n.elseStatement);
62                depthRemaining--;
63            }
64            else {
65                n.forEachChild(visitNonImportNode);
66            }
67            depthRemaining++;
68        }
69    }
70
71    function addRegionOutliningSpans(sourceFile: SourceFile, out: Push<OutliningSpan>): void {
72        const regions: OutliningSpan[] = [];
73        const lineStarts = sourceFile.getLineStarts();
74        for (const currentLineStart of lineStarts) {
75            const lineEnd = sourceFile.getLineEndOfPosition(currentLineStart);
76            const lineText = sourceFile.text.substring(currentLineStart, lineEnd);
77            const result = isRegionDelimiter(lineText);
78            if (!result || isInComment(sourceFile, currentLineStart)) {
79                continue;
80            }
81
82            if (!result[1]) {
83                const span = createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd);
84                regions.push(createOutliningSpan(span, OutliningSpanKind.Region, span, /*autoCollapse*/ false, result[2] || "#region"));
85            }
86            else {
87                const region = regions.pop();
88                if (region) {
89                    region.textSpan.length = lineEnd - region.textSpan.start;
90                    region.hintSpan.length = lineEnd - region.textSpan.start;
91                    out.push(region);
92                }
93            }
94        }
95    }
96
97    const regionDelimiterRegExp = /^\s*\/\/\s*#(end)?region(?:\s+(.*))?(?:\r)?$/;
98    function isRegionDelimiter(lineText: string) {
99        return regionDelimiterRegExp.exec(lineText);
100    }
101
102    function addOutliningForLeadingCommentsForNode(n: Node, sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push<OutliningSpan>): void {
103        const comments = getLeadingCommentRangesOfNode(n, sourceFile);
104        if (!comments) return;
105        let firstSingleLineCommentStart = -1;
106        let lastSingleLineCommentEnd = -1;
107        let singleLineCommentCount = 0;
108        const sourceText = sourceFile.getFullText();
109        for (const { kind, pos, end } of comments) {
110            cancellationToken.throwIfCancellationRequested();
111            switch (kind) {
112                case SyntaxKind.SingleLineCommentTrivia:
113                    // never fold region delimiters into single-line comment regions
114                    const commentText = sourceText.slice(pos, end);
115                    if (isRegionDelimiter(commentText)) {
116                        combineAndAddMultipleSingleLineComments();
117                        singleLineCommentCount = 0;
118                        break;
119                    }
120
121                    // For single line comments, combine consecutive ones (2 or more) into
122                    // a single span from the start of the first till the end of the last
123                    if (singleLineCommentCount === 0) {
124                        firstSingleLineCommentStart = pos;
125                    }
126                    lastSingleLineCommentEnd = end;
127                    singleLineCommentCount++;
128                    break;
129                case SyntaxKind.MultiLineCommentTrivia:
130                    combineAndAddMultipleSingleLineComments();
131                    out.push(createOutliningSpanFromBounds(pos, end, OutliningSpanKind.Comment));
132                    singleLineCommentCount = 0;
133                    break;
134                default:
135                    Debug.assertNever(kind);
136            }
137        }
138        combineAndAddMultipleSingleLineComments();
139
140        function combineAndAddMultipleSingleLineComments(): void {
141            // Only outline spans of two or more consecutive single line comments
142            if (singleLineCommentCount > 1) {
143                out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, OutliningSpanKind.Comment));
144            }
145        }
146    }
147
148    function createOutliningSpanFromBounds(pos: number, end: number, kind: OutliningSpanKind): OutliningSpan {
149        return createOutliningSpan(createTextSpanFromBounds(pos, end), kind);
150    }
151
152    function getOutliningSpanForNode(n: Node, sourceFile: SourceFile): OutliningSpan | undefined {
153        switch (n.kind) {
154            case SyntaxKind.Block:
155                if (isFunctionLike(n.parent)) {
156                    return functionSpan(n.parent, n as Block, sourceFile);
157                }
158                // Check if the block is standalone, or 'attached' to some parent statement.
159                // If the latter, we want to collapse the block, but consider its hint span
160                // to be the entire span of the parent.
161                switch (n.parent.kind) {
162                    case SyntaxKind.DoStatement:
163                    case SyntaxKind.ForInStatement:
164                    case SyntaxKind.ForOfStatement:
165                    case SyntaxKind.ForStatement:
166                    case SyntaxKind.IfStatement:
167                    case SyntaxKind.WhileStatement:
168                    case SyntaxKind.WithStatement:
169                    case SyntaxKind.CatchClause:
170                        return spanForNode(n.parent);
171                    case SyntaxKind.TryStatement:
172                        // Could be the try-block, or the finally-block.
173                        const tryStatement = <TryStatement>n.parent;
174                        if (tryStatement.tryBlock === n) {
175                            return spanForNode(n.parent);
176                        }
177                        else if (tryStatement.finallyBlock === n) {
178                            const node = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile);
179                            if (node) return spanForNode(node);
180                        }
181                        // falls through
182                    default:
183                        // Block was a standalone block.  In this case we want to only collapse
184                        // the span of the block, independent of any parent span.
185                        return createOutliningSpan(createTextSpanFromNode(n, sourceFile), OutliningSpanKind.Code);
186                }
187            case SyntaxKind.ModuleBlock:
188                return spanForNode(n.parent);
189            case SyntaxKind.ClassDeclaration:
190            case SyntaxKind.ClassExpression:
191            case SyntaxKind.StructDeclaration:
192            case SyntaxKind.InterfaceDeclaration:
193            case SyntaxKind.EnumDeclaration:
194            case SyntaxKind.CaseBlock:
195            case SyntaxKind.TypeLiteral:
196            case SyntaxKind.ObjectBindingPattern:
197                return spanForNode(n);
198            case SyntaxKind.TupleType:
199                return spanForNode(n, /*autoCollapse*/ false, /*useFullStart*/ !isTupleTypeNode(n.parent), SyntaxKind.OpenBracketToken);
200            case SyntaxKind.CaseClause:
201            case SyntaxKind.DefaultClause:
202                return spanForNodeArray((n as CaseClause | DefaultClause).statements);
203            case SyntaxKind.ObjectLiteralExpression:
204                return spanForObjectOrArrayLiteral(n);
205            case SyntaxKind.ArrayLiteralExpression:
206                return spanForObjectOrArrayLiteral(n, SyntaxKind.OpenBracketToken);
207            case SyntaxKind.JsxElement:
208                return spanForJSXElement(<JsxElement>n);
209            case SyntaxKind.JsxFragment:
210                return spanForJSXFragment(<JsxFragment>n);
211            case SyntaxKind.JsxSelfClosingElement:
212            case SyntaxKind.JsxOpeningElement:
213                return spanForJSXAttributes((<JsxOpeningLikeElement>n).attributes);
214            case SyntaxKind.TemplateExpression:
215            case SyntaxKind.NoSubstitutionTemplateLiteral:
216                return spanForTemplateLiteral(<TemplateExpression | NoSubstitutionTemplateLiteral>n);
217            case SyntaxKind.ArrayBindingPattern:
218                return spanForNode(n, /*autoCollapse*/ false, /*useFullStart*/ !isBindingElement(n.parent), SyntaxKind.OpenBracketToken);
219            case SyntaxKind.ArrowFunction:
220                return spanForArrowFunction(<ArrowFunction>n);
221            case SyntaxKind.CallExpression:
222                return spanForCallExpression(<CallExpression>n);
223        }
224
225        function spanForCallExpression(node: CallExpression): OutliningSpan | undefined {
226            if (!node.arguments.length) {
227                return undefined;
228            }
229            const openToken = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile);
230            const closeToken = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile);
231            if (!openToken || !closeToken || positionsAreOnSameLine(openToken.pos, closeToken.pos, sourceFile)) {
232                return undefined;
233            }
234
235            return spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ false, /*useFullStart*/ true);
236        }
237
238        function spanForArrowFunction(node: ArrowFunction): OutliningSpan | undefined {
239            if (isBlock(node.body) || positionsAreOnSameLine(node.body.getFullStart(), node.body.getEnd(), sourceFile)) {
240                return undefined;
241            }
242            const textSpan = createTextSpanFromBounds(node.body.getFullStart(), node.body.getEnd());
243            return createOutliningSpan(textSpan, OutliningSpanKind.Code, createTextSpanFromNode(node));
244        }
245
246        function spanForJSXElement(node: JsxElement): OutliningSpan | undefined {
247            const textSpan = createTextSpanFromBounds(node.openingElement.getStart(sourceFile), node.closingElement.getEnd());
248            const tagName = node.openingElement.tagName.getText(sourceFile);
249            const bannerText = "<" + tagName + ">...</" + tagName + ">";
250            return createOutliningSpan(textSpan, OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText);
251        }
252
253        function spanForJSXFragment(node: JsxFragment): OutliningSpan | undefined {
254            const textSpan = createTextSpanFromBounds(node.openingFragment.getStart(sourceFile), node.closingFragment.getEnd());
255            const bannerText = "<>...</>";
256            return createOutliningSpan(textSpan, OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText);
257        }
258
259        function spanForJSXAttributes(node: JsxAttributes): OutliningSpan | undefined {
260            if (node.properties.length === 0) {
261                return undefined;
262            }
263
264            return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), OutliningSpanKind.Code);
265        }
266
267        function spanForTemplateLiteral(node: TemplateExpression | NoSubstitutionTemplateLiteral) {
268            if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.text.length === 0) {
269                return undefined;
270            }
271            return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), OutliningSpanKind.Code);
272        }
273
274        function spanForObjectOrArrayLiteral(node: Node, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined {
275            // If the block has no leading keywords and is inside an array literal or call expression,
276            // we only want to collapse the span of the block.
277            // Otherwise, the collapsed section will include the end of the previous line.
278            return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent) && !isCallExpression(node.parent), open);
279        }
280
281        function spanForNode(hintSpanNode: Node, autoCollapse = false, useFullStart = true, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken, close: SyntaxKind = open === SyntaxKind.OpenBraceToken ? SyntaxKind.CloseBraceToken : SyntaxKind.CloseBracketToken): OutliningSpan | undefined {
282            const openToken = findChildOfKind(n, open, sourceFile);
283            const closeToken = findChildOfKind(n, close, sourceFile);
284            return openToken && closeToken && spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, autoCollapse, useFullStart);
285        }
286
287        function spanForNodeArray(nodeArray: NodeArray<Node>): OutliningSpan | undefined {
288            return nodeArray.length ? createOutliningSpan(createTextSpanFromRange(nodeArray), OutliningSpanKind.Code) : undefined;
289        }
290    }
291
292    function functionSpan(node: SignatureDeclaration, body: Block, sourceFile: SourceFile): OutliningSpan | undefined {
293        const openToken = tryGetFunctionOpenToken(node, body, sourceFile);
294        const closeToken = findChildOfKind(body, SyntaxKind.CloseBraceToken, sourceFile);
295        return openToken && closeToken && spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ node.kind !== SyntaxKind.ArrowFunction);
296    }
297
298    function spanBetweenTokens(openToken: Node, closeToken: Node, hintSpanNode: Node, sourceFile: SourceFile, autoCollapse = false, useFullStart = true): OutliningSpan {
299        const textSpan = createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd());
300        return createOutliningSpan(textSpan, OutliningSpanKind.Code, createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse);
301    }
302
303    function createOutliningSpan(textSpan: TextSpan, kind: OutliningSpanKind, hintSpan: TextSpan = textSpan, autoCollapse = false, bannerText = "..."): OutliningSpan {
304        return { textSpan, kind, hintSpan, bannerText, autoCollapse };
305    }
306
307    function tryGetFunctionOpenToken(node: SignatureDeclaration, body: Block, sourceFile: SourceFile): Node | undefined {
308        if (isNodeArrayMultiLine(node.parameters, sourceFile)) {
309            const openParenToken = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile);
310            if (openParenToken) {
311                return openParenToken;
312            }
313        }
314        return findChildOfKind(body, SyntaxKind.OpenBraceToken, sourceFile);
315    }
316}
317