// 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.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 [.. triviaList]; } triviaList.Clear(); AddRange(triviaList, this.Token1.TrailingTrivia); AddRange(triviaList, this.Token2.LeadingTrivia); return [.. 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 is { Column: 0, 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 is { Lines: 0, 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 is { Line: > 0, 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); } } |