From 26c36962fbd02868369a89da1c893278f4f14f88 Mon Sep 17 00:00:00 2001 From: IXLLEGACYIXL <107197024+IXLLEGACYIXL@users.noreply.github.com> Date: Thu, 1 Feb 2024 19:53:41 +0100 Subject: [PATCH] Repair projectwatcher (#2106) * use positive 77 * repair projectwatcher * remove unused line * undo -77 change * undo -77 * order usings * fix formatting * remove unused solution * remove unused async * use previous cancelation method * remove extra task * add check to not throw assembly changes away * rework distribution of assemblychanges * remove unused using * add broadcast back in * remove assembly broadcast * add cancelation * replace cancelation location * improve if nesting * improve naming, fix reload on new references * fix loading chain of assets * refactor * [Editor] Refactor initialization of CodeViewModel --------- Co-authored-by: IXLLEGACYIXL Co-authored-by: Nicolas Musset --- .../Stride.Core.Assets/PackageSession.cs | 85 +++++----- .../AssetEditors/ProjectWatcher.cs | 66 +++++--- .../ViewModel/CodeViewModel.cs | 156 ++++++++---------- .../ViewModels/DebuggingViewModel.cs | 65 ++++---- 4 files changed, 191 insertions(+), 181 deletions(-) diff --git a/sources/assets/Stride.Core.Assets/PackageSession.cs b/sources/assets/Stride.Core.Assets/PackageSession.cs index 1887a17ac9..7649dc9845 100644 --- a/sources/assets/Stride.Core.Assets/PackageSession.cs +++ b/sources/assets/Stride.Core.Assets/PackageSession.cs @@ -19,10 +19,12 @@ using Stride.Core.Packages; using Stride.Core.Reflection; using Stride.Core.Serialization; +using static Stride.Core.Assets.PackageSession; using ILogger = Stride.Core.Diagnostics.ILogger; namespace Stride.Core.Assets { + record AssetLoadingInfo(PackageSession session, ILogger log, Package package, PackageLoadParameters loadParameters, List pendingPackageUpgrades, PackageLoadParameters newLoadParameters); public abstract class PackageContainer { public PackageContainer([NotNull] Package package) @@ -955,7 +957,7 @@ public void LoadMissingAssets(ILogger log, IEnumerable packages, Packag var loadParameters = loadParametersArg ?? PackageLoadParameters.Default(); var cancelToken = loadParameters.CancelToken; - + List assetLoadInfos = []; // Make a copy of Packages as it can be modified by PreLoadPackageDependencies foreach (var package in packages) { @@ -964,8 +966,12 @@ public void LoadMissingAssets(ILogger log, IEnumerable packages, Packag { return; } - - TryLoadAssets(this, log, package, loadParameters); + if(TryLoadAssemblies(this, log, package, loadParameters,out var assetInfo)) + assetLoadInfos.Add(assetInfo); + } + foreach (AssetLoadingInfo assetInfo in assetLoadInfos) + { + LoadAssets(assetInfo.session,assetInfo.log,assetInfo.package, assetInfo.loadParameters,assetInfo.pendingPackageUpgrades,assetInfo.newLoadParameters); } } @@ -1389,11 +1395,12 @@ private Package PreLoadPackage(ILogger log, string filePath, PackageLoadParamete return null; } - private bool TryLoadAssets(PackageSession session, ILogger log, Package package, PackageLoadParameters loadParameters) + private bool TryLoadAssemblies(PackageSession session, ILogger log, Package package, PackageLoadParameters loadParameters, out AssetLoadingInfo info) { + info = null; // Already loaded if (package.State >= PackageState.AssetsReady) - return true; + return false; // Dependencies could not properly be loaded if (package.State < PackageState.DependenciesReady) @@ -1405,16 +1412,7 @@ private bool TryLoadAssets(PackageSession session, ILogger log, Package package, try { - // First, check that dependencies have their assets loaded - bool dependencyError = false; - foreach (var dependency in package.FindDependencies(false)) - { - if (!TryLoadAssets(session, log, dependency, loadParameters)) - dependencyError = true; - } - if (dependencyError) - return false; // Get pending package upgrades if (!pendingPackageUpgradesPerPackage.TryGetValue(package, out var pendingPackageUpgrades)) @@ -1502,44 +1500,47 @@ private bool TryLoadAssets(PackageSession session, ILogger log, Package package, // Mark package as dirty package.IsDirty = true; } + info = new AssetLoadingInfo(session, log, package, loadParameters, pendingPackageUpgrades, newLoadParameters); + return true; + } + catch (Exception ex) + { + log.Error($"Error while pre-loading package [{package}]", ex); + return false; + } + } - // Load assets - package.LoadAssets(log, newLoadParameters); + private static void LoadAssets(PackageSession session, ILogger log, Package package, PackageLoadParameters loadParameters, List pendingPackageUpgrades, PackageLoadParameters newLoadParameters) + { + // Load assets + package.LoadAssets(log, newLoadParameters); - // Validate assets from package - package.ValidateAssets(newLoadParameters.GenerateNewAssetIds, newLoadParameters.RemoveUnloadableObjects, log); + // Validate assets from package + package.ValidateAssets(newLoadParameters.GenerateNewAssetIds, newLoadParameters.RemoveUnloadableObjects, log); - if (pendingPackageUpgrades.Count > 0) + if (pendingPackageUpgrades.Count > 0) + { + // Perform post asset load upgrade + foreach (var pendingPackageUpgrade in pendingPackageUpgrades) { - // Perform post asset load upgrade - foreach (var pendingPackageUpgrade in pendingPackageUpgrades) + var packageUpgrader = pendingPackageUpgrade.PackageUpgrader; + var dependencyPackage = pendingPackageUpgrade.DependencyPackage; + if (!packageUpgrader.UpgradeAfterAssetsLoaded(loadParameters, session, log, package, pendingPackageUpgrade.Dependency, dependencyPackage, pendingPackageUpgrade.DependencyVersionBeforeUpgrade)) { - var packageUpgrader = pendingPackageUpgrade.PackageUpgrader; - var dependencyPackage = pendingPackageUpgrade.DependencyPackage; - if (!packageUpgrader.UpgradeAfterAssetsLoaded(loadParameters, session, log, package, pendingPackageUpgrade.Dependency, dependencyPackage, pendingPackageUpgrade.DependencyVersionBeforeUpgrade)) - { - log.Error($"Error while upgrading package [{package.Meta.Name}] for [{dependencyPackage.Meta.Name}] from version [{pendingPackageUpgrade.Dependency.Version}] to [{dependencyPackage.Meta.Version}]"); - return false; - } + log.Error($"Error while upgrading package [{package.Meta.Name}] for [{dependencyPackage.Meta.Name}] from version [{pendingPackageUpgrade.Dependency.Version}] to [{dependencyPackage.Meta.Version}]"); + return; } - - // Mark package as dirty - package.IsDirty = true; } - // Mark package as ready - package.State = PackageState.AssetsReady; + // Mark package as dirty + package.IsDirty = true; + } - // Freeze the package after loading the assets - session.FreezePackage(package); + // Mark package as ready + package.State = PackageState.AssetsReady; - return true; - } - catch (Exception ex) - { - log.Error($"Error while pre-loading package [{package}]", ex); - return false; - } + // Freeze the package after loading the assets + session.FreezePackage(package); } private static PackageUpgrader CheckPackageUpgrade(ILogger log, Package dependentPackage, PackageDependency dependency, Package dependencyPackage) diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/ProjectWatcher.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/ProjectWatcher.cs index 2485075d6e..b6dbe3dcdc 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/ProjectWatcher.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/ProjectWatcher.cs @@ -4,15 +4,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Specialized; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; -using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using Stride.Core.Assets; using Stride.Core.Assets.Editor.ViewModel; @@ -22,7 +21,6 @@ using Stride.Core.Extensions; using Stride.Assets.Presentation.AssetEditors.ScriptEditor; using Project = Microsoft.CodeAnalysis.Project; -using System.Diagnostics; namespace Stride.Assets.Presentation.AssetEditors { @@ -51,7 +49,7 @@ public AssemblyChangedEvent(PackageLoadedAssembly assembly, AssemblyChangeType c public Project Project { get; set; } } - + public class ProjectWatcher : IDisposable { private readonly TrackingCollection trackedAssemblies; @@ -65,7 +63,7 @@ public class ProjectWatcher : IDisposable private Project gameExecutable; private CancellationTokenSource batchChangesCancellationTokenSource = new CancellationTokenSource(); - private Task batchChangesTask; + public IAsyncEnumerable> BatchChange; private MSBuildWorkspace msbuildWorkspace; @@ -86,25 +84,51 @@ public ProjectWatcher(SessionViewModel session, bool trackBinaries = true) fileChangedLink1 = fileChanged.LinkTo(fileChangedTransform); fileChangedLink2 = fileChangedTransform.LinkTo(AssemblyChangedBroadcast); - batchChangesTask = BatchChanges(); + BatchChange = BatchChanges(); } - private async Task BatchChanges() + private async IAsyncEnumerable> BatchChanges() { var buffer = new BufferBlock(); using (AssemblyChangedBroadcast.LinkTo(buffer)) { - while (true) + while (!batchChangesCancellationTokenSource.IsCancellationRequested) { var hasChanged = false; var assemblyChanges = new List(); do { var assemblyChange = await buffer.ReceiveAsync(batchChangesCancellationTokenSource.Token); - if (assemblyChange != null) - assemblyChanges.Add(assemblyChange); - if (assemblyChange != null && !hasChanged) + if (assemblyChange == null) + continue; + + assemblyChanges.Add(assemblyChange); + var project = assemblyChange.Project; + var referencedProjects = msbuildWorkspace.CurrentSolution.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(project.Id); + foreach (var referenceProject in referencedProjects) + { + var foundProject = msbuildWorkspace.CurrentSolution.GetProject(referenceProject); + if(foundProject is null) + continue; + var assemblyName = foundProject.AssemblyName; + var target = trackedAssemblies.FirstOrDefault(x => x.Project.AssemblyName == assemblyName); + if (target != null) + { + string file = assemblyChange.ChangedFile; + if (assemblyChange.ChangeType == AssemblyChangeType.Binary) + { + file = target.LoadedAssembly.Path; + } + else if (assemblyChange.ChangeType == AssemblyChangeType.Project) + { + file = target.Project.FilePath; + } + assemblyChanges.Add(new AssemblyChangedEvent(target.LoadedAssembly, assemblyChange.ChangeType, file, target.Project)); + } + } + + if (!hasChanged) { // After the first change, wait for more changes await Task.Delay(TimeSpan.FromMilliseconds(500), batchChangesCancellationTokenSource.Token); @@ -116,13 +140,11 @@ private async Task BatchChanges() // Merge files that were modified multiple time assemblyChanges = assemblyChanges.GroupBy(x => x.ChangedFile).Select(x => x.Last()).ToList(); - AssembliesChangedBroadcast.Post(assemblyChanges); + yield return assemblyChanges; } } } - public BroadcastBlock AssemblyChangedBroadcast { get; } = new BroadcastBlock(null); - public BroadcastBlock> AssembliesChangedBroadcast { get; } = new BroadcastBlock>(null); public Project CurrentGameLibrary { @@ -145,7 +167,6 @@ public Project CurrentGameLibrary public void Dispose() { batchChangesCancellationTokenSource.Cancel(); - batchChangesTask.Wait(); directoryWatcher.Dispose(); fileChangedLink1.Dispose(); @@ -197,12 +218,12 @@ public async Task ReceiveAndDiscardChanges(TimeSpan batchInterval, CancellationT } } - private async Task FileChangeTransformation(FileEvent e) + private async Task FileChangeTransformation(FileEvent e) { string changedFile; var renameEvent = e as FileRenameEvent; changedFile = renameEvent?.OldFullPath ?? e.FullPath; - + foreach (var trackedAssembly in trackedAssemblies) { // Report change of the assembly binary @@ -214,9 +235,9 @@ private async Task FileChangeTransformation(FileEvent e) // Also check for .cs file changes (DefaultItems auto import *.cs, with some excludes such as obj subfolder) // TODO: Check actual unevaluated .csproj to get the auto includes/excludes? if (needProjectReload == false - && ((e.ChangeType == FileEventChangeType.Deleted ||e.ChangeType == FileEventChangeType.Renamed || e.ChangeType == FileEventChangeType.Created) + && ((e.ChangeType == FileEventChangeType.Deleted || e.ChangeType == FileEventChangeType.Renamed || e.ChangeType == FileEventChangeType.Created) && Path.GetExtension(changedFile)?.ToLowerInvariant() == ".cs" - && changedFile.StartsWith(Path.GetDirectoryName(trackedAssembly.Project.FilePath),StringComparison.OrdinalIgnoreCase))) + && changedFile.StartsWith(Path.GetDirectoryName(trackedAssembly.Project.FilePath), StringComparison.OrdinalIgnoreCase))) { needProjectReload = true; } @@ -312,7 +333,7 @@ private async Task TrackPackage(PackageViewModel package) directoryWatcher.Track(loadedAssembly.ProjectReference.Location); var trackedAssembly = new TrackedAssembly { Package = package, LoadedAssembly = loadedAssembly }; - + // Track project source code if (await UpdateProject(trackedAssembly)) trackedAssemblies.Add(trackedAssembly); @@ -358,17 +379,16 @@ private async Task OpenProject(UFile projectPath) var host = await RoslynHost; msbuildWorkspace = MSBuildWorkspace.Create(ImmutableDictionary.Empty, host.HostServices); } + await msbuildWorkspace.OpenSolutionAsync(session.SolutionPath.ToWindowsPath()); - msbuildWorkspace.CloseSolution(); - // Try up to 10 times (1 second) const int retryCount = 10; for (var i = retryCount - 1; i >= 0; --i) { try { - var project = await msbuildWorkspace.OpenProjectAsync(projectPath.ToWindowsPath()); + var project = msbuildWorkspace.CurrentSolution.Projects.FirstOrDefault(x => x.FilePath == projectPath.ToWindowsPath()); if (msbuildWorkspace.Diagnostics.Count > 0) { // There was an issue compiling the project diff --git a/sources/editor/Stride.Assets.Presentation/ViewModel/CodeViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModel/CodeViewModel.cs index ca057127d8..7f300ef0a5 100644 --- a/sources/editor/Stride.Assets.Presentation/ViewModel/CodeViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/ViewModel/CodeViewModel.cs @@ -1,30 +1,27 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; using System.Windows.Documents; using System.Windows.Media; using Microsoft.CodeAnalysis; -using RoslynPad.Roslyn; -using Stride.Core.Assets.Editor.Services; -using Stride.Core.Extensions; -using Stride.Core.Presentation.Services; using Stride.Assets.Presentation.AssetEditors; using Stride.Assets.Presentation.AssetEditors.ScriptEditor; -using RoslynWorkspace = Stride.Assets.Presentation.AssetEditors.ScriptEditor.RoslynWorkspace; -using System.Collections.Generic; -using System.IO; +using Stride.Assets.Scripts; using Stride.Core.Assets; using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Extensions; using Stride.Core.IO; using Stride.Core.Presentation.Dirtiables; -using Stride.Core.Translation; -using Stride.Assets.Scripts; -using System.Collections.Specialized; +using Stride.Core.Presentation.Services; using Stride.Core.Presentation.ViewModels; +using Stride.Core.Translation; +using RoslynWorkspace = Stride.Assets.Presentation.AssetEditors.ScriptEditor.RoslynWorkspace; namespace Stride.Assets.Presentation.ViewModel { @@ -43,112 +40,105 @@ public class CodeViewModel : DispatcherViewModel, IDisposable /// public const int MaximumEditorFontSize = 72; - private readonly Task projectWatcherTask; - private readonly Task workspaceTask; + private readonly TaskCompletionSource projectWatcherCompletion = new(); + private readonly TaskCompletionSource workspaceCompletion = new(); private int editorFontSize = ScriptEditorSettings.FontSize.GetValue(); // default size - private Brush keywordBrush; - private Brush typeBrush; + private readonly Brush keywordBrush; + private readonly Brush typeBrush; - public CodeViewModel(StrideAssetsViewModel strideAssetsViewModel) : base(strideAssetsViewModel.SafeArgument(nameof(strideAssetsViewModel)).ServiceProvider) + public CodeViewModel(StrideAssetsViewModel strideAssetsViewModel) + : base(strideAssetsViewModel.SafeArgument(nameof(strideAssetsViewModel)).ServiceProvider) { - projectWatcherTask = Task.Run(async () => - { - var result = new ProjectWatcher(strideAssetsViewModel.Session); - await result.Initialize(); - return result; - }); + _ = InitializeAsync(strideAssetsViewModel); - workspaceTask = projectWatcherTask.Result.RoslynHost.ContinueWith(roslynHost => roslynHost.Result.Workspace); + // Apply syntax highlighting for tooltips + keywordBrush = new SolidColorBrush(ClassificationHighlightColorsDark.KeywordColor); + typeBrush = new SolidColorBrush(ClassificationHighlightColorsDark.TypeColor); + keywordBrush.Freeze(); + typeBrush.Freeze(); - workspaceTask = workspaceTask.ContinueWith(workspaceTask => + // TODO: Update with latest RoslynPad + //SymbolDisplayPartExtensions.StyleRunFromSymbolDisplayPartKind = StyleRunFromSymbolDisplayPartKind; + //SymbolDisplayPartExtensions.StyleRunFromTextTag = StyleRunFromTextTag; + } + + private async Task InitializeAsync(StrideAssetsViewModel strideAssetsViewModel) + { + var projectWatcher = new ProjectWatcher(strideAssetsViewModel.Session); + await projectWatcher.Initialize(); + var workspace = (await projectWatcher.RoslynHost).Workspace; + + // Load and update roslyn workspace with latest compiled version + foreach (var trackedAssembly in projectWatcher.TrackedAssemblies) { - var projectWatcher = projectWatcherTask.Result; - var workspace = workspaceTask.Result; + if (trackedAssembly.Project is { } project) + workspace.AddOrUpdateProject(project); + } + projectWatcher.TrackedAssemblies.CollectionChanged += TrackedAssembliesCollectionChanged; - // Load and update roslyn workspace with latest compiled version - foreach (var trackedAssembly in projectWatcher.TrackedAssemblies) - { - var project = trackedAssembly.Project; - if (project != null) - workspace.AddOrUpdateProject(project); - } + _ = UpdateProjects(projectWatcher, workspace, strideAssetsViewModel); - void TrackedAssemblies_CollectionChanged(object sender, Core.Collections.TrackingCollectionChangedEventArgs e) + projectWatcherCompletion.SetResult(projectWatcher); + workspaceCompletion.SetResult(workspace); + + void TrackedAssembliesCollectionChanged(object sender, Core.Collections.TrackingCollectionChangedEventArgs e) + { + if (((ProjectWatcher.TrackedAssembly)e.Item).Project is { } project) { - var project = ((ProjectWatcher.TrackedAssembly)e.Item).Project; - if (project != null) + switch (e.Action) { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Add: { workspace.AddOrUpdateProject(project); break; } - case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Remove: { workspace.RemoveProject(project.Id); break; } - } } } - projectWatcher.TrackedAssemblies.CollectionChanged += TrackedAssemblies_CollectionChanged; + } + } - // TODO: Right now, we simply replace the solution with newly loaded one - // Ideally, we should keep our existing solution and update it to follow external changes after initial loading (similar to VisualStudioWorkspace) - // This should provide better integration with background changes and local changes - projectWatcher.AssembliesChangedBroadcast.LinkTo(new ActionBlock>(events => + private async Task UpdateProjects(ProjectWatcher projectWatcher, RoslynWorkspace workspace, StrideAssetsViewModel strideAssetsViewModel) + { + await foreach (var events in projectWatcher.BatchChange) + { + await Dispatcher.InvokeAsync(async () => { - if (events.Count == 0) - return; - - Dispatcher.InvokeAsync(async () => + // Update projects + foreach (var e in events.Where(x => x.ChangeType == AssemblyChangeType.Project)) { - // Update projects - foreach (var e in events.Where(x => x.ChangeType == AssemblyChangeType.Project)) - { - var project = e.Project; - if (project != null) - { - await ReloadProject(strideAssetsViewModel.Session, project); - } - } - - // Update files - foreach (var e in events.Where(x => x.ChangeType == AssemblyChangeType.Source)) + var project = e.Project; + if (project != null) { - var documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(e.ChangedFile).FirstOrDefault(); - if (documentId != null) - workspace.HostDocumentTextLoaderChanged(documentId, new FileTextLoader(e.ChangedFile, null)); + await ReloadProject(strideAssetsViewModel.Session, project); } - }).Wait(); - }), new DataflowLinkOptions()); - - return workspace; - }); - - // Apply syntax highlighting for tooltips - keywordBrush = new SolidColorBrush(ClassificationHighlightColorsDark.KeywordColor); - typeBrush = new SolidColorBrush(ClassificationHighlightColorsDark.TypeColor); - keywordBrush.Freeze(); - typeBrush.Freeze(); + } - // TODO: Update with latest RoslynPad - //SymbolDisplayPartExtensions.StyleRunFromSymbolDisplayPartKind = StyleRunFromSymbolDisplayPartKind; - //SymbolDisplayPartExtensions.StyleRunFromTextTag = StyleRunFromTextTag; + // Update files + foreach (var e in events.Where(x => x.ChangeType == AssemblyChangeType.Source)) + { + var documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(e.ChangedFile).FirstOrDefault(); + if (documentId != null) + workspace.HostDocumentTextLoaderChanged(documentId, new FileTextLoader(e.ChangedFile, null)); + } + }); + } } /// /// Gets the project watcher which tracks source code changes on the disk; it is created asychronously. /// - public Task ProjectWatcher => projectWatcherTask; + public Task ProjectWatcher => projectWatcherCompletion.Task; /// /// Gets the roslyn workspace; it is created asynchronously. /// - public Task Workspace => workspaceTask; + public Task Workspace => workspaceCompletion.Task; /// /// The editor current font size. It will be saved in the settings. @@ -364,7 +354,7 @@ void IDisposable.Dispose() private void Cleanup() { - projectWatcherTask.Dispose(); + // nothing for now } private void StyleRunFromSymbolDisplayPartKind(SymbolDisplayPartKind partKind, Run run) diff --git a/sources/editor/Stride.GameStudio/ViewModels/DebuggingViewModel.cs b/sources/editor/Stride.GameStudio/ViewModels/DebuggingViewModel.cs index 3ca8587214..af31616ee6 100644 --- a/sources/editor/Stride.GameStudio/ViewModels/DebuggingViewModel.cs +++ b/sources/editor/Stride.GameStudio/ViewModels/DebuggingViewModel.cs @@ -37,7 +37,7 @@ namespace Stride.GameStudio.ViewModels { public class DebuggingViewModel : DispatcherViewModel, IDisposable { - + private const int LookUpFrequency = 25; private readonly IDebugService debugService; private readonly GameStudioViewModel editor; private readonly Dictionary modifiedAssemblies; @@ -160,51 +160,50 @@ private void Cleanup() private async void PullAssemblyChanges([NotNull] ProjectWatcher projectWatcher) { - var changesBuffer = new BufferBlock(); - using (projectWatcher.AssemblyChangedBroadcast.LinkTo(changesBuffer)) + await foreach (var events in projectWatcher.BatchChange) { - while (!assemblyTrackingCancellation.IsCancellationRequested) + foreach (var assemblyChange in events) { - var assemblyChange = await changesBuffer.ReceiveAsync(assemblyTrackingCancellation.Token); - - if (!trackAssemblyChanges || assemblyChange == null) + if (assemblyChange == null || assemblyChange.ChangeType == AssemblyChangeType.Binary) continue; - - // Ignore Binary changes - if (assemblyChange.ChangeType == AssemblyChangeType.Binary) - continue; - - var shouldNotify = !assemblyChangesPending; modifiedAssemblies[assemblyChange.Assembly] = new ModifiedAssembly { LoadedAssembly = assemblyChange.Assembly, ChangeType = assemblyChange.ChangeType, Project = assemblyChange.Project }; + } - Dispatcher.Invoke(() => - { - UpdateCommands(); + var shouldNotify = !assemblyChangesPending; + if (modifiedAssemblies.Count > 0 && shouldNotify) + RunNotifier(true); + } + } - if (shouldNotify) + private void RunNotifier(bool shouldNotify) + { + Dispatcher.Invoke(() => + { + UpdateCommands(); + + if (shouldNotify) + { + var message = Tr._p("Message", "Some game code files have been modified. Do you want to reload the assemblies?"); + ServiceProvider.Get().AddDelayedNotification(EditorSettings.AskBeforeReloadingAssemblies, message, + Tr._p("Button", "Reload"), Tr._p("Button", "Don't reload"), + yesAction: async () => { - var message = Tr._p("Message", "Some game code files have been modified. Do you want to reload the assemblies?"); - ServiceProvider.Get().AddDelayedNotification(EditorSettings.AskBeforeReloadingAssemblies, message, - Tr._p("Button", "Reload"), Tr._p("Button", "Don't reload"), - yesAction: async () => - { - var undoRedoService = ServiceProvider.Get(); - // Wait for current transactions, undo/redo or save to complete before continuing. - await Task.WhenAll(undoRedoService.TransactionCompletion, undoRedoService.UndoRedoCompletion, Session.SaveCompletion); - // Reload assembly, if possible - if (ReloadAssembliesCommand.IsEnabled) - ReloadAssembliesCommand.Execute(); - }, - yesNoSettingsKey: EditorSettings.AutoReloadAssemblies); - } - }); + var undoRedoService = ServiceProvider.Get(); + // Wait for current transactions, undo/redo or save to complete before continuing. + await Task.WhenAll(undoRedoService.TransactionCompletion, undoRedoService.UndoRedoCompletion, Session.SaveCompletion); + // Reload assembly, if possible + if (ReloadAssembliesCommand.IsEnabled) + ReloadAssembliesCommand.Execute(); + }, + yesNoSettingsKey: EditorSettings.AutoReloadAssemblies); } - } + }); + } private void UpdateCommands()