|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Structure;
internal static class CSharpStructureHelpers
{
public const string Ellipsis = "...";
public const string MultiLineCommentSuffix = "*/";
public const int MaxXmlDocCommentBannerLength = 120;
private static readonly char[] s_newLineCharacters = ['\r', '\n'];
private static int GetCollapsibleStart(SyntaxToken firstToken)
{
// Check *this* token to see if it has any trailing comments and use the last one; otherwise, we use the end
// of this token.
var lastTrailingCommentOrWhitespaceTrivia = firstToken.TrailingTrivia.GetLastCommentOrWhitespace();
return lastTrailingCommentOrWhitespaceTrivia?.Span.End ?? firstToken.Span.End;
}
private static (int spanEnd, int hintEnd) GetCollapsibleEnd(SyntaxToken lastToken, bool compressEmptyLines)
{
// If the token has any trailing comments, we use the end of the token;
// otherwise, the behavior depends on 'compressEmptyLines':
// false: skip to the start of the first new line trivia
// true: skip to the start of the last new line trivia preceding a non-whitespace line
//
// The hint span never includes the compressed empty lines.
var trailingTrivia = lastToken.TrailingTrivia;
var nextLeadingTrivia = compressEmptyLines ? lastToken.GetNextToken(includeZeroWidth: true, includeSkipped: true).LeadingTrivia : default;
var end = lastToken.Span.End;
int? hintEnd = null;
foreach (var trivia in trailingTrivia)
{
if (!ProcessTrivia(trivia, compressEmptyLines, ref end, ref hintEnd))
return (end, hintEnd ?? end);
}
foreach (var trivia in nextLeadingTrivia)
{
if (!ProcessTrivia(trivia, compressEmptyLines, ref end, ref hintEnd))
return (end, hintEnd ?? end);
}
return (end, hintEnd ?? end);
// Return true to keep processing trivia; otherwise, false to return the current 'end'
static bool ProcessTrivia(SyntaxTrivia trivia, bool compressEmptyLines, ref int end, ref int? hintEnd)
{
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
{
end = trivia.SpanStart;
hintEnd ??= end;
if (!compressEmptyLines)
return false;
}
else if (!trivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
// We want this trivia to be visible even when the element is collapsed
return false;
}
return true;
}
}
public static SyntaxToken GetLastInlineMethodBlockToken(SyntaxNode node)
{
var lastToken = node.GetLastToken(includeZeroWidth: true);
if (lastToken.Kind() == SyntaxKind.None)
{
return default;
}
// If the next token is a semicolon, and we aren't in the initializer of a for-loop, use that token as the end.
var nextToken = lastToken.GetNextToken(includeSkipped: true);
if (nextToken.Kind() is not SyntaxKind.None and SyntaxKind.SemicolonToken)
{
var forStatement = nextToken.GetAncestor<ForStatementSyntax>();
if (forStatement != null && forStatement.FirstSemicolonToken == nextToken)
{
return default;
}
lastToken = nextToken;
}
return lastToken;
}
private static string CreateCommentBannerTextWithPrefix(string text, string prefix)
{
Contract.ThrowIfNull(text);
Contract.ThrowIfNull(prefix);
var prefixLength = prefix.Length;
return prefix + " " + text[prefixLength..].Trim() + " " + Ellipsis;
}
public static string GetCommentBannerText(SyntaxTrivia comment)
{
Contract.ThrowIfFalse(comment.IsSingleLineComment() || comment.IsMultiLineComment());
if (comment.IsSingleLineComment())
{
return CreateCommentBannerTextWithPrefix(comment.ToString(), "//");
}
else if (comment.IsMultiLineComment())
{
var lineBreakStart = comment.ToString().IndexOfAny(s_newLineCharacters);
var text = comment.ToString();
if (lineBreakStart >= 0)
{
text = text[..lineBreakStart];
}
else
{
text = text.Length >= "/**/".Length && text.EndsWith(MultiLineCommentSuffix)
? text[..^MultiLineCommentSuffix.Length]
: text;
}
return CreateCommentBannerTextWithPrefix(text, "/*");
}
else
{
return string.Empty;
}
}
private static BlockSpan CreateCommentBlockSpan(
SyntaxTrivia startComment, SyntaxTrivia endComment)
{
var span = TextSpan.FromBounds(startComment.SpanStart, endComment.Span.End);
return new BlockSpan(
isCollapsible: true,
textSpan: span,
hintSpan: span,
type: BlockTypes.Comment,
bannerText: GetCommentBannerText(startComment),
autoCollapse: true);
}
public static void CollectCommentBlockSpans(
SyntaxTriviaList triviaList, ArrayBuilder<BlockSpan> spans)
{
if (triviaList.Count > 0)
{
SyntaxTrivia? startComment = null;
SyntaxTrivia? endComment = null;
// Iterate through trivia and collect the following:
// 1. Groups of contiguous single-line comments that are only separated by whitespace
// 2. Multi-line comments
foreach (var trivia in triviaList)
{
if (trivia.IsSingleLineComment())
{
startComment ??= trivia;
endComment = trivia;
}
else if (trivia.IsMultiLineComment())
{
// Multiline comments are handled by the MultilineCommentBlockStructureProvider.
continue;
}
else if (trivia is not SyntaxTrivia(
SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia or SyntaxKind.EndOfFileToken))
{
completeSingleLineCommentGroup(spans);
}
}
completeSingleLineCommentGroup(spans);
return;
void completeSingleLineCommentGroup(ArrayBuilder<BlockSpan> spans)
{
if (startComment != null)
{
var singleLineCommentGroupRegion = CreateCommentBlockSpan(startComment.Value, endComment!.Value);
spans.Add(singleLineCommentGroupRegion);
startComment = null;
endComment = null;
}
}
}
}
public static void CollectCommentBlockSpans(
SyntaxNode node,
ArrayBuilder<BlockSpan> spans,
in BlockStructureOptions options)
{
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
if (options.IsMetadataAsSource && TryGetLeadingCollapsibleSpan(node, out var span))
{
spans.Add(span);
}
else
{
var triviaList = node.GetLeadingTrivia();
CollectCommentBlockSpans(triviaList, spans);
}
return;
// Local functions
static bool TryGetLeadingCollapsibleSpan(SyntaxNode node, out BlockSpan span)
{
var startToken = node.GetFirstToken();
var endToken = GetEndToken(node);
if (startToken.IsKind(SyntaxKind.None) || endToken.IsKind(SyntaxKind.None))
{
// if valid tokens can't be found then a meaningful span can't be generated
span = default;
return false;
}
var firstComment = startToken.LeadingTrivia.FirstOrNull(t => t.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.SingleLineDocumentationCommentTrivia);
var startPosition = firstComment.HasValue ? firstComment.Value.FullSpan.Start : startToken.SpanStart;
var endPosition = endToken.SpanStart;
// TODO (tomescht): Mark the regions to be collapsed by default.
if (startPosition != endPosition)
{
var hintTextEndToken = GetHintTextEndToken(node);
span = new BlockSpan(
isCollapsible: true,
type: BlockTypes.Comment,
textSpan: TextSpan.FromBounds(startPosition, endPosition),
hintSpan: TextSpan.FromBounds(startPosition, hintTextEndToken.Span.End),
bannerText: Ellipsis,
autoCollapse: true);
return true;
}
span = default;
return false;
}
static SyntaxToken GetEndToken(SyntaxNode node)
=> node switch
{
ConstructorDeclarationSyntax constructorDeclaration => constructorDeclaration.Modifiers.FirstOrNull() ?? constructorDeclaration.Identifier,
ConversionOperatorDeclarationSyntax conversionOperatorDeclaration => conversionOperatorDeclaration.Modifiers.FirstOrNull() ?? conversionOperatorDeclaration.ImplicitOrExplicitKeyword,
DelegateDeclarationSyntax delegateDeclaration => delegateDeclaration.Modifiers.FirstOrNull() ?? delegateDeclaration.DelegateKeyword,
DestructorDeclarationSyntax destructorDeclaration => destructorDeclaration.TildeToken,
EnumDeclarationSyntax enumDeclaration => enumDeclaration.Modifiers.FirstOrNull() ?? enumDeclaration.EnumKeyword,
EnumMemberDeclarationSyntax enumMemberDeclaration => enumMemberDeclaration.Identifier,
EventDeclarationSyntax eventDeclaration => eventDeclaration.Modifiers.FirstOrNull() ?? eventDeclaration.EventKeyword,
EventFieldDeclarationSyntax eventFieldDeclaration => eventFieldDeclaration.Modifiers.FirstOrNull() ?? eventFieldDeclaration.EventKeyword,
FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Modifiers.FirstOrNull() ?? fieldDeclaration.Declaration.GetFirstToken(),
IndexerDeclarationSyntax indexerDeclaration => indexerDeclaration.Modifiers.FirstOrNull() ?? indexerDeclaration.Type.GetFirstToken(),
MethodDeclarationSyntax methodDeclaration => methodDeclaration.Modifiers.FirstOrNull() ?? methodDeclaration.ReturnType.GetFirstToken(),
OperatorDeclarationSyntax operatorDeclaration => operatorDeclaration.Modifiers.FirstOrNull() ?? operatorDeclaration.ReturnType.GetFirstToken(),
PropertyDeclarationSyntax propertyDeclaration => propertyDeclaration.Modifiers.FirstOrNull() ?? propertyDeclaration.Type.GetFirstToken(),
TypeDeclarationSyntax typeDeclaration => typeDeclaration.Modifiers.FirstOrNull() ?? typeDeclaration.Keyword,
_ => default
};
static SyntaxToken GetHintTextEndToken(SyntaxNode node)
=> node switch
{
EnumDeclarationSyntax enumDeclaration => enumDeclaration.OpenBraceToken.GetPreviousToken(),
TypeDeclarationSyntax typeDeclaration => typeDeclaration.OpenBraceToken.GetPreviousToken(),
_ => node.GetLastToken()
};
}
private static BlockSpan CreateBlockSpan(
TextSpan textSpan, string bannerText, bool autoCollapse,
string type, bool isCollapsible)
{
return CreateBlockSpan(
textSpan, textSpan, bannerText, autoCollapse, type, isCollapsible, isDefaultCollapsed: false);
}
private static BlockSpan CreateBlockSpan(
TextSpan textSpan, TextSpan hintSpan,
string bannerText, bool autoCollapse,
string type, bool isCollapsible, bool isDefaultCollapsed)
{
return new BlockSpan(
textSpan: textSpan,
hintSpan: hintSpan,
bannerText: bannerText,
autoCollapse: autoCollapse,
type: type,
isCollapsible: isCollapsible,
isDefaultCollapsed: isDefaultCollapsed);
}
public static BlockSpan CreateBlockSpan(
SyntaxNode node, string bannerText, bool autoCollapse,
string type, bool isCollapsible)
{
return CreateBlockSpan(
node.Span,
bannerText,
autoCollapse,
type,
isCollapsible);
}
public static BlockSpan? CreateBlockSpan(
SyntaxNode node, SyntaxToken syntaxToken, bool compressEmptyLines,
string bannerText, bool autoCollapse,
string type, bool isCollapsible)
{
return CreateBlockSpan(
node, syntaxToken, node.GetLastToken(), compressEmptyLines,
bannerText, autoCollapse, type, isCollapsible);
}
public static BlockSpan? CreateBlockSpan(
SyntaxNode node, SyntaxToken startToken,
int spanEndPos, int hintEndPos, string bannerText, bool autoCollapse,
string type, bool isCollapsible)
{
// If the SyntaxToken is actually missing, don't attempt to create an outlining region.
if (startToken.IsMissing)
{
return null;
}
// Since we creating a span for everything after syntaxToken to ensure
// that it collapses properly. However, the hint span begins at the start
// of the next token so indentation in the tooltip is accurate.
var span = TextSpan.FromBounds(GetCollapsibleStart(startToken), spanEndPos);
var hintSpan = GetHintSpan(node, hintEndPos);
return CreateBlockSpan(
span,
hintSpan,
bannerText,
autoCollapse,
type,
isCollapsible,
isDefaultCollapsed: false);
}
private static TextSpan GetHintSpan(SyntaxNode node, int endPos)
{
// Don't include attributes in the BlockSpan for a node. When the user
// hovers over the indent-guide we don't want to show them the line with
// the attributes, we want to show them the line with the start of the
// actual structure.
foreach (var child in node.ChildNodesAndTokens())
{
if (child.Kind() != SyntaxKind.AttributeList)
{
return TextSpan.FromBounds(child.SpanStart, endPos);
}
}
return TextSpan.FromBounds(node.SpanStart, endPos);
}
public static BlockSpan? CreateBlockSpan(
SyntaxNode node, SyntaxToken startToken,
SyntaxToken endToken, bool compressEmptyLines, string bannerText, bool autoCollapse,
string type, bool isCollapsible)
{
var (spanEnd, hintEnd) = GetCollapsibleEnd(endToken, compressEmptyLines);
return CreateBlockSpan(
node, startToken, spanEnd, hintEnd,
bannerText, autoCollapse, type, isCollapsible);
}
public static BlockSpan CreateBlockSpan(
SyntaxNode node, bool autoCollapse, string type, bool isCollapsible)
{
return CreateBlockSpan(
node,
bannerText: Ellipsis,
autoCollapse: autoCollapse,
type: type,
isCollapsible: isCollapsible);
}
// Adds everything after 'syntaxToken' up to and including the end
// of node as a region. The snippet to display is just "..."
public static BlockSpan? CreateBlockSpan(
SyntaxNode node, SyntaxToken syntaxToken, bool compressEmptyLines,
bool autoCollapse, string type, bool isCollapsible)
{
return CreateBlockSpan(
node, syntaxToken, compressEmptyLines,
bannerText: Ellipsis,
autoCollapse: autoCollapse,
type: type,
isCollapsible: isCollapsible);
}
// Adds everything after 'syntaxToken' up to and including the end
// of node as a region. The snippet to display is just "..."
public static BlockSpan? CreateBlockSpan(
SyntaxNode node, SyntaxToken startToken, SyntaxToken endToken, bool compressEmptyLines,
bool autoCollapse, string type, bool isCollapsible)
{
return CreateBlockSpan(
node, startToken, endToken, compressEmptyLines,
bannerText: Ellipsis,
autoCollapse: autoCollapse,
type: type,
isCollapsible: isCollapsible);
}
// Adds the span surrounding the syntax list as a region. The
// snippet shown is the text from the first line of the first
// node in the list.
public static BlockSpan? CreateBlockSpan(
IEnumerable<SyntaxNode> syntaxList, bool compressEmptyLines, bool autoCollapse,
string type, bool isCollapsible, bool isDefaultCollapsed)
{
if (syntaxList.IsEmpty())
{
return null;
}
var (end, hintEnd) = GetCollapsibleEnd(syntaxList.Last().GetLastToken(), compressEmptyLines);
var spanStart = syntaxList.First().GetFirstToken().FullSpan.End;
var spanEnd = end >= spanStart
? end
: spanStart;
var hintSpanStart = syntaxList.First().SpanStart;
var hintSpanEnd = hintEnd >= hintSpanStart
? hintEnd
: hintSpanStart;
return CreateBlockSpan(
textSpan: TextSpan.FromBounds(spanStart, spanEnd),
hintSpan: TextSpan.FromBounds(hintSpanStart, hintSpanEnd),
bannerText: Ellipsis,
autoCollapse: autoCollapse,
type: type,
isCollapsible: isCollapsible,
isDefaultCollapsed: isDefaultCollapsed);
}
}
|