/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/VisualBasic/LineCommit/CommitBufferManager.vb
352 строки
16 KB
Cyrus Najmabadi
Organize VB imports
27 июн 2025, 21:53
27 июн 2025, 21:53
e9c47fd
Код
Авторство
О чём код?
' 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.Threading Imports Microsoft.CodeAnalysis.Text Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Projection Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.LineCommit ''' <summary> ''' This class watches for buffer-based events, tracks the dirty regions, and invokes the formatter as appropriate ''' </summary> Partial Friend Class CommitBufferManager Private ReadOnly _buffer As ITextBuffer Private ReadOnly _commitFormatter As ICommitFormatter Private ReadOnly _inlineRenameService As IInlineRenameService Private _referencingViews As Integer ''' <summary> ''' An object to use as a sync lock for <see cref="_referencingViews"/>. ''' </summary> Private ReadOnly _referencingViewsLock As Object = New Object() ''' <summary> ''' The tracking span which is the currently "dirty" region in the buffer. May be null if there is no dirty region. ''' </summary> Private _dirtyState As DirtyState Private _documentBeforePreviousEdit As Document ''' <summary> ''' The number of times BeginSuppressingCommits() has been called. ''' </summary> Private _suppressions As Integer Public Sub New( buffer As ITextBuffer, commitFormatter As ICommitFormatter, inlineRenameService As IInlineRenameService) Contract.ThrowIfNull(buffer) Contract.ThrowIfNull(commitFormatter) Contract.ThrowIfNull(inlineRenameService) _buffer = buffer _commitFormatter = commitFormatter _inlineRenameService = inlineRenameService End Sub Public Sub AddReferencingView() SyncLock _referencingViewsLock _referencingViews += 1 If _referencingViews = 1 Then AddHandler _buffer.Changing, AddressOf OnTextBufferChanging AddHandler _buffer.Changed, AddressOf OnTextBufferChanged Dim projectionBuffer = TryCast(_buffer, IProjectionBuffer) If projectionBuffer IsNot Nothing Then AddHandler projectionBuffer.SourceSpansChanged, AddressOf OnSourceSpansChanged End If End If End SyncLock End Sub Public Sub RemoveReferencingView() SyncLock _referencingViewsLock ' If someone enables line commit with a file already open, we might end up decrementing ' the ref count too many times, so only do work if we are still above 0. If _referencingViews > 0 Then _referencingViews -= 1 If _referencingViews = 0 Then RemoveHandler _buffer.Changed, AddressOf OnTextBufferChanged RemoveHandler _buffer.Changing, AddressOf OnTextBufferChanging Dim projectionBuffer = TryCast(_buffer, IProjectionBuffer) If projectionBuffer IsNot Nothing Then RemoveHandler projectionBuffer.SourceSpansChanged, AddressOf OnSourceSpansChanged End If End If End If End SyncLock End Sub Public ReadOnly Property HasDirtyRegion As Boolean Get Return _dirtyState IsNot Nothing End Get End Property ''' <summary> ''' Commits any dirty region, if one exists. ''' ''' To improve perf, passing false to isExplicitFormat will avoid semantic checks when expanding ''' the formatting span to an entire block ''' </summary> Public Sub CommitDirty(isExplicitFormat As Boolean, cancellationToken As CancellationToken) If _inlineRenameService.ActiveSession IsNot Nothing Then _dirtyState = Nothing Return End If If _dirtyState Is Nothing Then Return End If ' Is something else in the commit process suppressing the commit? If _suppressions > 0 Then Return End If Try ' Start to suppress commits to ensure we don't have any sort of re-entrancy in this process. ' We've seen bugs (17015) where waits triggered by some computation might re-enter. Using BeginSuppressingCommits() ' It's possible that an edit may already be in progress. In this scenario, there's ' really nothing we can do, so we'll just skip the format If _buffer.EditInProgress Then Return End If Dim dirtyRegion = _dirtyState.DirtyRegion.GetSpan(_buffer.CurrentSnapshot) Dim info As FormattingInfo If Not TryComputeExpandedSpanToFormat(dirtyRegion, info, cancellationToken) Then Return End If Dim useSemantics = info.UseSemantics If useSemantics AndAlso Not isExplicitFormat Then ' Avoid using semantics for formatting extremely large dirty spans without an explicit request ' from the user. The "large span threshold" is 7000 lines. The 7000 line threshold is an ' estimated value accounting for a lower-bound of the algorithmic complexity of text ' differencing in designer cases along with measurements of a pathological example demonstrated ' at 14000 lines. We expect Windows Forms designer formatting operations to run in under ~15 ' seconds on average current hardware when nearing the threshold. Dim startLineNumber = 0 Dim startCharIndex = 0 Dim endLineNumber = 0 Dim endCharIndex = 0 info.SpanToFormat.GetLinesAndCharacters(startLineNumber, startCharIndex, endLineNumber, endCharIndex) If endLineNumber - startLineNumber > 7000 Then useSemantics = False End If End If Dim tree = _dirtyState.BaseDocument.GetSyntaxTreeSynchronously(cancellationToken) _commitFormatter.CommitRegion(info.SpanToFormat, isExplicitFormat, useSemantics, dirtyRegion, _dirtyState.BaseSnapshot, tree, cancellationToken) End Using Finally ' We may have tracked a dirty region while committing or it may have been aborted. ' In any case, we want to guarantee we have no dirty region once we're done _dirtyState = Nothing End Try End Sub Private Structure FormattingInfo Public UseSemantics As Boolean Public SpanToFormat As SnapshotSpan End Structure Public Sub ExpandDirtyRegion(snapshotSpan As SnapshotSpan) If _dirtyState Is Nothing Then Dim document = snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges() If document IsNot Nothing Then _dirtyState = New DirtyState(snapshotSpan, snapshotSpan.Snapshot, document) End If Else _dirtyState = _dirtyState.WithExpandedDirtySpan(snapshotSpan) End If End Sub Private Shared Function TryComputeExpandedSpanToFormat(dirtySpan As SnapshotSpan, ByRef formattingInfo As FormattingInfo, cancellationToken As CancellationToken) As Boolean Dim document = dirtySpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges() If document Is Nothing Then Return False End If formattingInfo.UseSemantics = True Dim tree = document.GetSyntaxTreeSynchronously(cancellationToken) ' No matter what, we will always include the dirty span Dim finalSpanStart = dirtySpan.Start.Position Dim finalSpanEnd = dirtySpan.End.Position ' Find the containing statements Dim startingStatementInfo = ContainingStatementInfo.GetInfo(dirtySpan.Start, tree, cancellationToken) If startingStatementInfo IsNot Nothing Then finalSpanStart = Math.Min(finalSpanStart, startingStatementInfo.TextSpan.Start) If startingStatementInfo.MatchingBlockConstruct IsNot Nothing Then ' If we're expanding backwards because of editing an end construct, we don't wan to run ' expensive semantic formatting checks. We really just want to fix up indentation. formattingInfo.UseSemantics = finalSpanStart <= startingStatementInfo.MatchingBlockConstruct.SpanStart finalSpanStart = Math.Min(finalSpanStart, startingStatementInfo.MatchingBlockConstruct.SpanStart) End If End If Dim endingStatementInfo = If(ContainingStatementInfo.GetInfo(dirtySpan.End, tree, cancellationToken), startingStatementInfo) If endingStatementInfo IsNot Nothing Then finalSpanEnd = Math.Max(finalSpanEnd, endingStatementInfo.TextSpan.End) If endingStatementInfo.MatchingBlockConstruct IsNot Nothing Then finalSpanEnd = Math.Max(finalSpanEnd, endingStatementInfo.MatchingBlockConstruct.Span.End) End If End If Dim startingLine = dirtySpan.Snapshot.GetLineFromPosition(finalSpanStart) If startingLine.LineNumber = 0 Then finalSpanStart = 0 Else ' We want to include the line break into the line before finalSpanStart = dirtySpan.Snapshot.GetLineFromLineNumber(startingLine.LineNumber - 1).End End If formattingInfo.SpanToFormat = New SnapshotSpan(dirtySpan.Snapshot, Span.FromBounds(finalSpanStart, finalSpanEnd)) Return True End Function Public Shared Function IsMovementBetweenStatements(oldPoint As SnapshotPoint, newPoint As SnapshotPoint, cancellationToken As CancellationToken) As Boolean ' If they are the same line, then definitely no If oldPoint.GetContainingLineNumber() = newPoint.GetContainingLineNumber() Then Return False End If Dim document = newPoint.Snapshot.GetOpenDocumentInCurrentContextWithChanges() If document Is Nothing Then Return False End If Dim tree = document.GetSyntaxTreeSynchronously(cancellationToken) Dim oldStatement = ContainingStatementInfo.GetInfo(oldPoint, tree, cancellationToken) Dim newStatement = ContainingStatementInfo.GetInfo(newPoint, tree, cancellationToken) If oldStatement Is Nothing AndAlso newStatement Is Nothing Then Return True End If If (oldStatement Is Nothing) <> (newStatement Is Nothing) Then Return True End If Return oldStatement.TextSpan <> newStatement.TextSpan End Function Private Sub OnTextBufferChanging(sender As Object, e As TextContentChangingEventArgs) If _dirtyState Is Nothing Then ' Grab the current document for the text buffer before it changes so we can get any ' cached versions Dim documentBeforePreviousEdit = e.Before.GetOpenDocumentInCurrentContextWithChanges() If documentBeforePreviousEdit IsNot Nothing Then _documentBeforePreviousEdit = documentBeforePreviousEdit ' Kick off a task to eagerly force compute InternalsVisibleTo semantics for all the references. ' This provides a noticeable perf improvement when code cleanup is subsequently invoked on this document. Task.Run(Async Function() Await ForceComputeInternalsVisibleToAsync(documentBeforePreviousEdit, CancellationToken.None).ConfigureAwait(False) End Function) End If End If End Sub Private Sub OnTextBufferChanged(sender As Object, e As TextContentChangedEventArgs) ' Before we do anything else, ensure the field is nulled back out Dim documentBeforePreviousEdit = _documentBeforePreviousEdit _documentBeforePreviousEdit = Nothing If e.Changes.Count = 0 Then Return End If ' If this is a reiterated version, then it's part of undo/redo and we should ignore it If e.AfterVersion.ReiteratedVersionNumber <> e.AfterVersion.VersionNumber Then Return End If ' Add this region into our dirty region Dim encompassingNewSpan = New SnapshotSpan(e.After, Span.FromBounds(e.Changes.First().NewPosition, e.Changes.Last().NewEnd)) If _dirtyState Is Nothing Then ' Some times, we won't get a documentBeforePreviousEdit. This happens because sometimes OnTextBufferChanging ' isn't called before OnTextBufferChanged in undo scenarios. In those cases, since we can't set up valid state, ' just throw it out If documentBeforePreviousEdit IsNot Nothing Then _dirtyState = New DirtyState(encompassingNewSpan, e.Before, documentBeforePreviousEdit) End If Else _dirtyState = _dirtyState.WithExpandedDirtySpan(encompassingNewSpan) End If End Sub Private Sub OnSourceSpansChanged(sender As Object, e As ProjectionSourceSpansChangedEventArgs) ' DirtyState information should be purged when source spans change, as the new buffer content ' may be unrelated to prior content. Aspx and legacy razor may generate significant differences ' during generations (eg, converting a <%= to a <%# in aspx), causing unnecessary (and troublesome) large ' dirty state ranges. _dirtyState = Nothing End Sub Private Shared Async Function ForceComputeInternalsVisibleToAsync(document As Document, cancellationToken As CancellationToken) As Task Dim project = document.Project Dim compilation = Await project.GetCompilationAsync(cancellationToken).ConfigureAwait(False) For Each reference In project.ProjectReferences Dim refProject = project.Solution.GetProject(reference.ProjectId) If refProject IsNot Nothing Then Dim refCompilation = Await refProject.GetCompilationAsync(cancellationToken).ConfigureAwait(False) refCompilation.Assembly().GivesAccessTo(compilation.Assembly) End If Next For Each reference In project.MetadataReferences Dim refAssemblyOrModule = compilation.GetAssemblyOrModuleSymbol(reference) If refAssemblyOrModule.MatchesKind(SymbolKind.Assembly) Then Dim refAssembly = DirectCast(refAssemblyOrModule, IAssemblySymbol) refAssembly.GivesAccessTo(compilation.Assembly) End If Next End Function ''' <summary> ''' Suppresses future commits, causing all calls to CommitDirty() to be a simple no-op, even ''' if there is a dirty span. ''' </summary> ''' <returns>An IDisposable that should be disposed when the caller wants to resume ''' submissions.</returns> Friend Function BeginSuppressingCommits() As IDisposable _suppressions += 1 Return New SuppressionHandle(Me) End Function Private Class SuppressionHandle Implements IDisposable Private _manager As CommitBufferManager Public Sub New(manager As CommitBufferManager) Contract.ThrowIfNull(manager) _manager = manager End Sub Public Sub Dispose() Implements IDisposable.Dispose _manager._suppressions -= 1 _manager = Nothing End Sub End Class End Class End Namespace