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