|
// 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.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Formatting;
internal abstract class AbstractTriviaFormatter
{
#region Caches
private static readonly string[] s_spaceCache;
/// <summary>
/// set up space string caches
/// </summary>
static AbstractTriviaFormatter()
{
s_spaceCache = new string[20];
for (var i = 0; i < 20; i++)
{
s_spaceCache[i] = new string(' ', i);
}
}
#endregion
/// <summary>
/// format the trivia at the line column and put changes to the changes
/// </summary>
private delegate LineColumnDelta Formatter<T>(LineColumn lineColumn, SyntaxTrivia trivia, ArrayBuilder<T> changes, CancellationToken cancellationToken);
/// <summary>
/// create whitespace for the delta at the line column and put changes to the changes
/// </summary>
private delegate void WhitespaceAppender<T>(LineColumn lineColumn, LineColumnDelta delta, TextSpan span, ArrayBuilder<T> changes);
protected readonly FormattingContext Context;
protected readonly ChainedFormattingRules FormattingRules;
protected readonly string OriginalString;
protected readonly int LineBreaks;
protected readonly int Spaces;
protected readonly LineColumn InitialLineColumn;
protected readonly SyntaxToken Token1;
protected readonly SyntaxToken Token2;
private readonly int _indentation;
private readonly bool _firstLineBlank;
public AbstractTriviaFormatter(
FormattingContext context,
ChainedFormattingRules formattingRules,
SyntaxToken token1,
SyntaxToken token2,
string originalString,
int lineBreaks,
int spaces)
{
Contract.ThrowIfNull(context);
Contract.ThrowIfNull(formattingRules);
Contract.ThrowIfNull(originalString);
Contract.ThrowIfFalse(lineBreaks >= 0);
Contract.ThrowIfFalse(spaces >= 0);
Contract.ThrowIfTrue(token1 == default && token2 == default);
this.Context = context;
this.FormattingRules = formattingRules;
this.OriginalString = originalString;
this.Token1 = token1;
this.Token2 = token2;
this.LineBreaks = lineBreaks;
this.Spaces = spaces;
this.InitialLineColumn = GetInitialLineColumn();
// "Spaces" holds either space counts between two tokens if two are on same line or indentation of token2 if
// two are on different line. but actual "Indentation" of the line could be different than "Spaces" if there is
// noisy trivia before token2 on the same line.
// this.indentation indicates that trivia's indentation
//
// ex) [indentation]/** */ token2
// [spaces ]
_indentation = (this.LineBreaks > 0) ? GetIndentation() : -1;
// check whether first line between two tokens contains only whitespace
// depends on this we decide where to insert blank lines at the end
_firstLineBlank = FirstLineBlank();
}
/// <summary>
/// return whether this formatting succeeded or not
/// for example, if there is skipped tokens in one of trivia between tokens
/// we consider formatting this region is failed
/// </summary>
protected abstract bool Succeeded();
/// <summary>
/// check whether given trivia is whitespace trivia or not
/// </summary>
protected abstract bool IsWhitespace(SyntaxTrivia trivia);
/// <summary>
/// check whether given trivia is end of line trivia or not
/// </summary>
protected abstract bool IsEndOfLine(SyntaxTrivia trivia);
/// <summary>
/// true if previoustrivia is _ and nextTrivia is a Visual Basic comment
/// </summary>
protected abstract bool LineContinuationFollowedByWhitespaceComment(SyntaxTrivia previousTrivia, SyntaxTrivia nextTrivia);
/// <summary>
/// check whether given trivia is a Comment in VB or not
/// It is never reachable in C# since it follows a test for
/// LineContinuation Character.
/// </summary>
protected abstract bool IsVisualBasicComment(SyntaxTrivia trivia);
/// <summary>
/// check whether given string is either null or whitespace
/// </summary>
protected bool IsNullOrWhitespace([NotNullWhen(true)] string? text)
{
if (text == null)
{
return true;
}
for (var i = 0; i < text.Length; i++)
{
if (!IsWhitespace(text[i]) || !IsNewLine(text[i]))
{
return false;
}
}
return true;
}
/// <summary>
/// check whether given char is whitespace
/// </summary>
protected abstract bool IsWhitespace(char ch);
/// <summary>
/// check whether given char is new line char
/// </summary>
protected abstract bool IsNewLine(char ch);
/// <summary>
/// create whitespace trivia
/// </summary>
protected abstract SyntaxTrivia CreateWhitespace(string text);
/// <summary>
/// create end of line trivia
/// </summary>
protected abstract SyntaxTrivia CreateEndOfLine();
/// <summary>
/// return line column rule for the given two trivia
/// </summary>
protected abstract LineColumnRule GetLineColumnRuleBetween(SyntaxTrivia trivia1, LineColumnDelta existingWhitespaceBetween, bool implicitLineBreak, SyntaxTrivia trivia2, CancellationToken cancellationToken);
/// <summary>
/// format the given trivia at the line column position and put result to the changes list
/// </summary>
protected abstract LineColumnDelta Format(LineColumn lineColumn, SyntaxTrivia trivia, ArrayBuilder<SyntaxTrivia> changes, CancellationToken cancellationToken);
/// <summary>
/// format the given trivia at the line column position and put text change result to the changes list
/// </summary>
protected abstract LineColumnDelta Format(LineColumn lineColumn, SyntaxTrivia trivia, ArrayBuilder<TextChange> changes, CancellationToken cancellationToken);
/// <summary>
/// returns true if the trivia contains a Line break
/// </summary>
protected abstract bool ContainsImplicitLineBreak(SyntaxTrivia trivia);
protected int StartPosition
{
get
{
if (this.Token1.RawKind == 0)
{
return this.TreeInfo.StartPosition;
}
return this.Token1.Span.End;
}
}
protected int EndPosition
{
get
{
if (this.Token2.RawKind == 0)
{
return this.TreeInfo.EndPosition;
}
return this.Token2.SpanStart;
}
}
protected TreeData TreeInfo
{
get { return this.Context.TreeData; }
}
protected SyntaxFormattingOptions Options
{
get { return this.Context.Options; }
}
protected TokenStream TokenStream
{
get { return this.Context.TokenStream; }
}
public SyntaxTriviaList FormatToSyntaxTrivia(CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<SyntaxTrivia>.GetInstance(out var triviaList);
var lineColumn = FormatTrivia(Format, AddWhitespaceTrivia, triviaList, cancellationToken);
// deal with edges
// insert empty linebreaks at the beginning of trivia list
AddExtraLines(lineColumn.Line, triviaList);
if (Succeeded())
{
return new SyntaxTriviaList(triviaList);
}
triviaList.Clear();
AddRange(triviaList, this.Token1.TrailingTrivia);
AddRange(triviaList, this.Token2.LeadingTrivia);
return new SyntaxTriviaList(triviaList);
}
private static void AddRange(ArrayBuilder<SyntaxTrivia> result, SyntaxTriviaList triviaList)
{
foreach (var trivia in triviaList)
result.Add(trivia);
}
public ImmutableArray<TextChange> FormatToTextChanges(CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<TextChange>.GetInstance(out var changes);
var lineColumn = FormatTrivia(Format, AddWhitespaceTextChange, changes, cancellationToken);
// deal with edges
// insert empty linebreaks at the beginning of trivia list
AddExtraLines(lineColumn.Line, changes);
if (Succeeded())
{
return changes.ToImmutableAndClear();
}
return [];
}
private LineColumn FormatTrivia<T>(Formatter<T> formatter, WhitespaceAppender<T> whitespaceAdder, ArrayBuilder<T> changes, CancellationToken cancellationToken)
{
var lineColumn = this.InitialLineColumn;
var existingWhitespaceDelta = LineColumnDelta.Default;
var previousWhitespaceTrivia = default(SyntaxTrivia);
var previousTrivia = default(SyntaxTrivia);
var implicitLineBreak = false;
var list = new TriviaList(this.Token1.TrailingTrivia, this.Token2.LeadingTrivia);
// Holds last position before _ ' Comment so we can reset after processing comment
var previousLineColumn = LineColumn.Default;
SyntaxTrivia trivia;
for (var i = 0; i < list.Count; i++)
{
trivia = list[i];
if (trivia.RawKind == 0)
{
continue;
}
if (IsWhitespaceOrEndOfLine(trivia))
{
existingWhitespaceDelta = existingWhitespaceDelta.With(
GetLineColumnOfWhitespace(
lineColumn,
previousTrivia,
previousWhitespaceTrivia,
existingWhitespaceDelta,
trivia));
if (IsEndOfLine(trivia))
{
implicitLineBreak = false;
// If we are on a new line we don't want to continue
// reseting indenting this handles the case of a NewLine
// followed by whitespace and a comment
previousLineColumn = LineColumn.Default;
}
else if (LineContinuationFollowedByWhitespaceComment(previousTrivia, (i + 1) < list.Count ? list[i + 1] : default))
{
// we have a comment following an underscore space the formatter
// thinks this next line should be shifted to right by
// indentation value. Since we know through the test above that
// this is the special case of _ ' Comment we don't want the extra indent
// so we set the LineColumn value back to where it was before the comment
previousLineColumn = lineColumn;
}
previousWhitespaceTrivia = trivia;
continue;
}
previousWhitespaceTrivia = default;
lineColumn = FormatFirstTriviaAndWhitespaceAfter(
lineColumn,
previousTrivia, existingWhitespaceDelta, trivia,
formatter, whitespaceAdder,
changes, implicitLineBreak, cancellationToken);
if (previousLineColumn.Column != 0
&& previousLineColumn.Column < lineColumn.Column
&& IsVisualBasicComment(trivia))
{
lineColumn = previousLineColumn;
// When we see a NewLine we don't want any special handling
// for _ ' Comment
previousLineColumn = LineColumn.Default;
}
implicitLineBreak = implicitLineBreak || ContainsImplicitLineBreak(trivia);
existingWhitespaceDelta = LineColumnDelta.Default;
previousTrivia = trivia;
}
lineColumn = FormatFirstTriviaAndWhitespaceAfter(
lineColumn,
previousTrivia, existingWhitespaceDelta, default,
formatter, whitespaceAdder,
changes, implicitLineBreak, cancellationToken);
return lineColumn;
}
private LineColumn FormatFirstTriviaAndWhitespaceAfter<T>(
LineColumn lineColumnBeforeTrivia1,
SyntaxTrivia trivia1,
LineColumnDelta existingWhitespaceBetween,
SyntaxTrivia trivia2,
Formatter<T> format,
WhitespaceAppender<T> addWhitespaceTrivia,
ArrayBuilder<T> changes,
bool implicitLineBreak,
CancellationToken cancellationToken)
{
var lineColumnAfterTrivia1 = trivia1.RawKind == 0 ?
lineColumnBeforeTrivia1 : lineColumnBeforeTrivia1.With(format(lineColumnBeforeTrivia1, trivia1, changes, cancellationToken));
var rule = GetOverallLineColumnRuleBetween(trivia1, existingWhitespaceBetween, implicitLineBreak, trivia2, cancellationToken);
var whitespaceDelta = Apply(lineColumnBeforeTrivia1, trivia1, lineColumnAfterTrivia1, existingWhitespaceBetween, trivia2, rule);
var span = GetTextSpan(trivia1, trivia2);
addWhitespaceTrivia(lineColumnAfterTrivia1, whitespaceDelta, span, changes);
return lineColumnAfterTrivia1.With(whitespaceDelta);
}
/// <summary>
/// get line column rule between two trivia
/// </summary>
private LineColumnRule GetOverallLineColumnRuleBetween(SyntaxTrivia trivia1, LineColumnDelta existingWhitespaceBetween, bool implicitLineBreak, SyntaxTrivia trivia2, CancellationToken cancellationToken)
{
var defaultRule = GetLineColumnRuleBetween(trivia1, existingWhitespaceBetween, implicitLineBreak, trivia2, cancellationToken);
GetTokensAtEdgeOfStructureTrivia(trivia1, trivia2, out var token1, out var token2);
// if there are tokens, try formatting rules to see whether there is a user supplied one
if (token1.RawKind == 0 || token2.RawKind == 0)
{
return defaultRule;
}
// use line defined by the token formatting rules
var lineOperation = this.FormattingRules.GetAdjustNewLinesOperation(token1, token2);
// there is existing lines, but no line operation
if (existingWhitespaceBetween.Lines != 0 && lineOperation == null)
{
return defaultRule;
}
if (lineOperation != null)
{
switch (lineOperation.Option)
{
case AdjustNewLinesOption.PreserveLines:
if (existingWhitespaceBetween.Lines != 0)
{
return defaultRule.With(lines: lineOperation.Line, lineOperation: LineColumnRule.LineOperations.Preserve);
}
break;
case AdjustNewLinesOption.ForceLines:
return defaultRule.With(lines: lineOperation.Line, lineOperation: LineColumnRule.LineOperations.Force);
case AdjustNewLinesOption.ForceLinesIfOnSingleLine:
if (this.Context.TokenStream.TwoTokensOnSameLine(token1, token2))
{
return defaultRule.With(lines: lineOperation.Line, lineOperation: LineColumnRule.LineOperations.Force);
}
break;
default:
throw ExceptionUtilities.UnexpectedValue(lineOperation.Option);
}
}
// use space defined by the regular formatting rules
var spaceOperation = this.FormattingRules.GetAdjustSpacesOperation(token1, token2);
if (spaceOperation == null)
{
return defaultRule;
}
if (spaceOperation.Option == AdjustSpacesOption.DefaultSpacesIfOnSingleLine &&
spaceOperation.Space == 1)
{
return defaultRule;
}
return defaultRule.With(spaces: spaceOperation.Space);
}
/// <summary>
/// if the given trivia is the very first or the last trivia between two normal tokens and
/// if the trivia is structured trivia, get one token that belongs to the structured trivia and one belongs to the normal token stream
/// </summary>
private void GetTokensAtEdgeOfStructureTrivia(SyntaxTrivia trivia1, SyntaxTrivia trivia2, out SyntaxToken token1, out SyntaxToken token2)
{
token1 = default;
if (trivia1.RawKind == 0)
{
token1 = this.Token1;
}
else if (trivia1.HasStructure)
{
var lastToken = trivia1.GetStructure()!.GetLastToken(includeZeroWidth: true);
if (ContainsOnlyWhitespace(lastToken.Span.End, lastToken.FullSpan.End))
{
token1 = lastToken;
}
}
token2 = default;
if (trivia2.RawKind == 0)
{
token2 = this.Token2;
}
else if (trivia2.HasStructure)
{
var firstToken = trivia2.GetStructure()!.GetFirstToken(includeZeroWidth: true);
if (ContainsOnlyWhitespace(firstToken.FullSpan.Start, firstToken.SpanStart))
{
token2 = firstToken;
}
}
}
/// <summary>
/// check whether string between start and end position only contains whitespace
/// </summary>
private bool ContainsOnlyWhitespace(int start, int end)
{
var span = TextSpan.FromBounds(start, end);
for (var i = span.Start - this.Token1.Span.End; i < span.Length; i++)
{
if (!char.IsWhiteSpace(this.OriginalString[i]))
{
return false;
}
}
return true;
}
/// <summary>
/// check whether first line between two tokens contains only whitespace
/// </summary>
private bool FirstLineBlank()
{
// if we see elastic trivia as the first trivia in the trivia list,
// we consider it as blank line
if (this.Token1.TrailingTrivia.Count > 0 &&
this.Token1.TrailingTrivia[0].IsElastic())
{
return true;
}
var index = this.OriginalString.IndexOf(this.IsNewLine);
if (index < 0)
{
return IsNullOrWhitespace(this.OriginalString);
}
for (var i = 0; i < index; i++)
{
if (!IsWhitespace(this.OriginalString[i]))
{
return false;
}
}
return true;
}
private LineColumnDelta Apply(
LineColumn lineColumnBeforeTrivia1, SyntaxTrivia trivia1, LineColumn lineColumnAfterTrivia1, LineColumnDelta existingWhitespaceBetween, SyntaxTrivia trivia2, LineColumnRule rule)
{
// we do not touch spaces adjacent to missing token
// [missing token] [whitespace] [trivia] or [trivia] [whitespace] [missing token] case
if ((this.Token1.IsMissing && trivia1.RawKind == 0) ||
(trivia2.RawKind == 0 && this.Token2.IsMissing))
{
// leave things as it is
return existingWhitespaceBetween;
}
var lines = GetRuleLines(rule, lineColumnAfterTrivia1, existingWhitespaceBetween);
var spaceOrIndentations = GetRuleSpacesOrIndentation(lineColumnBeforeTrivia1, lineColumnAfterTrivia1, existingWhitespaceBetween, trivia2, rule);
return new LineColumnDelta(
lines,
spaceOrIndentations,
whitespaceOnly: true,
forceUpdate: existingWhitespaceBetween.ForceUpdate);
}
private int GetRuleSpacesOrIndentation(
LineColumn lineColumnBeforeTrivia1, LineColumn lineColumnAfterTrivia1, LineColumnDelta existingWhitespaceBetween, SyntaxTrivia trivia2, LineColumnRule rule)
{
var lineColumnAfterExistingWhitespace = lineColumnAfterTrivia1.With(existingWhitespaceBetween);
// next trivia is moved to next line or already on a new line, use indentation
if (rule.Lines > 0 || lineColumnAfterExistingWhitespace.WhitespaceOnly)
{
return rule.IndentationOperation switch
{
LineColumnRule.IndentationOperations.Absolute => Math.Max(0, rule.Indentation),
LineColumnRule.IndentationOperations.Default => this.Context.GetBaseIndentation(trivia2.RawKind == 0 ? this.EndPosition : trivia2.SpanStart),
LineColumnRule.IndentationOperations.Given => (trivia2.RawKind == 0) ? this.Spaces : Math.Max(0, _indentation),
LineColumnRule.IndentationOperations.Follow => Math.Max(0, lineColumnBeforeTrivia1.Column),
LineColumnRule.IndentationOperations.Preserve => existingWhitespaceBetween.Spaces,
_ => throw ExceptionUtilities.UnexpectedValue(rule.IndentationOperation),
};
}
// okay, we are not on a its own line, use space information
return rule.SpaceOperation switch
{
LineColumnRule.SpaceOperations.Preserve => Math.Max(rule.Spaces, existingWhitespaceBetween.Spaces),
LineColumnRule.SpaceOperations.Force => Math.Max(rule.Spaces, 0),
_ => throw ExceptionUtilities.UnexpectedValue(rule.SpaceOperation),
};
}
private static int GetRuleLines(LineColumnRule rule, LineColumn lineColumnAfterTrivia1, LineColumnDelta existingWhitespaceBetween)
{
var adjustedRuleLines = Math.Max(0, rule.Lines - GetTrailingLinesAtEndOfTrivia1(lineColumnAfterTrivia1));
return (rule.LineOperation == LineColumnRule.LineOperations.Preserve) ? Math.Max(adjustedRuleLines, existingWhitespaceBetween.Lines) : adjustedRuleLines;
}
private int GetIndentation()
{
var lastText = this.OriginalString.GetLastLineText();
var initialColumn = (lastText == this.OriginalString) ? this.InitialLineColumn.Column : 0;
var index = lastText.GetFirstNonWhitespaceIndexInString();
if (index < 0)
{
return this.Spaces;
}
var position = lastText.ConvertTabToSpace(Options.TabSize, initialColumn, index);
var tokenPosition = lastText.ConvertTabToSpace(Options.TabSize, initialColumn, lastText.Length);
return this.Spaces - (tokenPosition - position);
}
/// <summary>
/// return 0 or 1 based on line column of the trivia1's end point
/// this is based on our structured trivia's implementation detail that some structured trivia can have
/// one new line at the end of the trivia
/// </summary>
private static int GetTrailingLinesAtEndOfTrivia1(LineColumn lineColumnAfterTrivia1)
=> (lineColumnAfterTrivia1.Column == 0 && lineColumnAfterTrivia1.Line > 0) ? 1 : 0;
private void AddExtraLines(int linesBetweenTokens, ArrayBuilder<SyntaxTrivia> changes)
{
if (linesBetweenTokens < this.LineBreaks)
{
using var _ = ArrayBuilder<SyntaxTrivia>.GetInstance(out var lineBreaks);
AddWhitespaceTrivia(
LineColumn.Default,
new LineColumnDelta(lines: this.LineBreaks - linesBetweenTokens, spaces: 0),
lineBreaks);
var insertionIndex = GetInsertionIndex(changes);
for (var i = lineBreaks.Count - 1; i >= 0; i--)
changes.Insert(insertionIndex, lineBreaks[i]);
}
}
private int GetInsertionIndex(ArrayBuilder<SyntaxTrivia> changes)
{
// first line is blank or there is no changes.
// just insert at the head
if (_firstLineBlank ||
changes.Count == 0)
{
return 0;
}
// try to find end of line
for (var i = changes.Count - 1; i >= 0; i--)
{
// insert right after existing end of line trivia
if (IsEndOfLine(changes[i]))
{
return i + 1;
}
}
// can't find any line, put blank line right after any trivia that has lines in them
for (var i = changes.Count - 1; i >= 0; i--)
{
if (changes[i].ToFullString().ContainsLineBreak())
{
return i + 1;
}
}
// well, give up and insert at the top
return 0;
}
private void AddExtraLines(int linesBetweenTokens, ArrayBuilder<TextChange> changes)
{
if (linesBetweenTokens >= this.LineBreaks)
{
return;
}
if (changes.Count == 0)
{
AddWhitespaceTextChange(
LineColumn.Default, new LineColumnDelta(lines: this.LineBreaks - linesBetweenTokens, spaces: 0),
GetInsertionSpan(changes), changes);
return;
}
if (TryGetMatchingChangeIndex(changes, out var index))
{
// already change exist at same position that contains only whitespace
var delta = GetLineColumnDelta(0, changes[index].NewText ?? "");
changes[index] = GetWhitespaceTextChange(
LineColumn.Default,
new LineColumnDelta(lines: this.LineBreaks + delta.Lines - linesBetweenTokens, spaces: delta.Spaces),
changes[index].Span);
return;
}
else
{
var change = GetWhitespaceTextChange(
LineColumn.Default,
new LineColumnDelta(lines: this.LineBreaks - linesBetweenTokens, spaces: 0),
GetInsertionSpan(changes));
changes.Insert(0, change);
return;
}
}
private bool TryGetMatchingChangeIndex(ArrayBuilder<TextChange> changes, out int index)
{
index = -1;
var insertionPoint = GetInsertionSpan(changes);
for (var i = 0; i < changes.Count; i++)
{
var change = changes[i];
if (change.Span.Contains(insertionPoint) && IsNullOrWhitespace(change.NewText))
{
index = i;
return true;
}
}
return false;
}
private TextSpan GetInsertionSpan(ArrayBuilder<TextChange> changes)
{
// first line is blank or there is no changes.
// just insert at the head
if (_firstLineBlank ||
changes.Count == 0)
{
return new TextSpan(this.StartPosition, 0);
}
// try to find end of line
for (var i = this.OriginalString.Length - 1; i >= 0; i--)
{
if (this.OriginalString[i] == '\n')
{
return new TextSpan(Math.Min(this.StartPosition + i + 1, this.EndPosition), 0);
}
}
// well, give up and insert at the top
Debug.Assert(!_firstLineBlank);
return new TextSpan(this.EndPosition, 0);
}
private void AddWhitespaceTrivia(
LineColumn lineColumn,
LineColumnDelta delta,
ArrayBuilder<SyntaxTrivia> changes)
{
AddWhitespaceTrivia(lineColumn, delta, default, changes);
}
private void AddWhitespaceTrivia(
LineColumn lineColumn,
LineColumnDelta delta,
TextSpan notUsed,
ArrayBuilder<SyntaxTrivia> changes)
{
if (delta.Lines == 0 && delta.Spaces == 0)
{
// remove trivia
return;
}
for (var i = 0; i < delta.Lines; i++)
{
changes.Add(CreateEndOfLine());
}
if (delta.Spaces == 0)
{
return;
}
// space indicates indentation
if (delta.Lines > 0 || lineColumn.Column == 0)
{
changes.Add(CreateWhitespace(delta.Spaces.CreateIndentationString(Options.UseTabs, Options.TabSize)));
return;
}
// space indicates space between two noisy trivia or tokens
changes.Add(CreateWhitespace(GetSpaces(delta.Spaces)));
}
private string GetWhitespaceString(LineColumn lineColumn, LineColumnDelta delta)
{
var sb = StringBuilderPool.Allocate();
var newLine = Options.NewLine;
for (var i = 0; i < delta.Lines; i++)
{
sb.Append(newLine);
}
if (delta.Spaces == 0)
{
return StringBuilderPool.ReturnAndFree(sb);
}
// space indicates indentation
if (delta.Lines > 0 || lineColumn.Column == 0)
{
sb.AppendIndentationString(delta.Spaces, Options.UseTabs, Options.TabSize);
return StringBuilderPool.ReturnAndFree(sb);
}
// space indicates space between two noisy trivia or tokens
sb.Append(' ', repeatCount: delta.Spaces);
return StringBuilderPool.ReturnAndFree(sb);
}
private TextChange GetWhitespaceTextChange(LineColumn lineColumn, LineColumnDelta delta, TextSpan span)
=> new(span, GetWhitespaceString(lineColumn, delta));
private void AddWhitespaceTextChange(LineColumn lineColumn, LineColumnDelta delta, TextSpan span, ArrayBuilder<TextChange> changes)
{
var newText = GetWhitespaceString(lineColumn, delta);
changes.Add(new TextChange(span, newText));
}
private TextSpan GetTextSpan(SyntaxTrivia trivia1, SyntaxTrivia trivia2)
{
if (trivia1.RawKind == 0)
{
return TextSpan.FromBounds(this.StartPosition, trivia2.FullSpan.Start);
}
if (trivia2.RawKind == 0)
{
return TextSpan.FromBounds(trivia1.FullSpan.End, this.EndPosition);
}
return TextSpan.FromBounds(trivia1.FullSpan.End, trivia2.FullSpan.Start);
}
private bool IsWhitespaceOrEndOfLine(SyntaxTrivia trivia)
=> IsWhitespace(trivia) || IsEndOfLine(trivia);
private LineColumnDelta GetLineColumnOfWhitespace(
LineColumn lineColumn,
SyntaxTrivia previousTrivia,
SyntaxTrivia trivia1,
LineColumnDelta whitespaceBetween,
SyntaxTrivia trivia2)
{
Debug.Assert(IsWhitespaceOrEndOfLine(trivia2));
// treat elastic as new line as long as its previous trivia is not elastic or
// it has line break right before it
if (trivia2.IsElastic())
{
// eat up consecutive elastic trivia or next line
if (trivia1.IsElastic() || IsEndOfLine(trivia1))
{
return LineColumnDelta.Default;
}
// if there was already new lines, ignore elastic
var lineColumnAfterPreviousTrivia = GetLineColumn(lineColumn, previousTrivia);
var newLineFromPreviousOperation = (whitespaceBetween.Lines > 0) ||
(lineColumnAfterPreviousTrivia.Line > 0 && lineColumnAfterPreviousTrivia.Column == 0);
if (newLineFromPreviousOperation && whitespaceBetween.WhitespaceOnly)
{
return LineColumnDelta.Default;
}
return new LineColumnDelta(lines: 1, spaces: 0, whitespaceOnly: true, forceUpdate: true);
}
if (IsEndOfLine(trivia2))
{
return new LineColumnDelta(lines: 1, spaces: 0, whitespaceOnly: true, forceUpdate: false);
}
var text = trivia2.ToFullString();
return new LineColumnDelta(
lines: 0,
spaces: text.ConvertTabToSpace(Options.TabSize, lineColumn.With(whitespaceBetween).Column, text.Length),
whitespaceOnly: true,
forceUpdate: false);
}
private LineColumn GetInitialLineColumn()
{
var tokenText = this.Token1.ToString();
var initialColumn = this.Token1.RawKind == 0 ? 0 : this.TokenStream.GetCurrentColumn(this.Token1);
var delta = GetLineColumnDelta(initialColumn, tokenText);
return new LineColumn(line: 0, column: initialColumn + delta.Spaces, whitespaceOnly: delta.WhitespaceOnly);
}
protected LineColumn GetLineColumn(LineColumn lineColumn, SyntaxTrivia trivia)
{
var text = trivia.ToFullString();
return lineColumn.With(GetLineColumnDelta(lineColumn.Column, text));
}
protected LineColumnDelta GetLineColumnDelta(LineColumn lineColumn, SyntaxTrivia trivia)
{
var text = trivia.ToFullString();
return GetLineColumnDelta(lineColumn.Column, text);
}
protected LineColumnDelta GetLineColumnDelta(int initialColumn, string text)
{
var lineText = text.GetLastLineText();
if (text != lineText)
{
return new LineColumnDelta(
lines: text.GetNumberOfLineBreaks(),
spaces: lineText.GetColumnFromLineOffset(lineText.Length, Options.TabSize),
whitespaceOnly: IsNullOrWhitespace(lineText));
}
return new LineColumnDelta(
lines: 0,
spaces: text.ConvertTabToSpace(Options.TabSize, initialColumn, text.Length),
whitespaceOnly: IsNullOrWhitespace(lineText));
}
protected int GetExistingIndentation(SyntaxTrivia trivia)
{
var offset = trivia.FullSpan.Start - this.StartPosition;
var originalText = this.OriginalString[..offset];
var delta = GetLineColumnDelta(this.InitialLineColumn.Column, originalText);
return this.InitialLineColumn.With(delta).Column;
}
private static string GetSpaces(int space)
{
if (space is >= 0 and < 20)
{
return s_spaceCache[space];
}
return new string(' ', space);
}
}
|