diff --git a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs index 8baac6fe6c737..ab714e9c101fa 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeTracker.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; -using IVsAsyncFileChangeEx = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx; +using IVsAsyncFileChangeEx2 = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx2; using Microsoft.VisualStudio.Shell.Interop; using Roslyn.Utilities; @@ -112,7 +112,7 @@ public Task StartFileChangeListeningAsync() try { // TODO: Should we pass in cancellationToken here insead of CancellationToken.None? - return await ((IVsAsyncFileChangeEx)_fileChangeService).AdviseFileChangeAsync(_filePath, _fileChangeFlags, this, CancellationToken.None).ConfigureAwait(false); + return await ((IVsAsyncFileChangeEx2)_fileChangeService).AdviseFileChangeAsync(_filePath, _fileChangeFlags, this, CancellationToken.None).ConfigureAwait(false); } catch (Exception e) when (ReportException(e)) { diff --git a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs index 0aa18f88ed9c4..6e2208cb2dcfe 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs @@ -10,11 +10,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.ProjectSystem; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.Shell.Interop; using Roslyn.Utilities; -using IVsAsyncFileChangeEx = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx; +using IVsAsyncFileChangeEx2 = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx2; namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem { @@ -24,7 +25,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem /// internal sealed class FileChangeWatcher : IFileChangeWatcher { - private readonly Task _fileChangeService; + private readonly Task _fileChangeService; /// /// We create a batching queue of operations against the IVsFileChangeEx service for two reasons. First, we are obtaining the service asynchronously, and don't want to @@ -39,7 +40,7 @@ internal sealed class FileChangeWatcher : IFileChangeWatcher public FileChangeWatcher( IAsynchronousOperationListenerProvider listenerProvider, - Task fileChangeService) + Task fileChangeService) { _fileChangeService = fileChangeService; @@ -52,28 +53,34 @@ public FileChangeWatcher( CancellationToken.None); } - private async ValueTask ProcessBatchAsync(ImmutableSegmentedList workItems, CancellationToken cancellationToken) + private async ValueTask ProcessBatchAsync(ImmutableSegmentedList operations, CancellationToken cancellationToken) { var service = await _fileChangeService.ConfigureAwait(false); - var prior = WatcherOperation.Empty; - for (var i = 0; i < workItems.Count; i++) + for (var startIndex = 0; startIndex < operations.Count; startIndex++) { - if (prior.TryCombineWith(workItems[i], out var combined)) + var combinableEndIndex = FindCombinableRange(operations, startIndex); + + var combinedOp = WatcherOperation.CombineRange(operations, startIndex, combinableEndIndex); + + await combinedOp.ApplyAsync(service, cancellationToken).ConfigureAwait(false); + + startIndex = combinableEndIndex; + } + + return; + + static int FindCombinableRange(ImmutableSegmentedList operations, int startIndex) + { + var firstOp = operations[startIndex]; + for (var endIndex = startIndex + 1; endIndex < operations.Count; endIndex++) { - prior = combined; - continue; + if (!firstOp.CanCombineWith(operations[endIndex])) + return endIndex - 1; } - // The current item can't be combined with the prior item. Process the prior item before marking the - // current item as the new prior item. - await prior.ApplyAsync(service, cancellationToken).ConfigureAwait(false); - prior = workItems[i]; + return operations.Count - 1; } - - // The last item is always stored in prior rather than processing it directly. Make sure to process it - // before returning from the batch. - await prior.ApplyAsync(service, cancellationToken).ConfigureAwait(false); } public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirectories) @@ -82,7 +89,7 @@ public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirecto } /// - /// Represents an operation to subscribe or unsubscribe from events. The + /// Represents an operation to subscribe or unsubscribe from events. The /// values of the fields depends on the of the particular instance. /// private readonly struct WatcherOperation @@ -92,11 +99,6 @@ private readonly struct WatcherOperation /// private readonly Kind _kind; - /// - /// The path to subscribe to for or . - /// - private readonly string _directory; - /// /// The extension filter to apply for . This value may be /// to disable the extension filter. @@ -104,13 +106,13 @@ private readonly struct WatcherOperation private readonly string? _filter; /// - /// The file change flags to apply for . + /// The file change flags to apply for . /// private readonly _VSFILECHANGEFLAGS _fileChangeFlags; /// /// The instance to receive callback events for or - /// . + /// . /// private readonly IVsFreeThreadedFileChangeEvents2 _sink; @@ -122,66 +124,51 @@ private readonly struct WatcherOperation /// /// ⚠ Do not change this to another collection like ImmutableList<uint>. This collection /// references an instance held by the class, and the values are lazily read when - /// is called. + /// is called. /// private readonly List _cookies; /// - /// A file watcher token. The field is assigned by the - /// operation for , or read by the operation for . + /// A collection of file watcher tokens. The field is + /// assigned by the operation for , or read by the operation for + /// . /// - private readonly Context.RegularWatchedFile _token; + private readonly OneOrMany _tokens; /// - /// A collection of file watcher tokens to remove for . + /// A collection of file paths to subscribe to for or + /// . /// - private readonly IEnumerable _tokens; - - private WatcherOperation(Kind kind) - { - Contract.ThrowIfFalse(kind is Kind.None); - _kind = kind; - - // Other watching fields are not used for this kind - _directory = null!; - _filter = null; - _fileChangeFlags = 0; - _sink = null!; - _cookies = null!; - _token = null!; - _tokens = null!; - } + private readonly OneOrMany _paths; private WatcherOperation(Kind kind, string directory, string? filter, IVsFreeThreadedFileChangeEvents2 sink, List cookies) { Contract.ThrowIfFalse(kind is Kind.WatchDirectory); _kind = kind; - _directory = directory; + _paths = new OneOrMany(directory); _filter = filter; _sink = sink; _cookies = cookies; // Other watching fields are not used for this kind _fileChangeFlags = 0; - _token = null!; - _tokens = null!; + _tokens = OneOrMany.Empty; } - private WatcherOperation(Kind kind, string path, _VSFILECHANGEFLAGS fileChangeFlags, IVsFreeThreadedFileChangeEvents2 sink, Context.RegularWatchedFile token) + private WatcherOperation(Kind kind, OneOrMany files, _VSFILECHANGEFLAGS fileChangeFlags, IVsFreeThreadedFileChangeEvents2 sink, OneOrMany tokens) { - Contract.ThrowIfFalse(kind is Kind.WatchFile); + Contract.ThrowIfFalse(kind is Kind.WatchFiles); _kind = kind; - _directory = path; + _paths = files; _fileChangeFlags = fileChangeFlags; _sink = sink; - _token = token; + _tokens = tokens; // Other watching fields are not used for this kind _filter = null; _cookies = null!; - _tokens = null!; } private WatcherOperation(Kind kind, List cookies) @@ -192,15 +179,14 @@ private WatcherOperation(Kind kind, List cookies) _cookies = cookies; // Other watching fields are not used for this kind - _directory = null!; _filter = null; _fileChangeFlags = 0; _sink = null!; - _token = null!; - _tokens = null!; + _tokens = OneOrMany.Empty; + _paths = OneOrMany.Empty; } - private WatcherOperation(Kind kind, IEnumerable tokens) + private WatcherOperation(Kind kind, OneOrMany tokens) { Contract.ThrowIfFalse(kind is Kind.UnwatchFiles); _kind = kind; @@ -208,131 +194,122 @@ private WatcherOperation(Kind kind, IEnumerable toke _tokens = tokens; // Other watching fields are not used for this kind - _directory = null!; - _filter = null; - _fileChangeFlags = 0; - _sink = null!; - _cookies = null!; - _token = null!; - } - - private WatcherOperation(Kind kind, Context.RegularWatchedFile token) - { - Contract.ThrowIfFalse(kind is Kind.UnwatchFile); - _kind = kind; - - _token = token; - - // Other watching fields are not used for this kind - _directory = null!; _filter = null; _fileChangeFlags = 0; _sink = null!; _cookies = null!; - _tokens = null!; + _paths = OneOrMany.Empty; } private enum Kind { - None, WatchDirectory, - WatchFile, - UnwatchFile, + WatchFiles, UnwatchDirectories, UnwatchFiles, } - /// - /// Represents a watcher operation that takes no action when applied. This value intentionally has the same - /// representation as default(WatcherOperation). - /// - public static WatcherOperation Empty => new(Kind.None); - public static WatcherOperation WatchDirectory(string directory, string? filter, IVsFreeThreadedFileChangeEvents2 sink, List cookies) => new(Kind.WatchDirectory, directory, filter, sink, cookies); public static WatcherOperation WatchFile(string path, _VSFILECHANGEFLAGS fileChangeFlags, IVsFreeThreadedFileChangeEvents2 sink, Context.RegularWatchedFile token) - => new(Kind.WatchFile, path, fileChangeFlags, sink, token); + => new(Kind.WatchFiles, OneOrMany.Create(path), fileChangeFlags, sink, OneOrMany.Create(token)); + + public static WatcherOperation WatchFiles(ImmutableArray files, _VSFILECHANGEFLAGS fileChangeFlags, IVsFreeThreadedFileChangeEvents2 sink, ImmutableArray tokens) + => new(Kind.WatchFiles, new OneOrMany(files), fileChangeFlags, sink, new OneOrMany(tokens)); public static WatcherOperation UnwatchDirectories(List cookies) => new(Kind.UnwatchDirectories, cookies); - public static WatcherOperation UnwatchFiles(IEnumerable tokens) - => new(Kind.UnwatchFiles, tokens); + public static WatcherOperation UnwatchFiles(ImmutableArray tokens) + => new(Kind.UnwatchFiles, new OneOrMany(tokens)); public static WatcherOperation UnwatchFile(Context.RegularWatchedFile token) - => new(Kind.UnwatchFile, token); + => new(Kind.UnwatchFiles, OneOrMany.Create(token)); /// - /// Attempts to combine the current with the next operation in sequence. When - /// successful, is assigned a value which, when applied, performs an operation - /// equivalent to performing the current instance immediately followed by . + /// Combines instances between and + /// in . This input is assumed to have been pre-verified that all operations + /// within this range are combinable. The resultant value, when applied, performs an operation + /// equivalent to performing the specified range of operations consecutively. /// - /// The next operation to apply. - /// An operation representing the combined application of the current instance and - /// , in that order; otherwise, if the current operation cannot - /// be combined with . - /// if the current operation can be combined with ; - /// otherwise, . - public bool TryCombineWith(in WatcherOperation other, out WatcherOperation combined) + /// The collection containing the operations to combine. + /// Start index (inclusive) of operations to combine. + /// End index (inclusive) of operations to combine. + public static WatcherOperation CombineRange(ImmutableSegmentedList operations, int start, int end) { - if (other._kind == Kind.None) - { - combined = this; - return true; - } - else if (_kind == Kind.None) - { - combined = other; - return true; - } - - switch (_kind) - { - case Kind.WatchDirectory: - case Kind.WatchFile: - // Watching operations cannot be combined - break; + var firstOp = operations[start]; + if (start == end) + return firstOp; - case Kind.UnwatchFile when other._kind == Kind.UnwatchFile: - combined = UnwatchFiles(ImmutableList.Create(_token, other._token)); - return true; + using var _1 = ArrayBuilder.GetInstance(out var tokensBuilder); + using var _2 = ArrayBuilder.GetInstance(out var fileNamesBuilder); + using var _3 = ArrayBuilder.GetInstance(out var cookiesBuilder); - case Kind.UnwatchFile when other._kind == Kind.UnwatchFiles: - combined = UnwatchFiles(other._tokens.ToImmutableList().Insert(0, _token)); - return true; - - case Kind.UnwatchDirectories when other._kind == Kind.UnwatchDirectories: - var cookies = new List(_cookies); - cookies.AddRange(other._cookies); - combined = UnwatchDirectories(cookies); - return true; + for (; start <= end; start++) + { + var op = operations[start]; - case Kind.UnwatchFiles when other._kind == Kind.UnwatchFile: - combined = UnwatchFiles(_tokens.ToImmutableList().Add(other._token)); - return true; + switch (op._kind) + { + case Kind.WatchFiles: + for (var i = 0; i < op._paths.Count; i++) + { + fileNamesBuilder.Add(op._paths[i]); + } + + for (var i = 0; i < op._tokens.Count; i++) + { + tokensBuilder.Add(op._tokens[i]); + } + break; + + case Kind.UnwatchFiles: + for (var i = 0; i < op._tokens.Count; i++) + { + tokensBuilder.Add(op._tokens[i]); + } + break; + + case Kind.UnwatchDirectories: + cookiesBuilder.AddRange(op._cookies); + break; + + default: + break; + } + } - case Kind.UnwatchFiles when other._kind == Kind.UnwatchFiles: - combined = UnwatchFiles(_tokens.ToImmutableList().AddRange(other._tokens)); - return true; + return firstOp._kind switch + { + Kind.WatchFiles => + WatchFiles(fileNamesBuilder.ToImmutable(), firstOp._fileChangeFlags, firstOp._sink, tokensBuilder.ToImmutable()), + Kind.UnwatchFiles => + UnwatchFiles(tokensBuilder.ToImmutable()), + Kind.UnwatchDirectories => + UnwatchDirectories(cookiesBuilder.ToList()), + _ => + throw ExceptionUtilities.Unreachable() + }; + } - default: - break; - } + public bool CanCombineWith(in WatcherOperation other) + { + // Watching directory operation cannot be combined + if (_kind == Kind.WatchDirectory) + return false; - combined = default; - return false; + return (_kind == other._kind); } - public async ValueTask ApplyAsync(IVsAsyncFileChangeEx service, CancellationToken cancellationToken) + public async ValueTask ApplyAsync(IVsAsyncFileChangeEx2 service, CancellationToken cancellationToken) { switch (_kind) { - case Kind.None: - return; - case Kind.WatchDirectory: - var cookie = await service.AdviseDirChangeAsync(_directory, watchSubdirectories: true, _sink, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfTrue(_paths.Count != 1); + + var cookie = await service.AdviseDirChangeAsync(_paths[0], watchSubdirectories: true, _sink, cancellationToken).ConfigureAwait(false); _cookies.Add(cookie); if (_filter != null) @@ -340,12 +317,13 @@ public async ValueTask ApplyAsync(IVsAsyncFileChangeEx service, CancellationToke return; - case Kind.WatchFile: - _token.Cookie = await service.AdviseFileChangeAsync(_directory, _fileChangeFlags, _sink, cancellationToken).ConfigureAwait(false); - return; + case Kind.WatchFiles: + var cookies = await service.AdviseFileChangesAsync(_paths.ToImmutable(), _fileChangeFlags, _sink, cancellationToken).ConfigureAwait(false); + + Contract.ThrowIfTrue(cookies.Length != _tokens.Count); + for (var i = 0; i < cookies.Length; i++) + _tokens[i].Cookie = cookies[i]; - case Kind.UnwatchFile: - await service.UnadviseFileChangeAsync(_token.Cookie!.Value, cancellationToken).ConfigureAwait(false); return; case Kind.UnwatchDirectories: @@ -354,7 +332,6 @@ public async ValueTask ApplyAsync(IVsAsyncFileChangeEx service, CancellationToke return; case Kind.UnwatchFiles: - Contract.ThrowIfFalse(_tokens is not null); await service.UnadviseFileChangesAsync(_tokens.Select(token => token.Cookie!.Value).ToArray(), cancellationToken).ConfigureAwait(false); return; @@ -410,7 +387,7 @@ public void Dispose() } _fileChangeWatcher._taskQueue.AddWork(WatcherOperation.UnwatchDirectories(_directoryWatchCookies)); - _fileChangeWatcher._taskQueue.AddWork(WatcherOperation.UnwatchFiles(_activeFileWatchingTokens)); + _fileChangeWatcher._taskQueue.AddWork(WatcherOperation.UnwatchFiles(_activeFileWatchingTokens.ToImmutableArray())); } public IWatchedFile EnqueueWatchingFile(string filePath) diff --git a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcherProvider.cs b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcherProvider.cs index 270c5af020704..eac6119e8d0b6 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcherProvider.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcherProvider.cs @@ -11,7 +11,7 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; -using IVsAsyncFileChangeEx = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx; +using IVsAsyncFileChangeEx2 = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx2; namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem { @@ -30,7 +30,7 @@ public FileChangeWatcherProvider( { await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(threadingContext.DisposalToken); - var fileChangeService = (IVsAsyncFileChangeEx?)await serviceProvider.GetServiceAsync(typeof(SVsFileChangeEx)).ConfigureAwait(true); + var fileChangeService = (IVsAsyncFileChangeEx2?)await serviceProvider.GetServiceAsync(typeof(SVsFileChangeEx)).ConfigureAwait(true); Assumes.Present(fileChangeService); return fileChangeService; }, diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioRuleSetTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioRuleSetTests.vb index 51d1439c752b9..7633df2dc6236 100644 --- a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioRuleSetTests.vb +++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioRuleSetTests.vb @@ -11,7 +11,7 @@ Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework Imports Roslyn.Test.Utilities -Imports IVsAsyncFileChangeEx = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx +Imports IVsAsyncFileChangeEx2 = Microsoft.VisualStudio.Shell.IVsAsyncFileChangeEx2 Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim @@ -48,7 +48,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim File.WriteAllText(ruleSetPath, ruleSetSource) Dim fileChangeService = New MockVsFileChangeEx - Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx)(fileChangeService)) + Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx2)(fileChangeService)) Dim ruleSetManager = New VisualStudioRuleSetManager(workspace.ExportProvider.GetExportedValue(Of IThreadingContext), fileChangeWatcher, AsynchronousOperationListenerProvider.NullListener) Using visualStudioRuleSet = ruleSetManager.GetOrCreateRuleSet(ruleSetPath) @@ -92,7 +92,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Using workspace = New TestWorkspace() Dim fileChangeService = New MockVsFileChangeEx - Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx)(fileChangeService)) + Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx2)(fileChangeService)) Dim ruleSetManager = New VisualStudioRuleSetManager(workspace.ExportProvider.GetExportedValue(Of IThreadingContext), fileChangeWatcher, AsynchronousOperationListenerProvider.NullListener) Using visualStudioRuleSet = ruleSetManager.GetOrCreateRuleSet(ruleSetPath) @@ -136,7 +136,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Using workspace = New TestWorkspace() Dim fileChangeService = New MockVsFileChangeEx - Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx)(fileChangeService)) + Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx2)(fileChangeService)) Dim listenerProvider = workspace.ExportProvider.GetExportedValue(Of IAsynchronousOperationListenerProvider) Dim listener = listenerProvider.GetListener("test") @@ -177,7 +177,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Using workspace = New TestWorkspace() Dim fileChangeService = New MockVsFileChangeEx - Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx)(fileChangeService)) + Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx2)(fileChangeService)) Dim listenerProvider = workspace.ExportProvider.GetExportedValue(Of IAsynchronousOperationListenerProvider) Dim listener = listenerProvider.GetListener("test") @@ -226,7 +226,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Using workspace = New TestWorkspace() Dim fileChangeService = New MockVsFileChangeEx - Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx)(fileChangeService)) + Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx2)(fileChangeService)) Dim ruleSetManager = New VisualStudioRuleSetManager(workspace.ExportProvider.GetExportedValue(Of IThreadingContext), fileChangeWatcher, AsynchronousOperationListenerProvider.NullListener) Using ruleSet1 = ruleSetManager.GetOrCreateRuleSet(ruleSetPath) @@ -264,7 +264,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Using workspace = New TestWorkspace() Dim fileChangeService = New MockVsFileChangeEx - Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx)(fileChangeService)) + Dim fileChangeWatcher = New FileChangeWatcher(workspace.GetService(Of IAsynchronousOperationListenerProvider)(), Task.FromResult(Of IVsAsyncFileChangeEx2)(fileChangeService)) Dim ruleSetManager = New VisualStudioRuleSetManager(workspace.ExportProvider.GetExportedValue(Of IThreadingContext), fileChangeWatcher, AsynchronousOperationListenerProvider.NullListener) Using ruleSet = ruleSetManager.GetOrCreateRuleSet(ruleSetPath) diff --git a/src/VisualStudio/TestUtilities2/MockVsFileChangeEx.vb b/src/VisualStudio/TestUtilities2/MockVsFileChangeEx.vb index a2b4911de42e0..2f825f6d9f1d2 100644 --- a/src/VisualStudio/TestUtilities2/MockVsFileChangeEx.vb +++ b/src/VisualStudio/TestUtilities2/MockVsFileChangeEx.vb @@ -12,7 +12,7 @@ Imports Task = System.Threading.Tasks.Task Namespace Microsoft.VisualStudio.LanguageServices.UnitTests Friend Class MockVsFileChangeEx Implements IVsFileChangeEx - Implements IVsAsyncFileChangeEx + Implements IVsAsyncFileChangeEx2 Private ReadOnly _lock As New Object Private _watchedFiles As ImmutableList(Of WatchedEntity) = ImmutableList(Of WatchedEntity).Empty @@ -150,6 +150,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests 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