| File: LineSeparators\CSharpLineSeparatorService.cs | Web Access |
| Project: src\roslyn\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features) |
// 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.Immutable; using System.Composition; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LineSeparators; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.LineSeparators; [ExportLanguageService(typeof(ILineSeparatorService), LanguageNames.CSharp), Shared] internal sealed class CSharpLineSeparatorService : ILineSeparatorService { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CSharpLineSeparatorService() { } /// <summary> /// Given a tree returns line separator spans. /// The operation may take fairly long time on a big tree so it is cancellable. /// </summary> public async Task<ImmutableArray<TextSpan>> GetLineSeparatorsAsync( Document document, TextSpan textSpan, CancellationToken cancellationToken) { var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var node = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder<TextSpan>.GetInstance(out var spans); var blocks = node.Traverse<SyntaxNode>(textSpan, IsSeparableContainer); foreach (var block in blocks) { if (cancellationToken.IsCancellationRequested) return []; switch (block) { case TypeDeclarationSyntax typeBlock: ProcessNodeList(typeBlock.Members, spans, cancellationToken); continue; case BaseNamespaceDeclarationSyntax namespaceBlock: ProcessUsings(namespaceBlock.Usings, spans, cancellationToken); ProcessNodeList(namespaceBlock.Members, spans, cancellationToken); continue; case CompilationUnitSyntax progBlock: ProcessUsings(progBlock.Usings, spans, cancellationToken); ProcessNodeList(progBlock.Members, spans, cancellationToken); break; } } return spans.ToImmutableAndClear(); } /// <summary>Node types that are interesting for line separation.</summary> private static bool IsSeparableBlock(SyntaxNode node) { if (SyntaxFacts.IsTypeDeclaration(node.Kind())) { return true; } switch (node.Kind()) { case SyntaxKind.NamespaceDeclaration: case SyntaxKind.MethodDeclaration: case SyntaxKind.PropertyDeclaration: case SyntaxKind.EventDeclaration: case SyntaxKind.IndexerDeclaration: case SyntaxKind.ConstructorDeclaration: case SyntaxKind.DestructorDeclaration: case SyntaxKind.OperatorDeclaration: case SyntaxKind.ConversionOperatorDeclaration: return true; default: return false; } } /// <summary>Node types that may contain separable blocks.</summary> private static bool IsSeparableContainer(SyntaxNode node) => node is TypeDeclarationSyntax or BaseNamespaceDeclarationSyntax or CompilationUnitSyntax; private static bool IsBadType(SyntaxNode node) { if (node is TypeDeclarationSyntax typeDecl) { if (typeDecl.OpenBraceToken.IsMissing || typeDecl.CloseBraceToken.IsMissing) { return true; } } return false; } private static bool IsBadEnum(SyntaxNode node) { if (node is EnumDeclarationSyntax enumDecl) { if (enumDecl.OpenBraceToken.IsMissing || enumDecl.CloseBraceToken.IsMissing) { return true; } } return false; } private static bool IsBadMethod(SyntaxNode node) { if (node is MethodDeclarationSyntax methodDecl) { if (methodDecl.Body != null && (methodDecl.Body.OpenBraceToken.IsMissing || methodDecl.Body.CloseBraceToken.IsMissing)) { return true; } } return false; } private static bool IsBadProperty(SyntaxNode node) => IsBadAccessorList(node as PropertyDeclarationSyntax); private static bool IsBadEvent(SyntaxNode node) => IsBadAccessorList(node as EventDeclarationSyntax); private static bool IsBadIndexer(SyntaxNode node) => IsBadAccessorList(node as IndexerDeclarationSyntax); private static bool IsBadAccessorList(BasePropertyDeclarationSyntax? baseProperty) { if (baseProperty?.AccessorList == null) return false; return baseProperty.AccessorList.OpenBraceToken.IsMissing || baseProperty.AccessorList.CloseBraceToken.IsMissing; } private static bool IsBadConstructor(SyntaxNode node) { if (node is ConstructorDeclarationSyntax constructorDecl) { if (constructorDecl.Body != null && (constructorDecl.Body.OpenBraceToken.IsMissing || constructorDecl.Body.CloseBraceToken.IsMissing)) { return true; } } return false; } private static bool IsBadDestructor(SyntaxNode node) { if (node is DestructorDeclarationSyntax destructorDecl) { if (destructorDecl.Body != null && (destructorDecl.Body.OpenBraceToken.IsMissing || destructorDecl.Body.CloseBraceToken.IsMissing)) { return true; } } return false; } private static bool IsBadOperator(SyntaxNode node) { if (node is OperatorDeclarationSyntax operatorDecl) { if (operatorDecl.Body != null && (operatorDecl.Body.OpenBraceToken.IsMissing || operatorDecl.Body.CloseBraceToken.IsMissing)) { return true; } } return false; } private static bool IsBadConversionOperator(SyntaxNode node) { if (node is ConversionOperatorDeclarationSyntax conversionDecl) { if (conversionDecl.Body != null && (conversionDecl.Body.OpenBraceToken.IsMissing || conversionDecl.Body.CloseBraceToken.IsMissing)) { return true; } } return false; } private static bool IsBadNode(SyntaxNode node) { if (node is IncompleteMemberSyntax) { return true; } if (IsBadType(node) || IsBadEnum(node) || IsBadMethod(node) || IsBadProperty(node) || IsBadEvent(node) || IsBadIndexer(node) || IsBadConstructor(node) || IsBadDestructor(node) || IsBadOperator(node) || IsBadConversionOperator(node)) { return true; } return false; } private static void ProcessUsings(SyntaxList<UsingDirectiveSyntax> usings, ArrayBuilder<TextSpan> spans, CancellationToken cancellationToken) { Contract.ThrowIfNull(spans); if (usings.Any()) { AddLineSeparatorSpanForNode(usings.Last(), spans, cancellationToken); } } /// <summary> /// If node is separable and not the last in its container => add line separator after the node /// If node is separable and not the first in its container => ensure separator before the node /// last separable node in Program needs separator after it. /// </summary> private static void ProcessNodeList<T>(SyntaxList<T> children, ArrayBuilder<TextSpan> spans, CancellationToken cancellationToken) where T : SyntaxNode { Contract.ThrowIfNull(spans); if (children.Count == 0) { // nothing to separate return; } // first child needs no separator var seenSeparator = true; for (var i = 0; i < children.Count - 1; i++) { cancellationToken.ThrowIfCancellationRequested(); var cur = children[i]; if (!IsSeparableBlock(cur)) { seenSeparator = false; } else { if (!seenSeparator) { var prev = children[i - 1]; AddLineSeparatorSpanForNode(prev, spans, cancellationToken); } AddLineSeparatorSpanForNode(cur, spans, cancellationToken); seenSeparator = true; } } // last child may need separator only before it var lastChild = children.Last(); if (IsSeparableBlock(lastChild)) { if (!seenSeparator) { var nextToLast = children[^2]; AddLineSeparatorSpanForNode(nextToLast, spans, cancellationToken); } if (lastChild.IsParentKind(SyntaxKind.CompilationUnit)) { AddLineSeparatorSpanForNode(lastChild, spans, cancellationToken); } } } private static void AddLineSeparatorSpanForNode(SyntaxNode node, ArrayBuilder<TextSpan> spans, CancellationToken cancellationToken) { if (IsBadNode(node)) { return; } var span = GetLineSeparatorSpanForNode(node); if (IsLegalSpanForLineSeparator(node.SyntaxTree, span, cancellationToken)) { spans.Add(span); } } private static bool IsLegalSpanForLineSeparator(SyntaxTree syntaxTree, TextSpan textSpan, CancellationToken cancellationToken) { // A span is a legal location for a line separator if the following line // contains only whitespace or the span is the last line in the buffer. var line = syntaxTree.GetText(cancellationToken).Lines.IndexOf(textSpan.End); if (line == syntaxTree.GetText(cancellationToken).Lines.Count - 1) { return true; } if (string.IsNullOrWhiteSpace(syntaxTree.GetText(cancellationToken).Lines[line + 1].ToString())) { return true; } return false; } private static TextSpan GetLineSeparatorSpanForNode(SyntaxNode node) { // we only want to underline the node with a long line // for this purpose the last token is as good as the whole node, but has // simpler and typically single line geometry (so it will be easier to find "bottom") return node.GetLastToken().Span; } }