diff --git a/src/services/outliningElementsCollector.ts b/src/services/outliningElementsCollector.ts index bd294209088fb..f3f91724a9c9a 100644 --- a/src/services/outliningElementsCollector.ts +++ b/src/services/outliningElementsCollector.ts @@ -1,227 +1,166 @@ /* @internal */ namespace ts.OutliningElementsCollector { - const collapseText = "..."; - const maxDepth = 40; - const defaultLabel = "#region"; - const regionMatch = new RegExp("^\\s*//\\s*#(end)?region(?:\\s+(.*))?$"); - export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] { - const elements: OutliningSpan[] = []; - let depth = 0; - const regions: OutliningSpan[] = []; + const res: OutliningSpan[] = []; + addNodeOutliningSpans(sourceFile, cancellationToken, res); + addRegionOutliningSpans(sourceFile, res); + return res.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); + } - walk(sourceFile); - gatherRegions(); - return elements.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); + function addNodeOutliningSpans(sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push): void { + let depthRemaining = 40; + sourceFile.forEachChild(function walk(n) { + if (depthRemaining === 0) return; + cancellationToken.throwIfCancellationRequested(); - /** If useFullStart is true, then the collapsing span includes leading whitespace, including linebreaks. */ - function addOutliningSpan(hintSpanNode: Node, startElement: Node, endElement: Node, autoCollapse: boolean, useFullStart: boolean) { - if (hintSpanNode && startElement && endElement) { - const span: OutliningSpan = { - textSpan: createTextSpanFromBounds(useFullStart ? startElement.getFullStart() : startElement.getStart(), endElement.getEnd()), - hintSpan: createTextSpanFromNode(hintSpanNode, sourceFile), - bannerText: collapseText, - autoCollapse, - }; - elements.push(span); + if (isDeclaration(n)) { + addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out); } - } - function addOutliningSpanComments(commentSpan: CommentRange, autoCollapse: boolean) { - if (commentSpan) { - const span: OutliningSpan = { - textSpan: createTextSpanFromBounds(commentSpan.pos, commentSpan.end), - hintSpan: createTextSpanFromBounds(commentSpan.pos, commentSpan.end), - bannerText: collapseText, - autoCollapse, - }; - elements.push(span); - } - } + const span = getOutliningSpanForNode(n, sourceFile); + if (span) out.push(span); - function addOutliningForLeadingCommentsForNode(n: Node) { - const comments = ts.getLeadingCommentRangesOfNode(n, sourceFile); + depthRemaining--; + n.forEachChild(walk); + depthRemaining++; + }); + } - if (comments) { - let firstSingleLineCommentStart = -1; - let lastSingleLineCommentEnd = -1; - let isFirstSingleLineComment = true; - let singleLineCommentCount = 0; + function addRegionOutliningSpans(sourceFile: SourceFile, out: Push): void { + const regions: OutliningSpan[] = []; + const lineStarts = sourceFile.getLineStarts(); + for (let i = 0; i < lineStarts.length; i++) { + const currentLineStart = lineStarts[i]; + const lineEnd = i + 1 === lineStarts.length ? sourceFile.getEnd() : lineStarts[i + 1] - 1; + const lineText = sourceFile.text.substring(currentLineStart, lineEnd); + const result = lineText.match(/^\s*\/\/\s*#(end)?region(?:\s+(.*))?$/); + if (!result || isInComment(sourceFile, currentLineStart)) { + continue; + } - for (const currentComment of comments) { - cancellationToken.throwIfCancellationRequested(); + if (!result[1]) { + const span = createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd); + regions.push(createOutliningSpan(span, span, /*autoCollapse*/ false, result[2] || "#region")); + } + else { + const region = regions.pop(); + if (region) { + region.textSpan.length = lineEnd - region.textSpan.start; + region.hintSpan.length = lineEnd - region.textSpan.start; + out.push(region); + } + } + } + } + function addOutliningForLeadingCommentsForNode(n: Node, sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push): void { + const comments = getLeadingCommentRangesOfNode(n, sourceFile); + if (!comments) return; + let firstSingleLineCommentStart = -1; + let lastSingleLineCommentEnd = -1; + let singleLineCommentCount = 0; + for (const { kind, pos, end } of comments) { + cancellationToken.throwIfCancellationRequested(); + switch (kind) { + case SyntaxKind.SingleLineCommentTrivia: // For single line comments, combine consecutive ones (2 or more) into // a single span from the start of the first till the end of the last - if (currentComment.kind === SyntaxKind.SingleLineCommentTrivia) { - if (isFirstSingleLineComment) { - firstSingleLineCommentStart = currentComment.pos; - } - isFirstSingleLineComment = false; - lastSingleLineCommentEnd = currentComment.end; - singleLineCommentCount++; - } - else if (currentComment.kind === SyntaxKind.MultiLineCommentTrivia) { - combineAndAddMultipleSingleLineComments(singleLineCommentCount, firstSingleLineCommentStart, lastSingleLineCommentEnd); - addOutliningSpanComments(currentComment, /*autoCollapse*/ false); - - singleLineCommentCount = 0; - lastSingleLineCommentEnd = -1; - isFirstSingleLineComment = true; + if (singleLineCommentCount === 0) { + firstSingleLineCommentStart = pos; } - } - - combineAndAddMultipleSingleLineComments(singleLineCommentCount, firstSingleLineCommentStart, lastSingleLineCommentEnd); + lastSingleLineCommentEnd = end; + singleLineCommentCount++; + break; + case SyntaxKind.MultiLineCommentTrivia: + combineAndAddMultipleSingleLineComments(); + out.push(createOutliningSpanFromBounds(pos, end)); + singleLineCommentCount = 0; + break; + default: + Debug.assertNever(kind); } } + combineAndAddMultipleSingleLineComments(); - function combineAndAddMultipleSingleLineComments(count: number, start: number, end: number) { - + function combineAndAddMultipleSingleLineComments(): void { // Only outline spans of two or more consecutive single line comments - if (count > 1) { - const multipleSingleLineComments: CommentRange = { - kind: SyntaxKind.SingleLineCommentTrivia, - pos: start, - end, - }; - - addOutliningSpanComments(multipleSingleLineComments, /*autoCollapse*/ false); + if (singleLineCommentCount > 1) { + out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd)); } } + } - function autoCollapse(node: Node) { - return isFunctionBlock(node) && node.parent.kind !== SyntaxKind.ArrowFunction; - } - - function gatherRegions(): void { - const lineStarts = sourceFile.getLineStarts(); - - for (let i = 0; i < lineStarts.length; i++) { - const currentLineStart = lineStarts[i]; - const lineEnd = lineStarts[i + 1] - 1 || sourceFile.getEnd(); - const comment = sourceFile.text.substring(currentLineStart, lineEnd); - const result = comment.match(regionMatch); + function createOutliningSpanFromBounds(pos: number, end: number): OutliningSpan { + return createOutliningSpan(createTextSpanFromBounds(pos, end)); + } - if (result && !isInComment(sourceFile, currentLineStart)) { - if (!result[1]) { - const start = sourceFile.getFullText().indexOf("//", currentLineStart); - const textSpan = createTextSpanFromBounds(start, lineEnd); - const region: OutliningSpan = { - textSpan, - hintSpan: textSpan, - bannerText: result[2] || defaultLabel, - autoCollapse: false - }; - regions.push(region); - } - else { - const region = regions.pop(); - if (region) { - region.textSpan.length = lineEnd - region.textSpan.start; - region.hintSpan.length = lineEnd - region.textSpan.start; - elements.push(region); - } - } + function getOutliningSpanForNode(n: Node, sourceFile: SourceFile): OutliningSpan | undefined { + switch (n.kind) { + case SyntaxKind.Block: + if (isFunctionBlock(n)) { + return spanForNode(n.parent, /*autoCollapse*/ n.parent.kind !== SyntaxKind.ArrowFunction); } - } - } - - function walk(n: Node): void { - cancellationToken.throwIfCancellationRequested(); - if (depth > maxDepth) { - return; - } - - if (isDeclaration(n)) { - addOutliningForLeadingCommentsForNode(n); - } - - switch (n.kind) { - case SyntaxKind.Block: - if (!isFunctionBlock(n)) { - const parent = n.parent; - const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile); - const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile); - - // Check if the block is standalone, or 'attached' to some parent statement. - // If the latter, we want to collapse the block, but consider its hint span - // to be the entire span of the parent. - if (parent.kind === SyntaxKind.DoStatement || - parent.kind === SyntaxKind.ForInStatement || - parent.kind === SyntaxKind.ForOfStatement || - parent.kind === SyntaxKind.ForStatement || - parent.kind === SyntaxKind.IfStatement || - parent.kind === SyntaxKind.WhileStatement || - parent.kind === SyntaxKind.WithStatement || - parent.kind === SyntaxKind.CatchClause) { - - addOutliningSpan(parent, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true); - break; + // Check if the block is standalone, or 'attached' to some parent statement. + // If the latter, we want to collapse the block, but consider its hint span + // to be the entire span of the parent. + switch (n.parent.kind) { + case SyntaxKind.DoStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.CatchClause: + return spanForNode(n.parent); + case SyntaxKind.TryStatement: + // Could be the try-block, or the finally-block. + const tryStatement = n.parent; + if (tryStatement.tryBlock === n) { + return spanForNode(n.parent); } - - if (parent.kind === SyntaxKind.TryStatement) { - // Could be the try-block, or the finally-block. - const tryStatement = parent; - if (tryStatement.tryBlock === n) { - addOutliningSpan(parent, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true); - break; - } - else if (tryStatement.finallyBlock === n) { - const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile); - if (finallyKeyword) { - addOutliningSpan(finallyKeyword, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true); - break; - } - } - - // fall through. + else if (tryStatement.finallyBlock === n) { + return spanForNode(findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile)!); } - + // falls through + default: // Block was a standalone block. In this case we want to only collapse // the span of the block, independent of any parent span. - const span = createTextSpanFromNode(n); - elements.push({ - textSpan: span, - hintSpan: span, - bannerText: collapseText, - autoCollapse: autoCollapse(n) - }); - break; - } - // falls through - - case SyntaxKind.ModuleBlock: { - const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile); - const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile); - addOutliningSpan(n.parent, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true); - break; - } - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.CaseBlock: { - const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile); - const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile); - addOutliningSpan(n, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ true); - break; + return createOutliningSpan(createTextSpanFromNode(n, sourceFile)); } - // If the block has no leading keywords and is inside an array literal, - // we only want to collapse the span of the block. - // Otherwise, the collapsed section will include the end of the previous line. - case SyntaxKind.ObjectLiteralExpression: - const openBrace = findChildOfKind(n, SyntaxKind.OpenBraceToken, sourceFile); - const closeBrace = findChildOfKind(n, SyntaxKind.CloseBraceToken, sourceFile); - addOutliningSpan(n, openBrace, closeBrace, autoCollapse(n), /*useFullStart*/ !isArrayLiteralExpression(n.parent)); - break; - case SyntaxKind.ArrayLiteralExpression: - const openBracket = findChildOfKind(n, SyntaxKind.OpenBracketToken, sourceFile); - const closeBracket = findChildOfKind(n, SyntaxKind.CloseBracketToken, sourceFile); - addOutliningSpan(n, openBracket, closeBracket, autoCollapse(n), /*useFullStart*/ !isArrayLiteralExpression(n.parent)); - break; + case SyntaxKind.ModuleBlock: + return spanForNode(n.parent); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.CaseBlock: + return spanForNode(n); + case SyntaxKind.ObjectLiteralExpression: + return spanForObjectOrArrayLiteral(n); + case SyntaxKind.ArrayLiteralExpression: + return spanForObjectOrArrayLiteral(n, SyntaxKind.OpenBracketToken); + } + + function spanForObjectOrArrayLiteral(node: Node, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined { + // If the block has no leading keywords and is inside an array literal, + // we only want to collapse the span of the block. + // Otherwise, the collapsed section will include the end of the previous line. + return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent), open); + } + + function spanForNode(hintSpanNode: Node, autoCollapse = false, useFullStart = true, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined { + const openToken = findChildOfKind(n, open, sourceFile); + const close = open === SyntaxKind.OpenBraceToken ? SyntaxKind.CloseBraceToken : SyntaxKind.CloseBracketToken; + const closeToken = findChildOfKind(n, close, sourceFile); + if (!openToken || !closeToken) { + return undefined; } - depth++; - forEachChild(n, walk); - depth--; + const textSpan = createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd()); + return createOutliningSpan(textSpan, createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse); } } + + function createOutliningSpan(textSpan: TextSpan, hintSpan: TextSpan = textSpan, autoCollapse = false, bannerText = "..."): OutliningSpan { + return { textSpan, hintSpan, bannerText, autoCollapse }; + } } \ No newline at end of file diff --git a/tests/cases/fourslash/getOutliningSpans.ts b/tests/cases/fourslash/getOutliningSpans.ts index 4e2a58c9acb8f..63e8becfc4fcc 100644 --- a/tests/cases/fourslash/getOutliningSpans.ts +++ b/tests/cases/fourslash/getOutliningSpans.ts @@ -1,6 +1,6 @@ /// -////// interface +////// interface ////interface IFoo[| { //// getDist(): number; ////}|] @@ -63,9 +63,9 @@ ////}|]) //// ////// trivia handeling -////class ClassFooWithTrivia[| /* some comments */ +////class ClassFooWithTrivia[| /* some comments */ //// /* more trivia */ { -//// +//// //// //// /*some trailing trivia */ ////}|] /* even more */ @@ -85,8 +85,8 @@ //// [ //// [ //// [ -//// 1,2,3 -//// ] +//// 1,2,3 +//// ] //// ] //// ] //// ] diff --git a/tests/cases/fourslash/shims-pp/getOutliningSpans.ts b/tests/cases/fourslash/shims-pp/getOutliningSpans.ts index 4e2a58c9acb8f..63e8becfc4fcc 100644 --- a/tests/cases/fourslash/shims-pp/getOutliningSpans.ts +++ b/tests/cases/fourslash/shims-pp/getOutliningSpans.ts @@ -1,6 +1,6 @@ /// -////// interface +////// interface ////interface IFoo[| { //// getDist(): number; ////}|] @@ -63,9 +63,9 @@ ////}|]) //// ////// trivia handeling -////class ClassFooWithTrivia[| /* some comments */ +////class ClassFooWithTrivia[| /* some comments */ //// /* more trivia */ { -//// +//// //// //// /*some trailing trivia */ ////}|] /* even more */ @@ -85,8 +85,8 @@ //// [ //// [ //// [ -//// 1,2,3 -//// ] +//// 1,2,3 +//// ] //// ] //// ] //// ] diff --git a/tests/cases/fourslash/shims/getOutliningSpans.ts b/tests/cases/fourslash/shims/getOutliningSpans.ts index 4e2a58c9acb8f..63e8becfc4fcc 100644 --- a/tests/cases/fourslash/shims/getOutliningSpans.ts +++ b/tests/cases/fourslash/shims/getOutliningSpans.ts @@ -1,6 +1,6 @@ /// -////// interface +////// interface ////interface IFoo[| { //// getDist(): number; ////}|] @@ -63,9 +63,9 @@ ////}|]) //// ////// trivia handeling -////class ClassFooWithTrivia[| /* some comments */ +////class ClassFooWithTrivia[| /* some comments */ //// /* more trivia */ { -//// +//// //// //// /*some trailing trivia */ ////}|] /* even more */ @@ -85,8 +85,8 @@ //// [ //// [ //// [ -//// 1,2,3 -//// ] +//// 1,2,3 +//// ] //// ] //// ] //// ]