File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Indentation\AbstractIndentation.Indenter.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Indentation;
 
internal abstract partial class AbstractIndentation<TSyntaxRoot>
{
    protected readonly struct Indenter
    {
        private readonly AbstractIndentation<TSyntaxRoot> _service;
 
        public readonly IndentationOptions Options;
        public readonly TextLine LineToBeIndented;
        public readonly CancellationToken CancellationToken;
 
        public readonly IEnumerable<AbstractFormattingRule> Rules;
        public readonly BottomUpBaseIndentationFinder Finder;
 
        private readonly ISyntaxFacts _syntaxFacts;
        private readonly int _tabSize;
 
        public readonly SyntaxTree Tree;
        public readonly SourceText Text;
        public readonly TSyntaxRoot Root;
 
        public readonly ISmartTokenFormatter SmartTokenFormatter;
 
        public Indenter(
            AbstractIndentation<TSyntaxRoot> service,
            SyntaxTree tree,
            SourceText text,
            ImmutableArray<AbstractFormattingRule> rules,
            IndentationOptions options,
            TextLine lineToBeIndented,
            ISmartTokenFormatter smartTokenFormatter,
            CancellationToken cancellationToken)
        {
            _service = service;
            _syntaxFacts = service.SyntaxFacts;
            Options = options;
            Tree = tree;
            Root = (TSyntaxRoot)tree.GetRoot(cancellationToken);
            Text = text;
            LineToBeIndented = lineToBeIndented;
            _tabSize = options.FormattingOptions.TabSize;
            SmartTokenFormatter = smartTokenFormatter;
            CancellationToken = cancellationToken;
 
            Rules = rules;
            Finder = new BottomUpBaseIndentationFinder(
                new ChainedFormattingRules(this.Rules, options.FormattingOptions),
                _tabSize,
                options.FormattingOptions.IndentationSize,
                tokenStream: null,
                service.HeaderFacts);
        }
 
        public IndentationResult? GetDesiredIndentation(FormattingOptions2.IndentStyle indentStyle)
        {
            // If the caller wants no indent, then we'll return an effective '0' indent.
            if (indentStyle == FormattingOptions2.IndentStyle.None)
                return null;
 
            // If the user has explicitly set 'block' indentation, or they're in an inactive preprocessor region,
            // then just do simple block indentation.
            if (indentStyle == FormattingOptions2.IndentStyle.Block ||
                _syntaxFacts.IsInInactiveRegion(this.Tree, LineToBeIndented.Start, this.CancellationToken))
            {
                return GetDesiredBlockIndentation();
            }
 
            Debug.Assert(indentStyle == FormattingOptions2.IndentStyle.Smart);
            return GetDesiredSmartIndentation();
        }
 
        private readonly IndentationResult? GetDesiredSmartIndentation()
        {
            // For smart indent, we generally will be computing from either the previous token in the code, or in a
            // few special cases, the previous trivia.
            var token = TryGetPrecedingVisibleToken();
 
            // Look to see if we're immediately following some visible piece of trivia.  There may
            // be some cases where we'll base our indent off of that.  However, we only do this as 
            // long as we're immediately after the trivia.  If there are any blank lines between us
            // then we consider that unimportant for indentation.
            var trivia = TryGetImmediatelyPrecedingVisibleTrivia();
 
            if (token == null && trivia == null)
                return null;
 
            return _service.GetDesiredIndentationWorker(this, token, trivia);
        }
 
        private readonly SyntaxTrivia? TryGetImmediatelyPrecedingVisibleTrivia()
        {
            if (LineToBeIndented.LineNumber == 0)
                return null;
 
            var previousLine = this.Text.Lines[LineToBeIndented.LineNumber - 1];
            var lastPos = previousLine.GetLastNonWhitespacePosition();
            if (lastPos == null)
                return null;
 
            var trivia = Root.FindTrivia(lastPos.Value);
            if (trivia == default)
                return null;
 
            return trivia;
        }
 
        private readonly SyntaxToken? TryGetPrecedingVisibleToken()
        {
            var token = Root.FindToken(LineToBeIndented.Start);
 
            // we'll either be after the token at the end of a line, or before a token.  We compute indentation
            // based on the preceding token.  So if we're before a token, look back to the previous token to
            // determine what our indentation is based off of.
            if (token.SpanStart >= LineToBeIndented.Start)
            {
                token = token.GetPreviousToken();
 
                // Skip past preceding blank tokens.  This can happen in VB for example where there can be
                // whitespace tokens in things like xml literals.  We want to get the first visible token that we
                // would actually anch would anchor indentation off of.
                while (token != default && string.IsNullOrWhiteSpace(token.ToString()))
                    token = token.GetPreviousToken();
            }
 
            if (token == default)
                return null;
 
            return token;
        }
 
        private IndentationResult? GetDesiredBlockIndentation()
        {
            // Block indentation is simple, we keep walking back lines until we find a line with any sort of
            // text on it.  We then set our indentation to whatever the indentation of that line was.
            for (var currentLine = this.LineToBeIndented.LineNumber - 1; currentLine >= 0; currentLine--)
            {
                var line = this.Text.Lines[currentLine];
                var offset = line.GetFirstNonWhitespaceOffset();
                if (offset == null)
                    continue;
 
                // Found the previous non-blank line.  indent to the same level that it is at
                return new IndentationResult(basePosition: line.Start + offset.Value, offset: 0);
            }
 
            // Couldn't find a previous non-blank line.
            return null;
        }
 
        public bool TryGetSmartTokenIndentation(out IndentationResult indentationResult)
        {
            if (_service.ShouldUseTokenIndenter(this, out var token))
            {
                var changes = SmartTokenFormatter.FormatToken(token, CancellationToken);
 
                var updatedSourceText = Text.WithChanges(changes);
                if (LineToBeIndented.LineNumber < updatedSourceText.Lines.Count)
                {
                    var updatedLine = updatedSourceText.Lines[LineToBeIndented.LineNumber];
                    var nonWhitespaceOffset = updatedLine.GetFirstNonWhitespaceOffset();
                    if (nonWhitespaceOffset != null)
                    {
                        // 'nonWhitespaceOffset' is simply an int indicating how many
                        // *characters* of indentation to include.  For example, an indentation
                        // string of \t\t\t would just count for nonWhitespaceOffset of '3' (one
                        // for each tab char).
                        //
                        // However, what we want is the true columnar offset for the line.
                        // That's what our caller (normally the editor) needs to determine where
                        // to actually put the caret and what whitespace needs to proceed it.
                        //
                        // This can be computed with GetColumnFromLineOffset which again looks
                        // at the contents of the line, but this time evaluates how \t characters 
                        // should translate to column chars.
                        var offset = updatedLine.GetColumnFromLineOffset(nonWhitespaceOffset.Value, _tabSize);
                        indentationResult = new IndentationResult(basePosition: LineToBeIndented.Start, offset: offset);
                        return true;
                    }
                }
            }
 
            indentationResult = default;
            return false;
        }
 
        public IndentationResult IndentFromStartOfLine(int addedSpaces)
            => new(this.LineToBeIndented.Start, addedSpaces);
 
        public IndentationResult GetIndentationOfToken(SyntaxToken token)
            => GetIndentationOfToken(token, addedSpaces: 0);
 
        public IndentationResult GetIndentationOfToken(SyntaxToken token, int addedSpaces)
            => GetIndentationOfPosition(token.SpanStart, addedSpaces);
 
        public IndentationResult GetIndentationOfLine(TextLine lineToMatch)
            => GetIndentationOfLine(lineToMatch, addedSpaces: 0);
 
        public IndentationResult GetIndentationOfLine(TextLine lineToMatch, int addedSpaces)
        {
            var firstNonWhitespace = lineToMatch.GetFirstNonWhitespacePosition();
            firstNonWhitespace ??= lineToMatch.End;
 
            return GetIndentationOfPosition(firstNonWhitespace.Value, addedSpaces);
        }
 
        private IndentationResult GetIndentationOfPosition(int position, int addedSpaces)
        {
            if (this.Tree.OverlapsHiddenPosition(GetNormalizedSpan(position), CancellationToken))
            {
                // Oops, the line we want to line up to is either hidden, or is in a different
                // visible region.
                var token = Root.FindTokenFromEnd(LineToBeIndented.Start);
                var indentation = Finder.GetIndentationOfCurrentPosition(this.Tree, token, LineToBeIndented.Start, CancellationToken.None);
 
                return new IndentationResult(LineToBeIndented.Start, indentation);
            }
 
            return new IndentationResult(position, addedSpaces);
        }
 
        private TextSpan GetNormalizedSpan(int position)
        {
            if (LineToBeIndented.Start < position)
            {
                return TextSpan.FromBounds(LineToBeIndented.Start, position);
            }
 
            return TextSpan.FromBounds(position, LineToBeIndented.Start);
        }
 
        public int GetCurrentPositionNotBelongToEndOfFileToken(int position)
            => Math.Min(Root.EndOfFileToken.FullSpan.Start, position);
    }
}