235 lines
10 KiB
C#
235 lines
10 KiB
C#
![]() |
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||
|
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Diagnostics;
|
||
|
using System.Linq;
|
||
|
using System.Threading;
|
||
|
using Microsoft.CodeAnalysis;
|
||
|
using Microsoft.CodeAnalysis.Formatting;
|
||
|
using Microsoft.CodeAnalysis.Formatting.Rules;
|
||
|
using Microsoft.CodeAnalysis.LanguageServices;
|
||
|
using Microsoft.CodeAnalysis.Options;
|
||
|
using Microsoft.CodeAnalysis.Shared.Extensions;
|
||
|
using Microsoft.CodeAnalysis.Text;
|
||
|
|
||
|
namespace dnSpy.Roslyn.Internal.SmartIndent
|
||
|
{
|
||
|
internal abstract partial class AbstractIndentationService
|
||
|
{
|
||
|
internal abstract class AbstractIndenter
|
||
|
{
|
||
|
protected readonly OptionSet OptionSet;
|
||
|
protected readonly TextLine LineToBeIndented;
|
||
|
protected readonly int TabSize;
|
||
|
protected readonly CancellationToken CancellationToken;
|
||
|
|
||
|
protected readonly SyntaxTree Tree;
|
||
|
protected readonly IEnumerable<IFormattingRule> Rules;
|
||
|
protected readonly BottomUpBaseIndentationFinder Finder;
|
||
|
|
||
|
private static readonly Func<SyntaxToken, bool> s_tokenHasDirective = tk => tk.ContainsDirectives &&
|
||
|
(tk.LeadingTrivia.Any(tr => tr.IsDirective) || tk.TrailingTrivia.Any(tr => tr.IsDirective));
|
||
|
private readonly ISyntaxFactsService _syntaxFacts;
|
||
|
|
||
|
public AbstractIndenter(
|
||
|
ISyntaxFactsService syntaxFacts,
|
||
|
SyntaxTree syntaxTree,
|
||
|
IEnumerable<IFormattingRule> rules,
|
||
|
OptionSet optionSet,
|
||
|
TextLine lineToBeIndented,
|
||
|
CancellationToken cancellationToken)
|
||
|
{
|
||
|
var syntaxRoot = syntaxTree.GetRoot(cancellationToken);
|
||
|
|
||
|
this._syntaxFacts = syntaxFacts;
|
||
|
this.OptionSet = optionSet;
|
||
|
this.Tree = syntaxTree;
|
||
|
this.LineToBeIndented = lineToBeIndented;
|
||
|
this.TabSize = this.OptionSet.GetOption(FormattingOptions.TabSize, syntaxRoot.Language);
|
||
|
this.CancellationToken = cancellationToken;
|
||
|
|
||
|
this.Rules = rules;
|
||
|
this.Finder = new BottomUpBaseIndentationFinder(
|
||
|
new ChainedFormattingRules(this.Rules, OptionSet),
|
||
|
this.TabSize,
|
||
|
this.OptionSet.GetOption(FormattingOptions.IndentationSize, syntaxRoot.Language),
|
||
|
tokenStream: null,
|
||
|
lastToken: default);
|
||
|
}
|
||
|
|
||
|
public IndentationResult? GetDesiredIndentation(Document document)
|
||
|
{
|
||
|
var indentStyle = OptionSet.GetOption(FormattingOptions.SmartIndent, document.Project.Language);
|
||
|
if (indentStyle == FormattingOptions.IndentStyle.None)
|
||
|
{
|
||
|
// If there is no indent style, then do nothing.
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// find previous line that is not blank. this will skip over things like preprocessor
|
||
|
// regions and inactive code.
|
||
|
var previousLineOpt = GetPreviousNonBlankOrPreprocessorLine();
|
||
|
|
||
|
// it is beginning of the file, there is no previous line exists.
|
||
|
// in that case, indentation 0 is our base indentation.
|
||
|
if (previousLineOpt == null)
|
||
|
{
|
||
|
return IndentFromStartOfLine(0);
|
||
|
}
|
||
|
|
||
|
var previousNonWhitespaceOrPreprocessorLine = previousLineOpt.Value;
|
||
|
|
||
|
// If the user wants block indentation, then we just return the indentation
|
||
|
// of the last piece of real code.
|
||
|
//
|
||
|
// TODO(cyrusn): It's not clear to me that this is correct. Block indentation
|
||
|
// should probably follow the indentation of hte last non-blank line *regardless
|
||
|
// if it is inactive/preprocessor region. By skipping over thse, we are essentially
|
||
|
// being 'smart', and that seems to be overriding the user desire to have Block
|
||
|
// indentation.
|
||
|
if (indentStyle == FormattingOptions.IndentStyle.Block)
|
||
|
{
|
||
|
// If it's block indentation, then just base
|
||
|
return GetIndentationOfLine(previousNonWhitespaceOrPreprocessorLine);
|
||
|
}
|
||
|
|
||
|
Debug.Assert(indentStyle == FormattingOptions.IndentStyle.Smart);
|
||
|
|
||
|
// Because we know that previousLine is not-whitespace, we know that we should be
|
||
|
// able to get the last non-whitespace position.
|
||
|
var lastNonWhitespacePosition = previousNonWhitespaceOrPreprocessorLine.GetLastNonWhitespacePosition().Value;
|
||
|
|
||
|
var token = Tree.GetRoot(CancellationToken).FindToken(lastNonWhitespacePosition);
|
||
|
Debug.Assert(token.RawKind != 0, "FindToken should always return a valid token");
|
||
|
|
||
|
return GetDesiredIndentationWorker(
|
||
|
token, previousNonWhitespaceOrPreprocessorLine, lastNonWhitespacePosition);
|
||
|
}
|
||
|
|
||
|
protected abstract IndentationResult? GetDesiredIndentationWorker(
|
||
|
SyntaxToken token, TextLine previousLine, int lastNonWhitespacePosition);
|
||
|
|
||
|
protected IndentationResult IndentFromStartOfLine(int addedSpaces)
|
||
|
=> new IndentationResult(this.LineToBeIndented.Start, addedSpaces);
|
||
|
|
||
|
protected IndentationResult GetIndentationOfToken(SyntaxToken token)
|
||
|
=> GetIndentationOfToken(token, addedSpaces: 0);
|
||
|
|
||
|
protected IndentationResult GetIndentationOfToken(SyntaxToken token, int addedSpaces)
|
||
|
=> GetIndentationOfPosition(token.SpanStart, addedSpaces);
|
||
|
|
||
|
protected IndentationResult GetIndentationOfLine(TextLine lineToMatch)
|
||
|
=> GetIndentationOfLine(lineToMatch, addedSpaces: 0);
|
||
|
|
||
|
protected IndentationResult GetIndentationOfLine(TextLine lineToMatch, int addedSpaces)
|
||
|
{
|
||
|
var firstNonWhitespace = lineToMatch.GetFirstNonWhitespacePosition();
|
||
|
firstNonWhitespace = firstNonWhitespace ?? lineToMatch.End;
|
||
|
|
||
|
return GetIndentationOfPosition(firstNonWhitespace.Value, addedSpaces);
|
||
|
}
|
||
|
|
||
|
protected 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 root = this.Tree.GetRoot(CancellationToken.None);
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
protected TextLine? GetPreviousNonBlankOrPreprocessorLine()
|
||
|
{
|
||
|
if (LineToBeIndented.LineNumber <= 0)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
var sourceText = this.LineToBeIndented.Text;
|
||
|
|
||
|
var lineNumber = this.LineToBeIndented.LineNumber - 1;
|
||
|
while (lineNumber >= 0)
|
||
|
{
|
||
|
var actualLine = sourceText.Lines[lineNumber];
|
||
|
|
||
|
// Empty line, no indentation to match.
|
||
|
if (string.IsNullOrWhiteSpace(actualLine.ToString()))
|
||
|
{
|
||
|
lineNumber--;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// No preprocessors in the entire tree, so this
|
||
|
// line definitely doesn't have one
|
||
|
var root = Tree.GetRoot(CancellationToken);
|
||
|
if (!root.ContainsDirectives)
|
||
|
{
|
||
|
return sourceText.Lines[lineNumber];
|
||
|
}
|
||
|
|
||
|
// This line is inside an inactive region. Examine the
|
||
|
// first preceding line not in an inactive region.
|
||
|
var disabledSpan = _syntaxFacts.GetInactiveRegionSpanAroundPosition(this.Tree, actualLine.Span.Start, CancellationToken);
|
||
|
if (disabledSpan != default)
|
||
|
{
|
||
|
var targetLine = sourceText.Lines.GetLineFromPosition(disabledSpan.Start).LineNumber;
|
||
|
lineNumber = targetLine - 1;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// A preprocessor directive starts on this line.
|
||
|
if (HasPreprocessorCharacter(actualLine) &&
|
||
|
root.DescendantTokens(actualLine.Span, tk => tk.FullWidth() > 0).Any(s_tokenHasDirective))
|
||
|
{
|
||
|
lineNumber--;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
return sourceText.Lines[lineNumber];
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
protected int GetCurrentPositionNotBelongToEndOfFileToken(int position)
|
||
|
{
|
||
|
var compilationUnit = Tree.GetRoot(CancellationToken) as ICompilationUnitSyntax;
|
||
|
if (compilationUnit == null)
|
||
|
{
|
||
|
return position;
|
||
|
}
|
||
|
|
||
|
return Math.Min(compilationUnit.EndOfFileToken.FullSpan.Start, position);
|
||
|
}
|
||
|
|
||
|
protected bool HasPreprocessorCharacter(TextLine currentLine)
|
||
|
{
|
||
|
var text = currentLine.ToString();
|
||
|
//Contract.Requires(!string.IsNullOrWhiteSpace(text));
|
||
|
|
||
|
var trimmedText = text.Trim();
|
||
|
|
||
|
return trimmedText[0] == '#';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|