// 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.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Formatting; internal sealed partial class FormattingContext { private sealed class InitialContextFinder { private readonly TokenStream _tokenStream; private readonly ChainedFormattingRules _formattingRules; private readonly SyntaxNode _rootNode; public InitialContextFinder( TokenStream tokenStream, ChainedFormattingRules formattingRules, SyntaxNode rootNode) { Contract.ThrowIfNull(tokenStream); Contract.ThrowIfNull(formattingRules); Contract.ThrowIfNull(rootNode); _tokenStream = tokenStream; _formattingRules = formattingRules; _rootNode = rootNode; } public (List<IndentBlockOperation> indentOperations, ImmutableArray<SuppressOperation> suppressOperations) Do(SyntaxToken startToken, SyntaxToken endToken) { // we are formatting part of document, try to find initial context that formatting will be based on such as // initial indentation and etc. using (Logger.LogBlock(FunctionId.Formatting_ContextInitialization, CancellationToken.None)) { // first try to set initial indentation information var initialIndentationOperations = this.GetInitialIndentBlockOperations(startToken, endToken); // second try to set suppress wrapping regions var initialSuppressOperations = GetInitialSuppressOperations(startToken, endToken); if (initialSuppressOperations != null) { Debug.Assert( initialSuppressOperations.All( o => o.TextSpan.Contains(startToken.SpanStart) || o.TextSpan.Contains(endToken.SpanStart))); } return (initialIndentationOperations, initialSuppressOperations); } } private List<IndentBlockOperation> GetInitialIndentBlockOperations(SyntaxToken startToken, SyntaxToken endToken) { var span = TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End); var node = startToken.GetCommonRoot(endToken).GetParentWithBiggerSpan(); var previous = (SyntaxNode?)null; // starting from the common node, move up to the parent var operations = new List<IndentBlockOperation>(); var list = new List<IndentBlockOperation>(); while (node != null) { // get all operations for the nodes that contains the formatting span, but not ones contained by the span node.DescendantNodesAndSelf(n => n != previous && n.Span.IntersectsWith(span) && !span.Contains(n.Span)) .Do(n => { _formattingRules.AddIndentBlockOperations(list, n); foreach (var element in list) { if (element != null) { operations.Add(element); } } list.Clear(); }); // found some. use these as initial indentation if (operations.Any(o => o.TextSpan.Contains(span))) { break; } previous = node; node = node.Parent; } // make sure operations we have has effects over the formatting span operations.RemoveAll(o => o == null || !o.TextSpan.IntersectsWith(span)); // we couldn't find anything // return initial location so that we can get base indentation correctly if (operations.Count == 0) { operations.Add(new IndentBlockOperation( startToken: _rootNode.GetFirstToken(includeZeroWidth: true), endToken: _rootNode.GetLastToken(includeZeroWidth: true), textSpan: _rootNode.FullSpan, indentationDelta: 0, option: IndentBlockOption.AbsolutePosition)); return operations; } operations.Sort(CommonFormattingHelpers.IndentBlockOperationComparer); return operations; } private ImmutableArray<SuppressOperation> GetInitialSuppressOperations(SyntaxToken startToken, SyntaxToken endToken) { using var _ = ArrayBuilder<SuppressOperation>.GetInstance(out var result); this.AddInitialSuppressOperations(startToken, endToken, SuppressOption.NoWrapping, result); this.AddInitialSuppressOperations(startToken, endToken, SuppressOption.NoSpacing, result); result.Sort(CommonFormattingHelpers.SuppressOperationComparer); return result.ToImmutable(); } private void AddInitialSuppressOperations( SyntaxToken startToken, SyntaxToken endToken, SuppressOption mask, ArrayBuilder<SuppressOperation> result) { this.AddInitialSuppressOperations(startToken, mask, result); this.AddInitialSuppressOperations(endToken, mask, result); } private void AddInitialSuppressOperations(SyntaxToken token, SuppressOption mask, ArrayBuilder<SuppressOperation> result) { var startNode = token.Parent; var startPosition = token.SpanStart; // starting from given token, move up to root until the first meaningful // operation has found using var _ = ArrayBuilder<SuppressOperation>.GetInstance(out var buffer); var currentIndentationNode = startNode; while (currentIndentationNode != null) { _formattingRules.AddSuppressOperations(buffer, currentIndentationNode); buffer.RemoveAll(Predicate, (startPosition, _tokenStream, mask)); if (buffer.Count > 0) { result.AddRange(buffer); return; } currentIndentationNode = currentIndentationNode.Parent; } return; static bool Predicate(SuppressOperation operation, (int startPosition, TokenStream tokenStream, SuppressOption mask) tuple) { if (!operation.TextSpan.Contains(tuple.startPosition)) return true; if (operation.ContainsElasticTrivia(tuple.tokenStream) && !operation.Option.IsOn(SuppressOption.IgnoreElasticWrapping)) return true; if (!operation.Option.IsMaskOn(tuple.mask)) return true; return false; } } } } |