// 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; 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); } } |