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