File: MockVsFileChangeEx.vb
Web Access
Project: src\src\VisualStudio\TestUtilities2\Microsoft.VisualStudio.LanguageServices.Test.Utilities2.vbproj (Microsoft.VisualStudio.LanguageServices.Test.Utilities2)
' 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.Runtime.InteropServices
Imports System.Threading
Imports Microsoft.VisualStudio.Shell
Imports Microsoft.VisualStudio.Shell.Interop
Imports Task = System.Threading.Tasks.Task
 
Namespace Microsoft.VisualStudio.LanguageServices.UnitTests
    Friend Class MockVsFileChangeEx
        Implements IVsFileChangeEx
        Implements IVsAsyncFileChangeEx2
 
        Private ReadOnly _lock As New Object
        Private _watchedFiles As ImmutableList(Of WatchedEntity) = ImmutableList(Of WatchedEntity).Empty
        Private _watchedDirectories As ImmutableList(Of WatchedEntity) = ImmutableList(Of WatchedEntity).Empty
        Private _nextCookie As UInteger
 
        Public Function AdviseDirChange(pszDir As String, fWatchSubDir As Integer, pFCE As IVsFileChangeEvents, ByRef pvsCookie As UInteger) As Integer Implements IVsFileChangeEx.AdviseDirChange
            If fWatchSubDir = 0 Then
                Throw New NotImplementedException("Watching a single directory but not subdirectories is not implemented in this mock.")
            End If
 
            pvsCookie = AdviseDirectoryOrFileChange(_watchedDirectories, pszDir, pFCE)
            Return VSConstants.S_OK
        End Function
 
        Public Function AdviseFileChange(pszMkDocument As String, grfFilter As UInteger, pFCE As IVsFileChangeEvents, ByRef pvsCookie As UInteger) As Integer Implements IVsFileChangeEx.AdviseFileChange
            If (grfFilter And _VSFILECHANGEFLAGS.VSFILECHG_Time) <> _VSFILECHANGEFLAGS.VSFILECHG_Time Then
                Throw New NotImplementedException()
            End If
 
            pvsCookie = AdviseDirectoryOrFileChange(_watchedFiles, pszMkDocument, pFCE)
            Return VSConstants.S_OK
        End Function
 
        Private Function AdviseDirectoryOrFileChange(ByRef watchedList As ImmutableList(Of WatchedEntity),
                                                     pszMkDocument As String,
                                                     pFCE As IVsFileChangeEvents) As UInteger
 
            SyncLock _lock
                Dim cookie = _nextCookie
                watchedList = watchedList.Add(New WatchedEntity(cookie, pszMkDocument, DirectCast(pFCE, IVsFreeThreadedFileChangeEvents2)))
                _nextCookie += 1UI
 
                Return cookie
            End SyncLock
        End Function
 
        Public Function IgnoreFile(VSCOOKIE As UInteger, pszMkDocument As String, fIgnore As Integer) As Integer Implements IVsFileChangeEx.IgnoreFile
            Throw New NotImplementedException()
        End Function
 
        Public Function SyncFile(pszMkDocument As String) As Integer Implements IVsFileChangeEx.SyncFile
            Throw New NotImplementedException()
        End Function
 
        Public Function UnadviseDirChange(VSCOOKIE As UInteger) As Integer Implements IVsFileChangeEx.UnadviseDirChange
            SyncLock _lock
                _watchedDirectories = _watchedDirectories.RemoveAll(Function(t) t.Cookie = VSCOOKIE)
 
                Return VSConstants.S_OK
            End SyncLock
        End Function
 
        Public Function UnadviseFileChange(VSCOOKIE As UInteger) As Integer Implements IVsFileChangeEx.UnadviseFileChange
            SyncLock _lock
                _watchedFiles = _watchedFiles.RemoveAll(Function(t) t.Cookie = VSCOOKIE)
 
                Return VSConstants.S_OK
            End SyncLock
        End Function
 
        Public Sub FireUpdate(filename As String)
            FireUpdate(filename, _watchedFiles, _watchedDirectories)
        End Sub
 
        ''' <summary>
        ''' Raises a file change that raised after <paramref name="unsubscribingAction"/> is ran.
        ''' </summary>
        ''' <remarks>
        ''' File change notifications are inherently asynchronous -- it's always possible that a file change
        ''' notification may have been started for something we unsubscribe a moment later -- there's always a window of time
        ''' between the final check and the entry into Roslyn code which is unavoidable as long as notifications aren't called
        ''' by the shell under a lock, which they aren't for deadlock reasons.
        ''' </remarks>
        Public Sub FireStaleUpdate(filename As String, unsubscribingAction As Action)
            Dim watchedFiles = _watchedFiles
            Dim watchedDirectories = _watchedDirectories
 
            unsubscribingAction()
 
            FireUpdate(filename, watchedFiles, watchedDirectories)
        End Sub
 
        Private Shared Sub FireUpdate(filename As String, watchedFiles As ImmutableList(Of WatchedEntity), watchedDirectories As ImmutableList(Of WatchedEntity))
            Dim actionsToFire As List(Of Action) = New List(Of Action)()
 
            For Each watchedFile In watchedFiles
                If String.Equals(watchedFile.Path, filename, StringComparison.OrdinalIgnoreCase) Then
                    actionsToFire.Add(Sub()
                                          watchedFile.Sink.FilesChanged(1, {watchedFile.Path}, {CType(_VSFILECHANGEFLAGS.VSFILECHG_Time, UInteger)})
                                      End Sub)
                End If
            Next
 
            For Each watchedDirectory In watchedDirectories
                If FileNameMatchesFilter(filename, watchedDirectory) Then
                    actionsToFire.Add(Sub()
                                          watchedDirectory.Sink.DirectoryChangedEx2(watchedDirectory.Path, 1, {filename}, {CType(_VSFILECHANGEFLAGS.VSFILECHG_Time, UInteger)})
                                      End Sub)
                End If
            Next
 
            If actionsToFire.Count > 0 Then
                For Each actionToFire In actionsToFire
                    actionToFire()
                Next
            Else
                Throw New InvalidOperationException($"There is no subscription for file {filename}. Is the test authored correctly?")
            End If
        End Sub
 
        Private Shared Function FileNameMatchesFilter(filename As String, watchedDirectory As WatchedEntity) As Boolean
            If Not filename.StartsWith(watchedDirectory.Path, StringComparison.OrdinalIgnoreCase) Then
                Return False
            End If
 
            If watchedDirectory.ExtensionFilters Is Nothing Then
                ' We have no extension filter, so good
                Return True
            End If
 
            For Each extension In watchedDirectory.ExtensionFilters
                If filename.EndsWith(extension) Then
                    Return True
                End If
            Next
 
            ' Didn't match the extension
            Return False
        End Function
 
        Public Function AdviseFileChangeAsync(filename As String, filter As _VSFILECHANGEFLAGS, sink As IVsFreeThreadedFileChangeEvents2, Optional cancellationToken As CancellationToken = Nothing) As Task(Of UInteger) Implements IVsAsyncFileChangeEx.AdviseFileChangeAsync
            Dim cookie As UInteger
            Marshal.ThrowExceptionForHR(AdviseFileChange(filename, CType(filter, UInteger), sink, cookie))
            Return Task.FromResult(cookie)
        End Function
 
        Public Function AdviseFileChangesAsync(filenames As IReadOnlyCollection(Of String), filter As _VSFILECHANGEFLAGS, sink As IVsFreeThreadedFileChangeEvents2, cancellationToken As CancellationToken) As Task(Of UInteger()) Implements IVsAsyncFileChangeEx2.AdviseFileChangesAsync
            Dim cookies As New List(Of UInteger)()
 
            SyncLock _lock
                For Each filename In filenames
                    Dim cookie As UInteger
                    Marshal.ThrowExceptionForHR(AdviseFileChange(filename, CType(filter, UInteger), sink, cookie))
 
                    cookies.Add(cookie)
                Next
            End SyncLock
 
            Return Task.FromResult(cookies.ToArray())
        End Function
 
        Public Function UnadviseFileChangeAsync(cookie As UInteger, Optional cancellationToken As CancellationToken = Nothing) As Task(Of String) Implements IVsAsyncFileChangeEx.UnadviseFileChangeAsync
            SyncLock _lock
                Dim path = _watchedFiles.FirstOrDefault(Function(t) t.Cookie = cookie).Path
 
                Marshal.ThrowExceptionForHR(UnadviseFileChange(cookie))
 
                Return Task.FromResult(path)
            End SyncLock
        End Function
 
        Public Function UnadviseFileChangesAsync(cookies As IReadOnlyCollection(Of UInteger), Optional cancellationToken As CancellationToken = Nothing) As Task(Of String()) Implements IVsAsyncFileChangeEx.UnadviseFileChangesAsync
            Dim paths As New List(Of String)()
 
            SyncLock _lock
                For Each cookie In cookies
                    Dim path = _watchedFiles.FirstOrDefault(Function(t) t.Cookie = cookie).Path
 
                    Marshal.ThrowExceptionForHR(UnadviseFileChange(cookie))
 
                    paths.Add(path)
                Next
            End SyncLock
 
            Return Task.FromResult(paths.ToArray())
        End Function
 
        Public Function AdviseDirChangeAsync(directory As String, watchSubdirectories As Boolean, sink As IVsFreeThreadedFileChangeEvents2, Optional cancellationToken As CancellationToken = Nothing) As Task(Of UInteger) Implements IVsAsyncFileChangeEx.AdviseDirChangeAsync
            Dim cookie As UInteger
            Marshal.ThrowExceptionForHR(AdviseDirChange(directory, If(watchSubdirectories, 1, 0), sink, cookie))
            Return Task.FromResult(cookie)
        End Function
 
        Public Function UnadviseDirChangeAsync(cookie As UInteger, Optional cancellationToken As CancellationToken = Nothing) As Task(Of String) Implements IVsAsyncFileChangeEx.UnadviseDirChangeAsync
            SyncLock _lock
                Dim path = _watchedFiles.FirstOrDefault(Function(t) t.Cookie = cookie).Path
 
                Marshal.ThrowExceptionForHR(UnadviseFileChange(cookie))
 
                Return Task.FromResult(path)
            End SyncLock
        End Function
 
        Public Function UnadviseDirChangesAsync(cookies As IReadOnlyCollection(Of UInteger), Optional cancellationToken As CancellationToken = Nothing) As Task(Of String()) Implements IVsAsyncFileChangeEx.UnadviseDirChangesAsync
            Dim paths As New List(Of String)()
 
            SyncLock _lock
                For Each cookie In cookies
                    Dim path = _watchedFiles.FirstOrDefault(Function(t) t.Cookie = cookie).Path
 
                    Marshal.ThrowExceptionForHR(UnadviseFileChange(cookie))
 
                    paths.Add(path)
                Next
            End SyncLock
 
            Return Task.FromResult(paths.ToArray())
        End Function
 
        Public Function SyncFileAsync(filename As String, Optional cancellationToken As CancellationToken = Nothing) As Tasks.Task Implements IVsAsyncFileChangeEx.SyncFileAsync
            Throw New NotImplementedException()
        End Function
 
        Public Function IgnoreFileAsync(cookie As UInteger, filename As String, ignore As Boolean, Optional cancellationToken As CancellationToken = Nothing) As Tasks.Task Implements IVsAsyncFileChangeEx.IgnoreFileAsync
            Throw New NotImplementedException()
        End Function
 
        Public Function IgnoreDirAsync(directory As String, ignore As Boolean, Optional cancellationToken As CancellationToken = Nothing) As Tasks.Task Implements IVsAsyncFileChangeEx.IgnoreDirAsync
            Throw New NotImplementedException()
        End Function
 
        Public Function FilterDirectoryChangesAsync(cookie As UInteger, extensions As String(), cancellationToken As CancellationToken) As Task Implements IVsAsyncFileChangeEx.FilterDirectoryChangesAsync
            _watchedDirectories.FirstOrDefault(Function(t) t.Cookie = cookie).ExtensionFilters = extensions
            Return Task.CompletedTask
        End Function
 
        Public ReadOnly Property WatchedFileCount As Integer
            Get
                SyncLock _lock
                    Return _watchedFiles.Count
                End SyncLock
            End Get
        End Property
    End Class
 
    Friend Class WatchedEntity
        Public ReadOnly Cookie As UInteger
        Public ReadOnly Path As String
        Public ReadOnly Sink As IVsFreeThreadedFileChangeEvents2
        Public ExtensionFilters As String()
 
        Public Sub New(cookie As UInteger, path As String, sink As IVsFreeThreadedFileChangeEvents2)
            Me.Cookie = cookie
            Me.Path = path
            Me.Sink = sink
        End Sub
    End Class
End Namespace