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