File: LineCommit\CommitFormatter.vb
Web Access
Project: src\src\EditorFeatures\VisualBasic\Microsoft.CodeAnalysis.VisualBasic.EditorFeatures.vbproj (Microsoft.CodeAnalysis.VisualBasic.EditorFeatures)
' 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.
 
Imports System.Collections.Immutable
Imports System.ComponentModel.Composition
Imports System.Threading
Imports Microsoft.CodeAnalysis.AddImport
Imports Microsoft.CodeAnalysis.CodeCleanup
Imports Microsoft.CodeAnalysis.CodeCleanup.Providers
Imports Microsoft.CodeAnalysis.Formatting
Imports Microsoft.CodeAnalysis.Formatting.Rules
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.Internal.Log
Imports Microsoft.CodeAnalysis.Options
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.VisualStudio.Text
 
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.LineCommit
    <Export(GetType(ICommitFormatter))>
    Friend Class CommitFormatter
        Implements ICommitFormatter
 
        Private Shared ReadOnly s_codeCleanupPredicate As Func(Of ICodeCleanupProvider, Boolean) =
            Function(p)
                Return p.Name <> PredefinedCodeCleanupProviderNames.Simplification AndAlso
                       p.Name <> PredefinedCodeCleanupProviderNames.Format
            End Function
 
        Private ReadOnly _editorOptionsService As EditorOptionsService
 
        <ImportingConstructor>
        <Obsolete(MefConstruction.ImportingConstructorMessage, True)>
        Public Sub New(editorOptionsService As EditorOptionsService)
            _editorOptionsService = editorOptionsService
        End Sub
 
        Public Sub CommitRegion(spanToFormat As SnapshotSpan,
                                isExplicitFormat As Boolean,
                                useSemantics As Boolean,
                                dirtyRegion As SnapshotSpan,
                                baseSnapshot As ITextSnapshot,
                                baseTree As SyntaxTree,
                                cancellationToken As CancellationToken) Implements ICommitFormatter.CommitRegion
 
            Using Logger.LogBlock(FunctionId.LineCommit_CommitRegion, cancellationToken)
                Dim buffer = spanToFormat.Snapshot.TextBuffer
                Dim currentSnapshot = buffer.CurrentSnapshot
 
                ' make sure things are current
                spanToFormat = spanToFormat.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeInclusive)
                dirtyRegion = dirtyRegion.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeInclusive)
 
                ' Use frozen partial semantics here.  We're operating on the UI thread, and we don't want to block the
                ' user indefinitely while getting full semantics for this projects (which can require building all
                ' projects we depend on).
                Dim document = currentSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken)
                If document Is Nothing Then
                    Return
                End If
 
                If Not (isExplicitFormat OrElse _editorOptionsService.GlobalOptions.GetOption(LineCommitOptionsStorage.PrettyListing, LanguageNames.VisualBasic)) Then
                    Return
                End If
 
                Dim tree = document.GetSyntaxTreeSynchronously(cancellationToken)
 
                Dim textSpanToFormat = spanToFormat.Span.ToTextSpan()
                If AbortForDiagnostics(tree, cancellationToken) Then
                    Return
                End If
 
                ' create commit formatting cleanup provider that has line commit specific behavior
                Dim cleanupOptions = buffer.GetCodeCleanupOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), document.Project.Services, isExplicitFormat, allowImportsInHiddenRegions:=document.AllowImportsInHiddenRegions())
                Dim commitFormattingCleanup = GetCommitFormattingCleanupProvider(
                    document.Id,
                    document.Project.Services,
                    cleanupOptions.FormattingOptions,
                    spanToFormat,
                    baseSnapshot,
                    baseTree,
                    dirtyRegion,
                    tree,
                    cancellationToken)
 
                Dim codeCleanups = CodeCleaner.GetDefaultProviders(document).
                                               WhereAsArray(s_codeCleanupPredicate).
                                               Concat(commitFormattingCleanup)
 
                Dim cleanupService = document.GetRequiredLanguageService(Of ICodeCleanerService)
 
                Dim finalDocument As Document
                If useSemantics OrElse isExplicitFormat Then
                    finalDocument = cleanupService.CleanupAsync(
                        document,
                        ImmutableArray.Create(textSpanToFormat),
                        cleanupOptions,
                        codeCleanups,
                        cancellationToken).WaitAndGetResult(cancellationToken)
                Else
                    Dim root = document.GetSyntaxRootSynchronously(cancellationToken)
 
                    Dim newRoot = cleanupService.CleanupAsync(
                        root,
                        ImmutableArray.Create(textSpanToFormat),
                        cleanupOptions.FormattingOptions,
                        document.Project.Solution.Services,
                        codeCleanups,
                        cancellationToken).WaitAndGetResult(cancellationToken)
 
                    If root Is newRoot Then
                        finalDocument = document
                    Else
                        Dim text As SourceText = Nothing
                        If newRoot.SyntaxTree IsNot Nothing AndAlso newRoot.SyntaxTree.TryGetText(text) Then
                            finalDocument = document.WithText(text)
                        Else
                            finalDocument = document.WithSyntaxRoot(newRoot)
                        End If
                    End If
                End If
 
                Dim changes = finalDocument.GetTextChangesAsync(document, cancellationToken).WaitAndGetResult(cancellationToken)
                buffer.ApplyChanges(changes)
            End Using
        End Sub
 
        Private Shared Function AbortForDiagnostics(tree As SyntaxTree, cancellationToken As CancellationToken) As Boolean
            Const UnterminatedStringId = "BC30648"
 
            ' If we have any unterminated strings that overlap what we're trying to format, then
            ' bail out.  It's quite likely the unterminated string will cause a bunch of code to
            ' swap between real code and string literals, and committing will just cause problems.
            Dim diagnostics = tree.GetDiagnostics(cancellationToken).Where(
                    Function(d) d.Descriptor.Id = UnterminatedStringId)
 
            Return diagnostics.Any()
        End Function
 
        Private Shared Function GetCommitFormattingCleanupProvider(
            documentId As DocumentId,
            languageServices As CodeAnalysis.Host.LanguageServices,
            options As SyntaxFormattingOptions,
            spanToFormat As SnapshotSpan,
            oldSnapshot As ITextSnapshot,
            oldTree As SyntaxTree,
            newDirtySpan As SnapshotSpan,
            newTree As SyntaxTree,
            cancellationToken As CancellationToken) As ICodeCleanupProvider
 
            Dim oldDirtySpan = newDirtySpan.TranslateTo(oldSnapshot, SpanTrackingMode.EdgeInclusive)
 
            ' based on changes made to dirty spans, get right formatting rules to apply
            Dim rules = GetFormattingRules(documentId, languageServices, options, spanToFormat, oldDirtySpan, oldTree, newDirtySpan, newTree, cancellationToken)
 
            Return New FormatCodeCleanupProvider(rules)
        End Function
 
        Private Shared Function GetFormattingRules(
            documentId As DocumentId,
            languageServices As CodeAnalysis.Host.LanguageServices,
            options As SyntaxFormattingOptions,
            spanToFormat As SnapshotSpan,
            oldDirtySpan As SnapshotSpan,
            oldTree As SyntaxTree,
            newDirtySpan As SnapshotSpan,
            newTree As SyntaxTree,
            cancellationToken As CancellationToken) As ImmutableArray(Of AbstractFormattingRule)
 
            ' if the span we are going to format is same as the span that got changed, don't bother to do anything special.
            ' just do full format of the span.
            If spanToFormat = newDirtySpan Then
                Return Formatter.GetDefaultFormattingRules(languageServices)
            End If
 
            If oldTree Is Nothing OrElse newTree Is Nothing Then
                Return Formatter.GetDefaultFormattingRules(languageServices)
            End If
 
            ' TODO: remove this in dev14
            '
            ' workaround for VB razor case.
            ' if we are under VB razor, we always use anchor operation otherwise, due to our double formatting, everything will just get messed.
            ' this is really a hacky workaround we should remove this in dev14
            Dim formattingRuleService = languageServices.SolutionServices.GetService(Of IHostDependentFormattingRuleFactoryService)()
            If formattingRuleService IsNot Nothing Then
                If formattingRuleService.ShouldUseBaseIndentation(documentId) Then
                    Return Formatter.GetDefaultFormattingRules(languageServices)
                End If
            End If
 
            ' when commit formatter formats given span, it formats the span with or without anchor operations.
            ' the way we determine which formatting rules are used for the span is based on whether the region user has changed would change indentation
            ' following the dirty (committed) region. if indentation has changed, we will format with anchor operations. if not, we will format without anchor operations.
            '
            ' for example, for the code below
            '[          ]|If True And
            '                   False Then|
            '                 Dim a = 1
            ' if the [] is changed, when line commit runs, it sees indentation right after the commit (|If .. Then|) is same, so formatter will run without anchor operations,
            ' meaning, "False Then" will stay as it is even if "If True And" is moved due to change in []
            '
            ' if the [] is changed to
            '[       If True Then
            '      ]|If True And
            '              False Then|
            '            Dim a = 1
            ' when line commit runs, it sees that indentation after the commit is changed (due to inserted "If True Then"), so formatter runs with anchor operations,
            ' meaning, "False Then" will move along with "If True And"
            '
            ' for now, do very simple checking. basically, we see whether we get same number of indent operation for the give span. alternative, but little bit
            ' more expensive and complex, we can actually calculate indentation right after the span, and see whether that is changed. not sure whether that much granularity
            ' is needed.
            Dim coreRules = Formatter.GetDefaultFormattingRules(languageServices)
 
            If GetNumberOfIndentOperations(languageServices, options, oldTree, oldDirtySpan, cancellationToken) =
               GetNumberOfIndentOperations(languageServices, options, newTree, newDirtySpan, cancellationToken) Then
                Dim result = New FixedSizeArrayBuilder(Of AbstractFormattingRule)(coreRules.Length + 1)
                result.Add(New NoAnchorFormatterRule())
                result.AddRange(coreRules)
                Return result.MoveToImmutable()
            End If
 
            Return coreRules
        End Function
 
        Private Shared Function GetNumberOfIndentOperations(
            languageServices As CodeAnalysis.Host.LanguageServices,
            options As SyntaxFormattingOptions,
            syntaxTree As SyntaxTree,
            span As SnapshotSpan,
            cancellationToken As CancellationToken) As Integer
 
            ' find containing statement of the end point, and use its end point as position to get indent operation
            Dim containingStatement = ContainingStatementInfo.GetInfo(span.End, syntaxTree, cancellationToken)
            Dim endPosition = If(containingStatement Is Nothing, span.End.Position + 1, containingStatement.TextSpan.End + 1)
 
            ' get token right after given span
            Dim token = syntaxTree.GetRoot(cancellationToken).FindToken(Math.Min(endPosition, syntaxTree.GetRoot(cancellationToken).FullSpan.End))
 
            Dim node = token.Parent
 
            ' collect all indent operation
            Dim operations = New List(Of IndentBlockOperation)()
            While node IsNot Nothing
                operations.AddRange(FormattingOperations.GetIndentBlockOperations(
                                    Formatter.GetDefaultFormattingRules(languageServices), node, options))
                node = node.Parent
            End While
 
            ' get number of indent operation that affects the token.
            Return operations.Where(Function(o) o.TextSpan.Contains(token.SpanStart)).Count()
        End Function
 
        Private Class NoAnchorFormatterRule
            Inherits CompatAbstractFormattingRule
 
            Public Overrides Sub AddAnchorIndentationOperationsSlow(list As List(Of AnchorIndentationOperation), node As SyntaxNode, ByRef nextOperation As NextAnchorIndentationOperationAction)
                ' no anchor/relative formatting
                Return
            End Sub
        End Class
    End Class
End Namespace