File: FormattingContext.cs
Web Access
Project: ..\..\..\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle)
// 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;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Formatting
{
    /// <summary>
    /// this class maintain contextual information such as 
    /// indentation of current position, based token to follow in current position and etc.
    /// </summary>
    internal partial class FormattingContext
    {
        private readonly AbstractFormatEngine _engine;
        private readonly TokenStream _tokenStream;
 
        // interval tree for inseparable regions (Span to indentation data)
        // due to dependencies, each region defined in the data can't be formatted independently.
        private readonly ContextIntervalTree<RelativeIndentationData, FormattingContextIntervalIntrospector> _relativeIndentationTree;
 
        // interval tree for each operations.
        // given a span in the tree, it returns data (indentation, anchor delta, etc) to be applied for the span
        private readonly ContextIntervalTree<IndentationData, FormattingContextIntervalIntrospector> _indentationTree;
        private readonly ContextIntervalTree<SuppressWrappingData, SuppressIntervalIntrospector> _suppressWrappingTree;
        private readonly ContextIntervalTree<SuppressSpacingData, SuppressIntervalIntrospector> _suppressSpacingTree;
        private readonly ContextIntervalTree<SuppressSpacingData, SuppressIntervalIntrospector> _suppressFormattingTree;
        private readonly ContextIntervalTree<AnchorData, FormattingContextIntervalIntrospector> _anchorTree;
 
        // anchor token to anchor data map.
        // unlike anchorTree that would return anchor data for given span in the tree, it will return
        // anchorData based on key which is anchor token.
        private readonly SegmentedDictionary<SyntaxToken, AnchorData> _anchorBaseTokenMap = new();
 
        // hashset to prevent duplicate entries in the trees.
        private readonly HashSet<TextSpan> _indentationMap = new();
        private readonly HashSet<TextSpan> _suppressWrappingMap = new();
        private readonly HashSet<TextSpan> _suppressSpacingMap = new();
        private readonly HashSet<TextSpan> _suppressFormattingMap = new();
        private readonly HashSet<TextSpan> _anchorMap = new();
 
        // used for selection based formatting case. it contains operations that will define
        // what indentation to use as a starting indentation. (we always use 0 for formatting whole tree case)
        private List<IndentBlockOperation> _initialIndentBlockOperations = new();
 
        public FormattingContext(AbstractFormatEngine engine, TokenStream tokenStream)
        {
            Contract.ThrowIfNull(engine);
            Contract.ThrowIfNull(tokenStream);
 
            _engine = engine;
            _tokenStream = tokenStream;
 
            _relativeIndentationTree = new ContextIntervalTree<RelativeIndentationData, FormattingContextIntervalIntrospector>(new FormattingContextIntervalIntrospector());
 
            _indentationTree = new ContextIntervalTree<IndentationData, FormattingContextIntervalIntrospector>(new FormattingContextIntervalIntrospector());
            _suppressWrappingTree = new ContextIntervalTree<SuppressWrappingData, SuppressIntervalIntrospector>(new SuppressIntervalIntrospector());
            _suppressSpacingTree = new ContextIntervalTree<SuppressSpacingData, SuppressIntervalIntrospector>(new SuppressIntervalIntrospector());
            _suppressFormattingTree = new ContextIntervalTree<SuppressSpacingData, SuppressIntervalIntrospector>(new SuppressIntervalIntrospector());
            _anchorTree = new ContextIntervalTree<AnchorData, FormattingContextIntervalIntrospector>(new FormattingContextIntervalIntrospector());
        }
 
        public void Initialize(
            ChainedFormattingRules formattingRules,
            SyntaxToken startToken,
            SyntaxToken endToken,
            CancellationToken cancellationToken)
        {
            var rootNode = this.TreeData.Root;
            if (_tokenStream.IsFormattingWholeDocument)
            {
                // if we are trying to format whole document, there is no reason to get initial context. just set
                // initial indentation.
                var data = new SimpleIndentationData(rootNode.FullSpan, indentation: 0);
                _indentationTree.AddIntervalInPlace(data);
                _indentationMap.Add(data.TextSpan);
                return;
            }
 
            var initialContextFinder = new InitialContextFinder(_tokenStream, formattingRules, rootNode);
            var (indentOperations, suppressOperations) = initialContextFinder.Do(startToken, endToken);
 
            if (indentOperations != null)
            {
                var indentationOperations = indentOperations;
 
                var initialOperation = indentationOperations[0];
                var baseIndentationFinder = new BottomUpBaseIndentationFinder(
                                                formattingRules,
                                                Options.TabSize,
                                                Options.IndentationSize,
                                                _tokenStream,
                                                _engine.HeaderFacts);
                var initialIndentation = baseIndentationFinder.GetIndentationOfCurrentPosition(
                    rootNode,
                    initialOperation,
                    _tokenStream.GetCurrentColumn, cancellationToken);
 
                var data = new SimpleIndentationData(initialOperation.TextSpan, initialIndentation);
                _indentationTree.AddIntervalInPlace(data);
                _indentationMap.Add(data.TextSpan);
 
                // hold onto initial operations
                _initialIndentBlockOperations = indentationOperations;
            }
 
            suppressOperations?.Do(this.AddInitialSuppressOperation);
        }
 
        public void AddIndentBlockOperations(
            List<IndentBlockOperation> operations,
            CancellationToken cancellationToken)
        {
            Contract.ThrowIfNull(operations);
 
            // if there is no initial block operations
            if (_initialIndentBlockOperations.Count <= 0)
            {
                // sort operations and add them to interval tree
                operations.Sort(CommonFormattingHelpers.IndentBlockOperationComparer);
                operations.Do(o =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    this.AddIndentBlockOperation(o);
                });
 
                return;
            }
 
            var baseSpan = _initialIndentBlockOperations[0].TextSpan;
 
            // indentation tree must build up from inputs that are in right order except initial indentation.
            // merge indentation operations from two places (initial operations for current selection, and nodes inside of selections)
            // sort it in right order and apply them to tree
            var count = _initialIndentBlockOperations.Count - 1 + operations.Count;
            var mergedList = new List<IndentBlockOperation>(count);
 
            // initial operations are already sorted, just add, no need to filter
            for (var i = 1; i < _initialIndentBlockOperations.Count; i++)
            {
                mergedList.Add(_initialIndentBlockOperations[i]);
            }
 
            for (var i = 0; i < operations.Count; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                // filter out operations whose position is before the base indentation
                var operationSpan = operations[i].TextSpan;
 
                if (operationSpan.Start < baseSpan.Start ||
                    operationSpan.Contains(baseSpan))
                {
                    continue;
                }
 
                mergedList.Add(operations[i]);
            }
 
            mergedList.Sort(CommonFormattingHelpers.IndentBlockOperationComparer);
            mergedList.Do(o =>
            {
                cancellationToken.ThrowIfCancellationRequested();
                this.AddIndentBlockOperation(o);
            });
        }
 
        public void AddIndentBlockOperation(IndentBlockOperation operation)
        {
            var intervalTreeSpan = operation.TextSpan;
 
            // don't add stuff if it is empty
            if (intervalTreeSpan.IsEmpty ||
                _indentationMap.Contains(intervalTreeSpan))
            {
                return;
            }
 
            // relative indentation case where indentation depends on other token
            if (operation.IsRelativeIndentation)
            {
                Func<FormattingContext, IndentBlockOperation, SyntaxToken> effectiveBaseTokenGetter = operation.Option.IsOn(IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine)
                    ? static (self, operation) => self._tokenStream.FirstTokenOfBaseTokenLine(operation.BaseToken)
                    : static (self, operation) => operation.BaseToken;
 
                Func<FormattingContext, IndentBlockOperation, SyntaxToken, int> relativeIndentationDeltaGetter = static (self, operation, effectiveBaseToken) =>
                {
                    var baseIndentationDelta = operation.GetAdjustedIndentationDelta(self._engine.HeaderFacts, self.TreeData.Root, effectiveBaseToken);
                    return baseIndentationDelta * self.Options.IndentationSize;
                };
 
                // baseIndentation is calculated for the adjusted token if option is RelativeToFirstTokenOnBaseTokenLine
                Func<FormattingContext, SyntaxToken, int> relativeIndentationBaseIndentationGetter =
                    static (self, effectiveBaseToken) => self._tokenStream.GetCurrentColumn(effectiveBaseToken);
 
                // set new indentation
                var inseparableRegionStartingPosition = effectiveBaseTokenGetter(this, operation).FullSpan.Start;
                var relativeIndentationData = new RelativeIndentationData(this, inseparableRegionStartingPosition, intervalTreeSpan, operation, effectiveBaseTokenGetter, relativeIndentationDeltaGetter, relativeIndentationBaseIndentationGetter);
 
                _indentationTree.AddIntervalInPlace(relativeIndentationData);
                _relativeIndentationTree.AddIntervalInPlace(relativeIndentationData);
                _indentationMap.Add(intervalTreeSpan);
 
                return;
            }
 
            // absolute position case
            if (operation.Option.IsOn(IndentBlockOption.AbsolutePosition))
            {
                _indentationTree.AddIntervalInPlace(new SimpleIndentationData(intervalTreeSpan, operation.IndentationDeltaOrPosition));
                _indentationMap.Add(intervalTreeSpan);
                return;
            }
 
            // regular indentation case where indentation is based on its previous indentation
            var indentationData = _indentationTree.GetSmallestContainingInterval(operation.TextSpan.Start, 0);
            if (indentationData == null)
            {
                // no previous indentation
                var indentation = operation.IndentationDeltaOrPosition * Options.IndentationSize;
                _indentationTree.AddIntervalInPlace(new SimpleIndentationData(intervalTreeSpan, indentation));
                _indentationMap.Add(intervalTreeSpan);
                return;
            }
 
            // get indentation based on its previous indentation
            if (indentationData is not AdjustedIndentationData { BaseIndentationData: var baseIndentationData, Adjustment: var totalAdjustment })
            {
                (baseIndentationData, totalAdjustment) = (indentationData, 0);
            }
 
            totalAdjustment += operation.IndentationDeltaOrPosition * Options.IndentationSize;
 
            IndentationData derived;
            if (totalAdjustment == 0)
            {
                derived = baseIndentationData.WithTextSpan(intervalTreeSpan);
            }
            else if (baseIndentationData is SimpleIndentationData { Indentation: var baseIndentation })
            {
                derived = new SimpleIndentationData(intervalTreeSpan, baseIndentation + totalAdjustment);
            }
            else
            {
                derived = new AdjustedIndentationData(intervalTreeSpan, baseIndentationData, totalAdjustment);
            }
 
            // set new indentation
            _indentationTree.AddIntervalInPlace(derived);
            _indentationMap.Add(intervalTreeSpan);
        }
 
        public void AddInitialSuppressOperation(SuppressOperation operation)
        {
            // don't add stuff if it is empty
            if (operation == null || operation.TextSpan.IsEmpty)
            {
                return;
            }
 
            var onSameLine = _tokenStream.TwoTokensOriginallyOnSameLine(operation.StartToken, operation.EndToken);
            AddSuppressOperation(operation, onSameLine);
        }
 
        public void AddSuppressOperations(
            List<SuppressOperation> operations,
            CancellationToken cancellationToken)
        {
            var valuePairs = new SegmentedArray<(SuppressOperation operation, bool shouldSuppress, bool onSameLine)>(operations.Count);
 
            // TODO: think about a way to figure out whether it is already suppressed and skip the expensive check below.
            for (var i = 0; i < operations.Count; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                var operation = operations[i];
 
                // if an operation contains elastic trivia itself and the operation is not marked to ignore the elastic trivia 
                // ignore the operation 
                if (operation.ContainsElasticTrivia(_tokenStream) && !operation.Option.IsOn(SuppressOption.IgnoreElasticWrapping))
                {
                    // don't bother to calculate line alignment between tokens 
                    valuePairs[i] = (operation, shouldSuppress: false, onSameLine: false);
                    continue;
                }
 
                var onSameLine = _tokenStream.TwoTokensOriginallyOnSameLine(operation.StartToken, operation.EndToken);
                valuePairs[i] = (operation, shouldSuppress: true, onSameLine);
            }
 
            foreach (var (operation, shouldSuppress, onSameLine) in valuePairs)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                if (shouldSuppress)
                {
                    AddSuppressOperation(operation, onSameLine);
                }
            }
        }
 
        private void AddSuppressOperation(SuppressOperation operation, bool onSameLine)
        {
            AddSpacingSuppressOperation(operation, onSameLine);
            AddFormattingSuppressOperation(operation);
            AddWrappingSuppressOperation(operation, onSameLine);
        }
 
        private void AddSpacingSuppressOperation(SuppressOperation operation, bool twoTokensOnSameLine)
        {
            // don't add stuff if it is empty
            if (operation == null ||
                operation.TextSpan.IsEmpty)
            {
                return;
            }
 
            // we might need to merge bits with enclosing suppress flag
            var option = operation.Option;
            if (!option.IsMaskOn(SuppressOption.NoSpacing) || _suppressSpacingMap.Contains(operation.TextSpan))
            {
                return;
            }
 
            if (!(option.IsOn(SuppressOption.NoSpacingIfOnSingleLine) && twoTokensOnSameLine) &&
                !(option.IsOn(SuppressOption.NoSpacingIfOnMultipleLine) && !twoTokensOnSameLine))
            {
                return;
            }
 
            var data = new SuppressSpacingData(operation.TextSpan);
 
            _suppressSpacingMap.Add(operation.TextSpan);
            _suppressSpacingTree.AddIntervalInPlace(data);
        }
 
        private void AddFormattingSuppressOperation(SuppressOperation operation)
        {
            // don't add stuff if it is empty
            if (operation == null ||
                operation.TextSpan.IsEmpty)
            {
                return;
            }
 
            // we might need to merge bits with enclosing suppress flag
            var option = operation.Option;
            if (!option.IsOn(SuppressOption.DisableFormatting) || _suppressFormattingMap.Contains(operation.TextSpan))
            {
                return;
            }
 
            var data = new SuppressSpacingData(operation.TextSpan);
 
            _suppressFormattingMap.Add(operation.TextSpan);
            _suppressFormattingTree.AddIntervalInPlace(data);
        }
 
        private void AddWrappingSuppressOperation(SuppressOperation operation, bool twoTokensOnSameLine)
        {
            // don't add stuff if it is empty
            if (operation == null ||
                operation.TextSpan.IsEmpty)
            {
                return;
            }
 
            var option = operation.Option;
            if (!option.IsMaskOn(SuppressOption.NoWrapping) || _suppressWrappingMap.Contains(operation.TextSpan))
            {
                return;
            }
 
            if (!(option.IsOn(SuppressOption.NoWrappingIfOnSingleLine) && twoTokensOnSameLine) &&
                !(option.IsOn(SuppressOption.NoWrappingIfOnMultipleLine) && !twoTokensOnSameLine))
            {
                return;
            }
 
            var ignoreElastic = option.IsMaskOn(SuppressOption.IgnoreElasticWrapping) ||
                                !operation.ContainsElasticTrivia(_tokenStream);
 
            var data = new SuppressWrappingData(operation.TextSpan, ignoreElastic: ignoreElastic);
 
            _suppressWrappingMap.Add(operation.TextSpan);
            _suppressWrappingTree.AddIntervalInPlace(data);
        }
 
        public void AddAnchorIndentationOperation(AnchorIndentationOperation operation)
        {
            // don't add stuff if it is empty
            if (operation.TextSpan.IsEmpty ||
                _anchorMap.Contains(operation.TextSpan) ||
                _anchorBaseTokenMap.ContainsKey(operation.AnchorToken))
            {
                return;
            }
 
            // If the indentation changes on a line which other code is anchored to, adjust those other lines to reflect
            // the same change in indentation. Note that we anchor to the first token on a line to account for common
            // cases like the following code, where the `{` token is anchored to the `(` token of `()`:
            //
            //                ↓ this space can be removed, which moves `(` one character to the left
            // var x = Method( () =>
            // {
            // ↑ this `{` anchors to `var` instead of `(`, which prevents it from moving when `(` is moved
            // });
            //
            // The calculation of true anchor token (which is always the first token on a line) is delayed to account
            // for cases where the original anchor token is moved to a new line during a formatting operation.
            var anchorToken = _tokenStream.FirstTokenOfBaseTokenLine(operation.AnchorToken);
            var originalSpace = _tokenStream.GetOriginalColumn(anchorToken);
            var data = new AnchorData(operation, anchorToken, originalSpace);
 
            _anchorTree.AddIntervalInPlace(data);
 
            _anchorBaseTokenMap.Add(operation.AnchorToken, data);
            _anchorMap.Add(operation.TextSpan);
        }
 
        [Conditional("DEBUG")]
        private static void DebugCheckEmpty<T, TIntrospector>(ContextIntervalTree<T, TIntrospector> tree, TextSpan textSpan)
            where TIntrospector : struct, IIntervalIntrospector<T>
        {
            var intervals = tree.GetIntervalsThatContain(textSpan.Start, textSpan.Length);
            Contract.ThrowIfFalse(intervals.Length == 0);
        }
 
        public int GetBaseIndentation(SyntaxToken token)
            => GetBaseIndentation(token.SpanStart);
 
        public int GetBaseIndentation(int position)
        {
            var indentationData = _indentationTree.GetSmallestContainingInterval(position, 0);
            if (indentationData == null)
            {
                DebugCheckEmpty(_indentationTree, new TextSpan(position, 0));
                return 0;
            }
 
            return indentationData.Indentation;
        }
 
        public IEnumerable<IndentBlockOperation> GetAllRelativeIndentBlockOperations()
            => _relativeIndentationTree.GetIntervalsThatIntersectWith(this.TreeData.StartPosition, this.TreeData.EndPosition, new FormattingContextIntervalIntrospector()).Select(i => i.Operation);
 
        public bool TryGetEndTokenForRelativeIndentationSpan(SyntaxToken token, int maxChainDepth, out SyntaxToken endToken, CancellationToken cancellationToken)
        {
            endToken = default;
 
            var depth = 0;
            while (true)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                if (depth++ > maxChainDepth)
                {
                    return false;
                }
 
                var span = token.Span;
                var indentationData = _relativeIndentationTree.GetSmallestContainingInterval(span.Start, 0);
                if (indentationData == null)
                {
                    // this means the given token is not inside of inseparable regions
                    endToken = token;
                    return true;
                }
 
                // recursively find the end token outside of inseparable regions
                token = indentationData.EndToken.GetNextToken(includeZeroWidth: true);
                if (token.RawKind == 0)
                {
                    // reached end of tree
                    return true;
                }
            }
        }
 
        private AnchorData? GetAnchorData(SyntaxToken token)
        {
            var span = token.Span;
 
            var anchorData = _anchorTree.GetSmallestContainingInterval(span.Start, 0);
            if (anchorData == null)
            {
                // no anchor
                DebugCheckEmpty(_anchorTree, new TextSpan(span.Start, 0));
                return null;
            }
 
            return anchorData;
        }
 
        public int GetAnchorDeltaFromOriginalColumn(SyntaxToken token)
        {
            var anchorData = GetAnchorData(token);
            if (anchorData == null)
            {
                return 0;
            }
 
            var currentColumn = _tokenStream.GetCurrentColumn(anchorData.AnchorToken);
            return currentColumn - anchorData.OriginalColumn;
        }
 
        public SyntaxToken GetAnchorToken(SyntaxToken token)
        {
            var anchorData = GetAnchorData(token);
            if (anchorData == null)
            {
                return default;
            }
 
            return anchorData.AnchorToken;
        }
 
        public int GetDeltaFromPreviousChangesMap(SyntaxToken token, Dictionary<SyntaxToken, int> previousChangesMap)
        {
            // no changes
            if (!previousChangesMap.ContainsKey(token))
            {
                return 0;
            }
 
            var currentColumn = _tokenStream.GetCurrentColumn(token);
            return currentColumn - previousChangesMap[token];
        }
 
        public SyntaxToken GetEndTokenForAnchorSpan(TokenData tokenData)
        {
            // consider situation like below
            //
            // var q = from c in cs
            //              where c > 1
            //                          + 
            //                            2;
            //
            // if alignment operation moves "where" to align with "from"
            // we want to move "+" and "2" along with it (anchor operation)
            // 
            // below we are trying to figure out up to which token ("2" in the above example)
            // we should apply the anchor operation
            var baseAnchorData = FindAnchorSpanOnSameLineAfterToken(tokenData);
            if (baseAnchorData == null)
            {
                return default;
            }
 
            // our anchor operation is very flexible so it not only let one anchor to contain others, it also
            // let anchors to overlap each other for whatever reasons
            // below, we will try to flat the overlapped anchor span, and find the last position (token) of that span
 
            // find other anchors overlapping with current anchor span
            var anchorData = _anchorTree.GetIntervalsThatOverlapWith(baseAnchorData.TextSpan.Start, baseAnchorData.TextSpan.Length);
 
            // among those anchors find the biggest end token
            var lastEndToken = baseAnchorData.EndToken;
            foreach (var interval in anchorData)
            {
                // anchor token is not in scope, move to next
                if (!baseAnchorData.TextSpan.IntersectsWith(interval.AnchorToken.Span))
                {
                    continue;
                }
 
                if (interval.EndToken.Span.End < lastEndToken.Span.End)
                {
                    continue;
                }
 
                lastEndToken = interval.EndToken;
            }
 
            return lastEndToken;
        }
 
        private AnchorData? FindAnchorSpanOnSameLineAfterToken(TokenData tokenData)
        {
            // every token after given token on same line is implicitly dependent to the token.
            // check whether one of them is an anchor token.
 
            AnchorData? lastBaseAnchorData = null;
            while (tokenData.IndexInStream >= 0)
            {
                if (_anchorBaseTokenMap.TryGetValue(tokenData.Token, out var tempAnchorData))
                {
                    lastBaseAnchorData = tempAnchorData;
                }
 
                // tokenPairIndex is always 0 <= ... < TokenCount - 1
                var tokenPairIndex = tokenData.IndexInStream;
                if (_tokenStream.TokenCount - 1 <= tokenPairIndex ||
                    _tokenStream.GetTriviaData(tokenPairIndex).SecondTokenIsFirstTokenOnLine)
                {
                    return lastBaseAnchorData;
                }
 
                tokenData = tokenData.GetNextTokenData();
            }
 
            return lastBaseAnchorData;
        }
 
        public bool IsWrappingSuppressed(TextSpan textSpan, bool containsElasticTrivia)
        {
            if (IsFormattingDisabled(textSpan))
            {
                return true;
            }
 
            // use edge exclusive version of GetSmallestContainingInterval
            var data = _suppressWrappingTree.GetSmallestEdgeExclusivelyContainingInterval(textSpan.Start, textSpan.Length);
            if (data == null)
            {
                return false;
            }
 
            if (containsElasticTrivia && !data.IgnoreElastic)
            {
                return false;
            }
 
            return true;
        }
 
        public bool IsSpacingSuppressed(TextSpan textSpan, bool containsElasticTrivia)
        {
            if (IsFormattingDisabled(textSpan))
            {
                return true;
            }
 
            // For spaces, never ignore elastic trivia because that can 
            // generate incorrect code
            if (containsElasticTrivia)
            {
                return false;
            }
 
            // use edge exclusive version of GetSmallestContainingInterval
            var data = _suppressSpacingTree.GetSmallestEdgeExclusivelyContainingInterval(textSpan.Start, textSpan.Length);
            if (data == null)
            {
                return false;
            }
 
            return true;
        }
 
        public bool IsSpacingSuppressed(int pairIndex)
        {
            var token1 = _tokenStream.GetToken(pairIndex);
            var token2 = _tokenStream.GetToken(pairIndex + 1);
 
            var spanBetweenTwoTokens = TextSpan.FromBounds(token1.SpanStart, token2.Span.End);
 
            // this version of SpacingSuppressed will be called after all basic space operations are done. 
            // so no more elastic trivia should have left out
            return IsSpacingSuppressed(spanBetweenTwoTokens, containsElasticTrivia: false);
        }
 
        public bool IsFormattingDisabled(TextSpan textSpan)
            => _suppressFormattingTree.HasIntervalThatIntersectsWith(textSpan.Start, textSpan.Length);
 
        public bool IsFormattingDisabled(int pairIndex)
        {
            var token1 = _tokenStream.GetToken(pairIndex);
            var token2 = _tokenStream.GetToken(pairIndex + 1);
 
            var spanBetweenTwoTokens = TextSpan.FromBounds(token1.SpanStart, token2.Span.End);
            return IsFormattingDisabled(spanBetweenTwoTokens);
        }
 
        public SyntaxFormattingOptions Options => _engine.Options;
 
        public TreeData TreeData => _engine.TreeData;
 
        public TokenStream TokenStream => _tokenStream;
    }
}