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