-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
|
||
/** 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 }; | ||
} | ||
} |
There was a problem hiding this comment.
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.