|
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Formatting;
internal static class FormattingExtensions
{
public static SyntaxNode GetParentWithBiggerSpan(this SyntaxNode node)
{
if (node.Parent == null)
{
return node;
}
if (node.Parent.Span != node.Span)
{
return node.Parent;
}
return GetParentWithBiggerSpan(node.Parent);
}
public static IEnumerable<AbstractFormattingRule> Concat(this AbstractFormattingRule rule, IEnumerable<AbstractFormattingRule> rules)
=> [rule, .. rules];
[return: NotNullIfNotNull(nameof(list1)), NotNullIfNotNull(nameof(list2))]
public static List<T>? Combine<T>(this List<T>? list1, List<T>? list2)
=> (list1, list2) switch
{
(null, _) => list2,
(_, null) => list1,
// normal case
_ => [.. list1, .. list2]
};
public static bool ContainsElasticTrivia(this SuppressOperation operation, TokenStream tokenStream)
{
var startToken = tokenStream.GetTokenData(operation.StartToken);
var nextToken = startToken.GetNextTokenData();
var endToken = tokenStream.GetTokenData(operation.EndToken);
var previousToken = endToken.GetPreviousTokenData();
return CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(startToken.Token, nextToken.Token) ||
CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(previousToken.Token, endToken.Token);
}
public static bool HasAnyWhitespaceElasticTrivia(this SyntaxTriviaList list)
{
// Use foreach to avoid accessing indexer as it will call GetSlotOffset for each trivia
foreach (var trivia in list)
{
if (trivia.IsElastic())
return true;
}
return false;
}
public static bool IsOn(this IndentBlockOption option, IndentBlockOption flag)
=> (option & flag) == flag;
public static bool IsMaskOn(this IndentBlockOption option, IndentBlockOption mask)
=> (option & mask) != 0x0;
public static bool IsOn(this SuppressOption option, SuppressOption flag)
=> (option & flag) == flag;
public static bool IsMaskOn(this SuppressOption option, SuppressOption mask)
=> (option & mask) != 0x0;
public static SuppressOption RemoveFlag(this SuppressOption option, SuppressOption flag)
=> option & ~flag;
public static string CreateIndentationString(this int desiredIndentation, bool useTab, int tabSize)
{
var numberOfTabs = 0;
var numberOfSpaces = Math.Max(0, desiredIndentation);
if (useTab)
{
numberOfTabs = desiredIndentation / tabSize;
numberOfSpaces -= numberOfTabs * tabSize;
}
return new string('\t', numberOfTabs) + new string(' ', numberOfSpaces);
}
public static StringBuilder AppendIndentationString(this StringBuilder sb, int desiredIndentation, bool useTab, int tabSize)
{
var numberOfTabs = 0;
var numberOfSpaces = Math.Max(0, desiredIndentation);
if (useTab)
{
numberOfTabs = desiredIndentation / tabSize;
numberOfSpaces -= numberOfTabs * tabSize;
}
return sb.Append('\t', repeatCount: numberOfTabs).Append(' ', repeatCount: numberOfSpaces);
}
public static void ProcessTextBetweenTokens(
this string text,
TreeData treeInfo,
SyntaxToken baseToken,
int tabSize,
out int lineBreaks,
out int spaceOrIndentation)
{
// initialize out param
lineBreaks = text.GetNumberOfLineBreaks();
// multiple line case
if (lineBreaks > 0)
{
var indentationString = text.GetLastLineText();
spaceOrIndentation = indentationString.GetColumnFromLineOffset(indentationString.Length, tabSize);
return;
}
// with tab, more expensive way. get column of token1 and then calculate right space amount
var initialColumn = baseToken.RawKind == 0 ? 0 /* the very beginning of the file */ : treeInfo.GetOriginalColumn(tabSize, baseToken);
spaceOrIndentation = text.ConvertTabToSpace(tabSize, baseToken.ToString().GetTextColumn(tabSize, initialColumn), text.Length);
}
private static readonly char[] s_trimChars = ['\r', '\n'];
public static string AdjustIndentForXmlDocExteriorTrivia(
this string triviaText,
bool forceIndentation,
int indentation,
int indentationDelta,
bool useTab,
int tabSize)
{
var isEmptyString = false;
var builder = StringBuilderPool.Allocate();
var nonWhitespaceCharIndex = GetFirstNonWhitespaceIndexInString(triviaText);
if (nonWhitespaceCharIndex == -1)
{
isEmptyString = true;
nonWhitespaceCharIndex = triviaText.Length;
}
var newIndentation = GetNewIndentationForComments(triviaText, nonWhitespaceCharIndex, forceIndentation, indentation, indentationDelta, tabSize);
builder.AppendIndentationString(newIndentation, useTab, tabSize);
if (!isEmptyString)
{
builder.Append(triviaText, nonWhitespaceCharIndex, triviaText.Length - nonWhitespaceCharIndex);
}
return StringBuilderPool.ReturnAndFree(builder);
}
public static string ReindentStartOfXmlDocumentationComment(
this string triviaText,
bool forceIndentation,
int indentation,
int indentationDelta,
bool useTab,
int tabSize,
string newLine)
{
var builder = StringBuilderPool.Allocate();
// split xml doc comments into lines
var lines = triviaText.Split('\n');
Contract.ThrowIfFalse(lines.Length > 0);
// add first line and append new line iff it is not a single line xml doc comment
builder.Append(lines[0].Trim(s_trimChars));
if (0 < lines.Length - 1)
{
builder.Append(newLine);
}
// add rest of xml doc comments
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i].TrimEnd(s_trimChars);
var nonWhitespaceCharIndex = GetFirstNonWhitespaceIndexInString(line);
if (nonWhitespaceCharIndex >= 0)
{
var newIndentation = GetNewIndentationForComments(line, nonWhitespaceCharIndex, forceIndentation, indentation, indentationDelta, tabSize);
builder.AppendIndentationString(newIndentation, useTab, tabSize);
builder.Append(line, nonWhitespaceCharIndex, line.Length - nonWhitespaceCharIndex);
}
if (i < lines.Length - 1)
{
builder.Append(newLine);
}
}
return StringBuilderPool.ReturnAndFree(builder);
}
private static int GetNewIndentationForComments(this string line, int nonWhitespaceCharIndex, bool forceIndentation, int indentation, int indentationDelta, int tabSize)
{
if (forceIndentation)
{
return indentation;
}
var currentIndentation = line.GetColumnFromLineOffset(nonWhitespaceCharIndex, tabSize);
return Math.Max(currentIndentation + indentationDelta, 0);
}
public static int GetFirstNonWhitespaceIndexInString(this string text)
{
for (var i = 0; i < text.Length; i++)
{
if (text[i] is not ' ' and not '\t')
{
return i;
}
}
return -1;
}
public static TextChange SimpleDiff(this TextChange textChange, string text)
{
var span = textChange.Span;
var newText = textChange.NewText ?? "";
var i = 0;
for (; i < span.Length; i++)
{
if (i >= newText.Length || text[i] != newText[i])
{
break;
}
}
// two texts are exactly same
if (i == span.Length && text.Length == newText.Length)
{
// don't do anything
return textChange;
}
if (i > 0)
{
span = new TextSpan(span.Start + i, span.Length - i);
newText = newText[i..];
}
return new TextChange(span, newText);
}
internal static IEnumerable<TextSpan> GetAnnotatedSpans(SyntaxNode node, SyntaxAnnotation annotation)
{
if (annotation == SyntaxAnnotation.ElasticAnnotation)
{
var tokens = node.GetAnnotatedTrivia(SyntaxAnnotation.ElasticAnnotation).Select(tr => tr.Token).Distinct();
return AggregateSpans(tokens.Select(GetElasticSpan));
}
return EnumerateAnnotatedSpans(node, annotation);
static IEnumerable<TextSpan> EnumerateAnnotatedSpans(SyntaxNode node, SyntaxAnnotation annotation)
{
foreach (var nodeOrToken in node.GetAnnotatedNodesAndTokens(annotation))
{
var (firstToken, lastToken) = nodeOrToken.AsNode(out var childNode)
? (childNode.GetFirstToken(includeZeroWidth: true), childNode.GetLastToken(includeZeroWidth: true))
: (nodeOrToken.AsToken(), nodeOrToken.AsToken());
yield return GetSpan(firstToken, lastToken);
}
}
}
internal static TextSpan GetSpan(SyntaxToken firstToken, SyntaxToken lastToken)
{
var previousToken = firstToken.GetPreviousToken();
var nextToken = lastToken.GetNextToken();
if (previousToken.RawKind != 0)
{
firstToken = previousToken;
}
if (nextToken.RawKind != 0)
{
lastToken = nextToken;
}
return TextSpan.FromBounds(firstToken.SpanStart, lastToken.Span.End);
}
internal static TextSpan GetElasticSpan(SyntaxToken token)
=> GetSpan(token, token);
private static IEnumerable<TextSpan> AggregateSpans(IEnumerable<TextSpan> spans)
{
var aggregateSpans = new List<TextSpan>();
var last = default(TextSpan);
foreach (var span in spans)
{
if (last == default)
{
last = span;
}
else if (span.IntersectsWith(last))
{
last = TextSpan.FromBounds(last.Start, span.End);
}
else
{
aggregateSpans.Add(last);
last = span;
}
}
if (last != default)
{
aggregateSpans.Add(last);
}
return aggregateSpans;
}
internal static int GetAdjustedIndentationDelta(
this IndentBlockOperation operation, IHeaderFacts headerFacts, SyntaxNode root, SyntaxToken indentationAnchor)
{
if (operation.Option.IsOn(IndentBlockOption.AbsolutePosition))
{
// Absolute positioning is absolute
return operation.IndentationDeltaOrPosition;
}
if (!operation.Option.IsOn(IndentBlockOption.IndentIfConditionOfAnchorToken))
{
// No adjustment operations are being applied
return operation.IndentationDeltaOrPosition;
}
// Consider syntax forms similar to the following:
//
// if (conditionLine1
// conditionLine2)
//
// Adjustments may be requested for conditionLine2 in cases where the anchor for relative indentation is the
// first token of the containing statement (in this case, the 'if' token).
if (headerFacts.IsOnIfStatementHeader(root, operation.BaseToken.SpanStart, out var conditionStatement)
|| headerFacts.IsOnWhileStatementHeader(root, operation.BaseToken.SpanStart, out conditionStatement))
{
if (conditionStatement.GetFirstToken() == indentationAnchor)
{
// The node is located within the condition of a conditional block statement (or
// syntactically-similar), uses a relative anchor to the block statement, and has requested an
// additional indentation adjustment for this case.
return operation.IndentationDeltaOrPosition + 1;
}
}
// No adjustments were necessary/applicable
return operation.IndentationDeltaOrPosition;
}
}
|