Skip to content

Clean up outliningElementsCollector #20143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
2 commits merged into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 134 additions & 195 deletions src/services/outliningElementsCollector.ts
Original file line number Diff line number Diff line change
@@ -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<OutliningSpan>): void {
let depthRemaining = 40;
sourceFile.forEachChild(function walk(n) {
if (depthRemaining === 0) return;
cancellationToken.throwIfCancellationRequested();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are calling this on every single node -- we should come up with a way to call this less often.


/** 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<OutliningSpan>): 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<OutliningSpan>): 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 = <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 = <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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this not properly find the open and close brace tokens? They're children of the block itself, but we want to pass the parent to createOutliningSpan.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spanForNode closes over n and uses that to find tokens. The argument is hintSpanNode which is only used for its own span, not for its tokens.

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 };
}
}
10 changes: 5 additions & 5 deletions tests/cases/fourslash/getOutliningSpans.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference path="fourslash.ts"/>

////// interface
////// interface
////interface IFoo[| {
//// getDist(): number;
////}|]
Expand Down Expand Up @@ -63,9 +63,9 @@
////}|])
////
////// trivia handeling
////class ClassFooWithTrivia[| /* some comments */
////class ClassFooWithTrivia[| /* some comments */
//// /* more trivia */ {
////
////
////
//// /*some trailing trivia */
////}|] /* even more */
Expand All @@ -85,8 +85,8 @@
//// [
//// [
//// [
//// 1,2,3
//// ]
//// 1,2,3
//// ]
//// ]
//// ]
//// ]
Expand Down
Loading