From 3e86bd8b9abfcaec1709dddb5519ce0d7e183445 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Mon, 30 Sep 2024 23:04:40 +0200 Subject: [PATCH 01/12] Cache separators and loading --- src/RepoM.App/MainWindow.xaml.cs | 37 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index 8fa3e373..b59340a7 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -51,6 +51,15 @@ public partial class MainWindow private readonly IUserMenuActionMenuFactory _userMenuActionFactory; private readonly IAppDataPathProvider _appDataPathProvider; + private readonly List _separators = new(); + private int _separatorIndex = 0; + + private readonly AcrylicMenuItem _loadingMenuItem = new() + { + Header = "Loading ..", + IsEnabled = true, + }; + public MainWindow( IRepositoryInformationAggregator aggregator, IRepositoryMonitor repositoryMonitor, @@ -248,6 +257,7 @@ private async Task LstRepositoriesContextMenuOpeningWrapperAsync(ContextMe private async Task LstRepositoriesContextMenuOpeningAsync(ContextMenu ctxMenu) { + ResetIndex(); if (lstRepositories.SelectedItem is not RepositoryViewModel vm) { return false; @@ -267,11 +277,7 @@ private async Task LstRepositoriesContextMenuOpeningAsync(ContextMenu ctxM // } ctxMenu.Items.Clear(); - ctxMenu.Items.Add(new AcrylicMenuItem - { - Header = "Loading ..", - IsEnabled = true, - }); + ctxMenu.Items.Add(_loadingMenuItem); await foreach (UserInterfaceRepositoryActionBase action in _userMenuActionFactory.CreateMenuAsync(vm.Repository).ConfigureAwait(true)) { @@ -279,7 +285,7 @@ private async Task LstRepositoriesContextMenuOpeningAsync(ContextMenu ctxM { if (items.Count > 0 && items[^1] is not Separator) { - items.Add(new Separator()); + items.Add(GetSeparator()); } } else if (action is DeferredSubActionsUserInterfaceRepositoryAction or UserInterfaceRepositoryAction) @@ -496,7 +502,7 @@ private void ShowUpdateIfAvailable() { if (action is RepositorySeparatorAction) { - return new Separator(); + return GetSeparator(); } if (action is not RepositoryAction repositoryAction) @@ -577,7 +583,7 @@ void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) { if (action is UserInterfaceSeparatorRepositoryAction) { - return new Separator(); + return GetSeparator(); } // UserInterfaceRepositoryAction @@ -853,6 +859,21 @@ private void TxtFilter_Finish(object sender, EventArgs e) var item = (ListBoxItem)lstRepositories.ItemContainerGenerator.ContainerFromIndex(0); item?.Focus(); } + + private void ResetIndex() + { + _separatorIndex = 0; + } + + private Separator GetSeparator() + { + while (_separatorIndex >= _separators.Count) + { + _separators.Add(new Separator()); + } + + return _separators[_separatorIndex++]; + } public bool IsShown => Visibility == Visibility.Visible && IsActive; } \ No newline at end of file From 13875f620f24de6416b905a73d795afd50ebc0a4 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Mon, 30 Sep 2024 23:56:20 +0200 Subject: [PATCH 02/12] WIP --- src/RepoM.App/MainWindow.xaml.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index b59340a7..bbe3bb6c 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -52,7 +52,9 @@ public partial class MainWindow private readonly IAppDataPathProvider _appDataPathProvider; private readonly List _separators = new(); + private readonly List _menuItems = new(); private int _separatorIndex = 0; + private int _menuItemIndex = 0; private readonly AcrylicMenuItem _loadingMenuItem = new() { @@ -615,11 +617,10 @@ void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) } }; - var item = new AcrylicMenuItem - { - Header = repositoryAction.Name, - IsEnabled = repositoryAction.CanExecute, - }; + AcrylicMenuItem item = GetMenuItem(); + item.Header = repositoryAction.Name; + item.IsEnabled = repositoryAction.CanExecute; + item.Items.Clear(); item.Click += new RoutedEventHandler(clickAction); // this is a deferred submenu. We want to make sure that the context menu can pop up @@ -863,6 +864,7 @@ private void TxtFilter_Finish(object sender, EventArgs e) private void ResetIndex() { _separatorIndex = 0; + _menuItemIndex = 0; } private Separator GetSeparator() @@ -875,5 +877,15 @@ private Separator GetSeparator() return _separators[_separatorIndex++]; } + private AcrylicMenuItem GetMenuItem() + { + while (_menuItemIndex >= _menuItems.Count) + { + _menuItems.Add(new AcrylicMenuItem()); + } + + return _menuItems[_menuItemIndex++]; + } + public bool IsShown => Visibility == Visibility.Visible && IsActive; } \ No newline at end of file From e5ebb955a372429eab053db5f6494bce2a673337 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Tue, 1 Oct 2024 18:58:05 +0200 Subject: [PATCH 03/12] update --- src/RepoM.App/Controls/AcrylicMenuItem.cs | 21 ++++++++++++++++++++- src/RepoM.App/MainWindow.xaml.cs | 18 +++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/RepoM.App/Controls/AcrylicMenuItem.cs b/src/RepoM.App/Controls/AcrylicMenuItem.cs index ce024f8f..3ecc7649 100644 --- a/src/RepoM.App/Controls/AcrylicMenuItem.cs +++ b/src/RepoM.App/Controls/AcrylicMenuItem.cs @@ -8,6 +8,7 @@ namespace RepoM.App.Controls; public class AcrylicMenuItem : MenuItem { + private RoutedEventHandler? _evt; private static readonly Brush _solidColorBrush = new SolidColorBrush(Color.FromArgb(80, 0, 0, 0)); protected override void OnSubmenuOpened(RoutedEventArgs e) @@ -31,7 +32,7 @@ private void BlurSubMenu() return; } - DependencyObject parent = container; + DependencyObject? parent = container; var borderIndex = 0; while (parent != null) @@ -51,4 +52,22 @@ private void BlurSubMenu() AcrylicHelper.EnableBlur(container); } + + public void SetClick(RoutedEventHandler routedEventHandler) + { + ClearClick(); + Click += routedEventHandler; + _evt = routedEventHandler; + } + + public void ClearClick() + { + if (_evt == null) + { + return; + } + + Click -= _evt; + _evt = null; + } } \ No newline at end of file diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index bbe3bb6c..0ca95a7e 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -533,12 +533,10 @@ private void ShowUpdateIfAvailable() } }; - var item = new AcrylicMenuItem - { - Header = repositoryAction.Name, - IsEnabled = repositoryAction.CanExecute, - }; - item.Click += new RoutedEventHandler(clickAction); + AcrylicMenuItem item = GetMenuItem(); + item.Header = repositoryAction.Name; + item.IsEnabled = repositoryAction.CanExecute; + item.SetClick(new RoutedEventHandler(clickAction)); // this is a deferred submenu. We want to make sure that the context menu can pop up // fast, while submenus are not evaluated yet. We don't want to make the context menu @@ -620,8 +618,7 @@ void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) AcrylicMenuItem item = GetMenuItem(); item.Header = repositoryAction.Name; item.IsEnabled = repositoryAction.CanExecute; - item.Items.Clear(); - item.Click += new RoutedEventHandler(clickAction); + item.SetClick(new RoutedEventHandler(clickAction)); // this is a deferred submenu. We want to make sure that the context menu can pop up // fast, while submenus are not evaluated yet. We don't want to make the context menu @@ -884,7 +881,10 @@ private AcrylicMenuItem GetMenuItem() _menuItems.Add(new AcrylicMenuItem()); } - return _menuItems[_menuItemIndex++]; + AcrylicMenuItem result = _menuItems[_menuItemIndex++]; + result.Items.Clear(); + result.ClearClick(); + return result; } public bool IsShown => Visibility == Visibility.Visible && IsActive; From 70a88874207eed607829088f2c56e9501a3ff5f6 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Sat, 5 Oct 2024 17:16:39 +0200 Subject: [PATCH 04/12] update --- src/RepoM.App/Controls/AcrylicMenuItem.cs | 45 ++++- src/RepoM.App/MainWindow.xaml.cs | 234 ++++++++++++---------- 2 files changed, 165 insertions(+), 114 deletions(-) diff --git a/src/RepoM.App/Controls/AcrylicMenuItem.cs b/src/RepoM.App/Controls/AcrylicMenuItem.cs index 3ecc7649..e143b472 100644 --- a/src/RepoM.App/Controls/AcrylicMenuItem.cs +++ b/src/RepoM.App/Controls/AcrylicMenuItem.cs @@ -8,7 +8,8 @@ namespace RepoM.App.Controls; public class AcrylicMenuItem : MenuItem { - private RoutedEventHandler? _evt; + private RoutedEventHandler? _clickEvtHandler; + private RoutedEventHandler? _subMenuOpenedEventHandler; private static readonly Brush _solidColorBrush = new SolidColorBrush(Color.FromArgb(80, 0, 0, 0)); protected override void OnSubmenuOpened(RoutedEventArgs e) @@ -53,21 +54,55 @@ private void BlurSubMenu() AcrylicHelper.EnableBlur(container); } + public void SoftReset() + { + ClearClick(); + ClearSubMenuOpened(); + } + public void SetClick(RoutedEventHandler routedEventHandler) { ClearClick(); Click += routedEventHandler; - _evt = routedEventHandler; + _clickEvtHandler = routedEventHandler; } public void ClearClick() { - if (_evt == null) + if (_clickEvtHandler == null) + { + return; + } + + Click -= _clickEvtHandler; + _clickEvtHandler = null; + } + + public void SetSubMenuOpened(RoutedEventHandler routedEventHandler) + { + ClearSubMenuOpened(); + SubmenuOpened += routedEventHandler; + _subMenuOpenedEventHandler = routedEventHandler; + } + + public void ClearSubMenuOpened() + { + if (_subMenuOpenedEventHandler == null) + { + return; + } + + SubmenuOpened -= _subMenuOpenedEventHandler; + _subMenuOpenedEventHandler = null; + } + + public void ClearItems() + { + if (Items.Count == 0) { return; } - Click -= _evt; - _evt = null; + Items.Clear(); } } \ No newline at end of file diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index 0ca95a7e..9049fcbc 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -1,7 +1,6 @@ namespace RepoM.App; using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO.Abstractions; @@ -50,18 +49,7 @@ public partial class MainWindow private readonly ILogger _logger; private readonly IUserMenuActionMenuFactory _userMenuActionFactory; private readonly IAppDataPathProvider _appDataPathProvider; - - private readonly List _separators = new(); - private readonly List _menuItems = new(); - private int _separatorIndex = 0; - private int _menuItemIndex = 0; - - private readonly AcrylicMenuItem _loadingMenuItem = new() - { - Header = "Loading ..", - IsEnabled = true, - }; - + public MainWindow( IRepositoryInformationAggregator aggregator, IRepositoryMonitor repositoryMonitor, @@ -259,57 +247,128 @@ private async Task LstRepositoriesContextMenuOpeningWrapperAsync(ContextMe private async Task LstRepositoriesContextMenuOpeningAsync(ContextMenu ctxMenu) { - ResetIndex(); if (lstRepositories.SelectedItem is not RepositoryViewModel vm) { return false; } - var items = new List(); - // ItemCollection items = ctxMenu.Items; - // ItemCollection items = new ItemCollection(); - //items.Clear(); - - // foreach (var item in ctxMenu.Items) - // { - // if (item is Control c) - // { - // c.IsEnabled = false; - // } - // } - - ctxMenu.Items.Clear(); - ctxMenu.Items.Add(_loadingMenuItem); + if (ctxMenu.Items.Count == 0) + { + for (int i = 0; i < 50; i++) + { + ctxMenu.Items.Add(new AcrylicMenuItem + { + Header = string.Empty, + Visibility = Visibility.Collapsed, + }); + ctxMenu.Items.Add(new Separator + { + Visibility = Visibility.Collapsed, + }); + } + } + + int j = -1; + bool lastVisibleSeparator = false; await foreach (UserInterfaceRepositoryActionBase action in _userMenuActionFactory.CreateMenuAsync(vm.Repository).ConfigureAwait(true)) { + j++; if (action is UserInterfaceSeparatorRepositoryAction) { - if (items.Count > 0 && items[^1] is not Separator) + while (ctxMenu.Items[j] is AcrylicMenuItem ami) { - items.Add(GetSeparator()); + ami.Visibility = Visibility.Collapsed; + ami.SoftReset(); + j++; } + + if (ctxMenu.Items[j] is Separator s) + { + s.Visibility = lastVisibleSeparator + ? Visibility.Collapsed + : Visibility.Visible; + lastVisibleSeparator = true; + continue; + } + + /* should never happen */ } - else if (action is DeferredSubActionsUserInterfaceRepositoryAction or UserInterfaceRepositoryAction) + + + if (action is DeferredSubActionsUserInterfaceRepositoryAction or UserInterfaceRepositoryAction) { - Control? controlItem = CreateMenuItemNewStyleAsync(action, vm); - if (controlItem != null) + while (ctxMenu.Items[j] is /*not AcrylicMenuItem*/ Separator s) { - items.Add(controlItem); + s.Visibility = Visibility.Collapsed; + j++; } + + var acrylicMenuItem = (AcrylicMenuItem)ctxMenu.Items[j]; + lastVisibleSeparator = false; + + if (action is UserInterfaceRepositoryAction repositoryAction) + { + acrylicMenuItem.Header = repositoryAction.Name; + acrylicMenuItem.IsEnabled = repositoryAction.CanExecute; + acrylicMenuItem.ClearItems(); + SetClick(acrylicMenuItem, repositoryAction, vm); + SetSubMenu(acrylicMenuItem, repositoryAction); + acrylicMenuItem.Visibility = Visibility.Visible; + continue; + } + + /* should never happen */ } } - ctxMenu.Items.Clear(); - foreach (Control item in items) + if (!lastVisibleSeparator) { - ctxMenu.Items.Add(item); + j++; + } + + var len = ctxMenu.Items.Count; + while (j < len) + { + if (ctxMenu.Items[j] is AcrylicMenuItem ami) + { + ami.SoftReset(); + } + + ((Control)ctxMenu.Items[j]).Visibility = Visibility.Collapsed; + j++; } return true; } - + private void SetClick(AcrylicMenuItem acrylicMenuItem, UserInterfaceRepositoryAction action, RepositoryViewModel? affectedViews) + { + void ClickAction(object clickSender, object clickArgs) + { + // run actions in the UI async to not block it + if (action.ExecutionCausesSynchronizing) + { + Task.Run(() => SetVmSynchronizing(affectedViews, true)) + .ContinueWith(t => _executor.Execute(action.Repository, action.RepositoryCommand)) + .ContinueWith(t => SetVmSynchronizing(affectedViews, false)); + } + else + { + Task.Run(() => _executor.Execute(action.Repository, action.RepositoryCommand)); + } + } + + if (action.RepositoryCommand is null or NullRepositoryCommand) + { + acrylicMenuItem.ClearClick(); + } + else + { + acrylicMenuItem.SetClick(new RoutedEventHandler((Action)ClickAction)); + } + } + private async void LstRepositories_KeyDown(object? sender, KeyEventArgs e) { if (e.Key is Key.Return or Key.Enter) @@ -504,7 +563,7 @@ private void ShowUpdateIfAvailable() { if (action is RepositorySeparatorAction) { - return GetSeparator(); + return new Separator(); } if (action is not RepositoryAction repositoryAction) @@ -533,9 +592,11 @@ private void ShowUpdateIfAvailable() } }; - AcrylicMenuItem item = GetMenuItem(); - item.Header = repositoryAction.Name; - item.IsEnabled = repositoryAction.CanExecute; + var item = new AcrylicMenuItem + { + Header = repositoryAction.Name, + IsEnabled = repositoryAction.CanExecute, + }; item.SetClick(new RoutedEventHandler(clickAction)); // this is a deferred submenu. We want to make sure that the context menu can pop up @@ -579,47 +640,33 @@ void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) return item; } - private Control? /*MenuItem*/ CreateMenuItemNewStyleAsync(UserInterfaceRepositoryActionBase action, RepositoryViewModel? affectedViews = null) + private Control? CreateMenuItemNewStyleAsync(UserInterfaceRepositoryActionBase action, RepositoryViewModel? affectedViews = null) { if (action is UserInterfaceSeparatorRepositoryAction) { - return GetSeparator(); + return new Separator(); } - // UserInterfaceRepositoryAction - // DeferredSubActionsUserInterfaceRepositoryAction - if (action is not UserInterfaceRepositoryAction repositoryAction) { // throw?? return null; } - Action clickAction = (object clickSender, object clickArgs) => + var item = new AcrylicMenuItem { - if (repositoryAction.RepositoryCommand is null or NullRepositoryCommand) - { - return; - } - - // run actions in the UI async to not block it - if (repositoryAction.ExecutionCausesSynchronizing) - { - Task.Run(() => SetVmSynchronizing(affectedViews, true)) - .ContinueWith(t => _executor.Execute(action.Repository, action.RepositoryCommand)) - .ContinueWith(t => SetVmSynchronizing(affectedViews, false)); - } - else - { - Task.Run(() => _executor.Execute(action.Repository, action.RepositoryCommand)); - } + Header = repositoryAction.Name, + IsEnabled = repositoryAction.CanExecute, }; + SetClick(item, repositoryAction, affectedViews); + SetSubMenu(item, repositoryAction); + return item; + } - AcrylicMenuItem item = GetMenuItem(); - item.Header = repositoryAction.Name; - item.IsEnabled = repositoryAction.CanExecute; - item.SetClick(new RoutedEventHandler(clickAction)); - + private void SetSubMenu(AcrylicMenuItem item, UserInterfaceRepositoryAction repositoryAction) + { + item.ClearItems(); + // this is a deferred submenu. We want to make sure that the context menu can pop up // fast, while submenus are not evaluated yet. We don't want to make the context menu // itself slow because the creation of the submenu items takes some time. @@ -631,9 +678,9 @@ void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) async void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) { - item.SubmenuOpened -= SelfDetachingEventHandler; - item.Items.Clear(); - + item.ClearSubMenuOpened(); + item.ClearItems(); + foreach (UserInterfaceRepositoryActionBase subAction in await deferredRepositoryAction.GetAsync().ConfigureAwait(true)) { Control? controlItem = CreateMenuItemNewStyleAsync(subAction); @@ -661,18 +708,18 @@ async void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) } } - item.SubmenuOpened += SelfDetachingEventHandler; + item.SetSubMenuOpened(SelfDetachingEventHandler); } else if (repositoryAction.SubActions != null) { // this is a template submenu item to enable submenus under the current // menu item. this item gets removed when the real subitems are created item.Items.Add("Loading.."); - + async void SelfDetachingEventHandler1(object _, RoutedEventArgs evtArgs) { - item.SubmenuOpened -= SelfDetachingEventHandler1; - item.Items.Clear(); + item.ClearSubMenuOpened(); + item.ClearItems(); foreach (UserInterfaceRepositoryActionBase subAction in repositoryAction.SubActions) { @@ -701,10 +748,8 @@ async void SelfDetachingEventHandler1(object _, RoutedEventArgs evtArgs) } } - item.SubmenuOpened += SelfDetachingEventHandler1; + item.SetSubMenuOpened(SelfDetachingEventHandler1); } - - return item; } private static void SetVmSynchronizing(RepositoryViewModel? affectedVm, bool synchronizing) @@ -858,34 +903,5 @@ private void TxtFilter_Finish(object sender, EventArgs e) item?.Focus(); } - private void ResetIndex() - { - _separatorIndex = 0; - _menuItemIndex = 0; - } - - private Separator GetSeparator() - { - while (_separatorIndex >= _separators.Count) - { - _separators.Add(new Separator()); - } - - return _separators[_separatorIndex++]; - } - - private AcrylicMenuItem GetMenuItem() - { - while (_menuItemIndex >= _menuItems.Count) - { - _menuItems.Add(new AcrylicMenuItem()); - } - - AcrylicMenuItem result = _menuItems[_menuItemIndex++]; - result.Items.Clear(); - result.ClearClick(); - return result; - } - public bool IsShown => Visibility == Visibility.Visible && IsActive; } \ No newline at end of file From e99887bf30772145272c76879c35b5ae9c4b3c79 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Thu, 10 Oct 2024 23:26:08 +0200 Subject: [PATCH 05/12] Speedup context menu by cloning TemplateContext --- .../ActionMenu/Context/EnvScriptObject.cs | 11 +- .../ActionMenu/Context/EnvSetScriptObject.cs | 11 +- ...epositoryActionBrowseRepositoryV1Mapper.cs | 1 - .../Folder/RepositoryActionFolderV1Mapper.cs | 9 +- .../RepositoryActionGitCheckoutV1Mapper.cs | 2 - .../Model/ActionMenuGenerationContext.cs | 91 ++++-- .../Model/DisposableContextScriptObject.cs | 137 +++++++-- .../Model/RepoMScriptObject.cs | 42 +-- .../UserInterfaceActionMenuFactory.cs | 5 - .../ContextActionProcessorBase.cs | 2 +- .../ActionContext/IContextActionProcessor.cs | 2 +- ...SubActionsUserInterfaceRepositoryAction.cs | 6 +- .../ActionToRepositoryActionMapperBase.cs | 4 +- .../IActionToRepositoryActionMapper.cs | 4 +- src/RepoM.App/Controls/AcrylicMenuItem.cs | 139 ++++++--- src/RepoM.App/MainWindow.xaml.cs | 284 +++++++++--------- .../Services/UserMenuActionMenuFactory.cs | 8 +- .../DisposableContextScriptObjectTests.cs | 56 ++-- 18 files changed, 472 insertions(+), 342 deletions(-) diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs index c0433bb2..48a9df2b 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs @@ -11,7 +11,7 @@ internal sealed class EnvScriptObject : IScriptObject { private readonly IDictionary _env; - public static EnvScriptObject Create() + private static EnvScriptObject Create() { var env = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -33,6 +33,8 @@ public static EnvScriptObject Create() return new EnvScriptObject(env); } + public static EnvScriptObject Instance { get; } = Create(); + public EnvScriptObject(IDictionary envVars) { _env = envVars; @@ -80,11 +82,16 @@ public void SetReadOnly(string member, bool readOnly) // intentionally do nothing } - public IScriptObject Clone(bool deep) + public EnvScriptObject Clone() { return new EnvScriptObject(new Dictionary(_env)); } + IScriptObject IScriptObject.Clone(bool deep) + { + return Clone(); + } + public int Count => _env.Count; public bool IsReadOnly diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs index 9cd705d3..25f6c615 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs @@ -74,10 +74,10 @@ public void SetReadOnly(string member, bool readOnly) // intentionally do nothing } - public IScriptObject Clone(bool deep) + public EnvSetScriptObject Clone() { EnvScriptObject[] items = _stack.Items; - var result = new EnvSetScriptObject((EnvScriptObject)items[0].Clone(true)); + var result = new EnvSetScriptObject(items[0].Clone()); if (items.Length <= 1) { @@ -88,7 +88,7 @@ public IScriptObject Clone(bool deep) { if (items[i] != null) { - result.Push((EnvScriptObject)items[i].Clone(true)); + result.Push(items[i].Clone()); } } @@ -122,4 +122,9 @@ public void Dispose() return null; } + + IScriptObject IScriptObject.Clone(bool deep) + { + return Clone(); + } } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1Mapper.cs index 3f4e8db1..0d7c6fa8 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1Mapper.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1Mapper.cs @@ -42,7 +42,6 @@ protected override async IAsyncEnumerable Map name, repository, context, - captureScope: false, async ctx => await EnumerateRemotes(ctx.Repository).ConfigureAwait(false)) { CanExecute = true, diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs index 51195539..707180e6 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs @@ -11,7 +11,6 @@ namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Folder; [UsedImplicitly] internal class RepositoryActionFolderV1Mapper : ActionToRepositoryActionMapperBase { - protected override async IAsyncEnumerable MapAsync(RepositoryActionFolderV1 action, IActionMenuGenerationContext context, IRepository repository) { var name = await context.RenderStringAsync(action.Name).ConfigureAwait(false); @@ -22,12 +21,7 @@ protected override async IAsyncEnumerable Map yield break; } -#pragma warning disable S2583 // Change this condition so that it does not always evaluate to 'False'. Some code paths are unreachable - // Deferred with cloning the context doesn't work yet (https://github.com/coenm/RepoM/issues/85) therefore, set to false. -#pragma warning disable S125 - // var isDeferred = await action.IsDeferred.EvaluateAsync(context).ConfigureAwait(false); -#pragma warning restore S125 - var isDeferred = false; + var isDeferred = await action.IsDeferred.EvaluateAsync(context).ConfigureAwait(false); if (isDeferred) { @@ -35,7 +29,6 @@ protected override async IAsyncEnumerable Map name, repository, context, - action.Actions != null, async ctx => await ctx.AddActionMenusAsyncArray(action.Actions).ConfigureAwait(false)) { CanExecute = true, diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1Mapper.cs index f322372a..6b716e53 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1Mapper.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1Mapper.cs @@ -26,7 +26,6 @@ protected override async IAsyncEnumerable Map name, repository, context, - false, _ => Task.FromResult(repository.LocalBranches .Take(50) @@ -42,7 +41,6 @@ protected override async IAsyncEnumerable Map remoteBranchesTranslated, repository, context, - false, _ => { UserInterfaceRepositoryActionBase[] remoteBranches = repository diff --git a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs index c42ee95c..505eeb60 100644 --- a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs +++ b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs @@ -24,10 +24,13 @@ internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerat { private readonly ITemplateParser _templateParser; private readonly ITemplateContextRegistration[] _functionsArray; - private readonly IActionMenuDeserializer _deserializer; - private readonly IActionToRepositoryActionMapper[] _repositoryActionMappers; + private readonly IActionMenuDeserializer _deserializer; + private readonly IActionToRepositoryActionMapper[] _repositoryActionMappers; private readonly IContextActionProcessor[] _contextActionMappers; - private RepoMScriptObject _rootScriptObject = null!; // used for cloning. + private RepoMScriptObject _rootScriptObject = null!; + private EnvSetScriptObject? _env; + private FastStack _globals = new(4); + private DisposableContextScriptObject _repositoryActionsScriptContext = null!; public ActionMenuGenerationContext( ITemplateParser templateParser, @@ -44,15 +47,11 @@ public ActionMenuGenerationContext( _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); _contextActionMappers = contextActionMappers ?? throw new ArgumentNullException(nameof(contextActionMappers)); } - - public IFileSystem FileSystem { get; } - public DisposableContextScriptObject RepositoryActionsScriptContext { get; private set; } = null!; - - public IRepository Repository { get; private set; } = null!; - - private EnvSetScriptObject? _env; + public IRepository Repository { get; private set; } = null!; + public IFileSystem FileSystem { get; } + public EnvSetScriptObject Env => _env ??= (EnvSetScriptObject)_rootScriptObject["env"]; public async Task AddRepositoryContextAsync(Context? reposContext) @@ -64,7 +63,7 @@ public async Task AddRepositoryContextAsync(Context? reposContext) foreach (IContextAction contextAction in reposContext) { - await RepositoryActionsScriptContext.AddContextActionAsync(contextAction).ConfigureAwait(false); + await _repositoryActionsScriptContext.AddContextActionAsync(contextAction).ConfigureAwait(false); } } @@ -86,19 +85,20 @@ public async IAsyncEnumerable AddActionMenusA } } - public IActionMenuGenerationContext Clone() + public void PushGlobal(DisposableContextScriptObject scriptObject) { -#pragma warning disable S125 - // this method doesn't work yet. Cloning the full Template context is not possible. - // to be implemented (https://github.com/coenm/RepoM/issues/85) - /* - var repoMScriptObject = (RepoMScriptObject)_rootScriptObject.Clone(true); + base.PushGlobal(scriptObject); + _globals.Push(scriptObject); + } - IScriptObject e = ((EnvSetScriptObject)_rootScriptObject["env"]).Clone(true); - repoMScriptObject.SetValue("env", e, false); - */ -#pragma warning restore S125 + public new void PopGlobal() + { + _globals.Pop(); + base.PopGlobal(); + } + public IActionMenuGenerationContext Clone() + { var result = new ActionMenuGenerationContext( _templateParser, FileSystem, @@ -107,8 +107,7 @@ public IActionMenuGenerationContext Clone() _deserializer, _contextActionMappers); - result.Initialize(Repository); - + result.InitializeFrom(this); return result; } @@ -116,28 +115,56 @@ internal void Initialize(IRepository repository) { Repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _rootScriptObject = CreateAndInitRepoMScriptObject(Repository); - + _rootScriptObject = CreateAndInitRepoMScriptObject( + new EnvSetScriptObject(EnvScriptObject.Instance)); + foreach (ITemplateContextRegistration contextRegistration in _functionsArray) { contextRegistration.RegisterFunctions(Decorate(_rootScriptObject)); } PushGlobal(_rootScriptObject); - RepositoryActionsScriptContext = new DisposableContextScriptObject(this, Env, _contextActionMappers); - PushGlobal(RepositoryActionsScriptContext); + _repositoryActionsScriptContext = new DisposableContextScriptObject(this, _contextActionMappers); + PushGlobal(_repositoryActionsScriptContext); + } + + private void InitializeFrom(ActionMenuGenerationContext @this) + { + Repository = @this.Repository; + + _rootScriptObject = CreateAndInitRepoMScriptObject(@this.Env.Clone()); + foreach (ITemplateContextRegistration contextRegistration in _functionsArray) + { + contextRegistration.RegisterFunctions(Decorate(_rootScriptObject)); + } + + PushGlobal(_rootScriptObject); + + // -2 because _rootScriptObject and RepositoryActionsScriptContext are already added + if (@this.GlobalCount -2 != @this._globals.Count) + { + throw new Exception(); + } + + _repositoryActionsScriptContext = new DisposableContextScriptObject(this, _contextActionMappers); + PushGlobal(_repositoryActionsScriptContext); + + for (var index = 0; index < @this._globals.Count; index++) + { + @this._globals.Items[index].CloneUsingNewContext(this); + } } - private static RepoMScriptObject CreateAndInitRepoMScriptObject(IRepository repository) + private RepoMScriptObject CreateAndInitRepoMScriptObject(EnvSetScriptObject env) { var scriptObj = new RepoMScriptObject(); scriptObj.Import(typeof(InitialFunctions)); scriptObj.SetValue("file", new FileFunctions(), true); - scriptObj.SetValue("repository", new RepositoryFunctions(repository), true); + scriptObj.SetValue("repository", new RepositoryFunctions(Repository), true); - scriptObj.Add("env", new EnvSetScriptObject(EnvScriptObject.Create())); + scriptObj.Add("env", env); scriptObj.SetReadOnly("env", false); // this is not what we want, but it's the only way to make it work return scriptObj; @@ -168,7 +195,7 @@ private async Task> AddMenuAction } var items = new List(); - await foreach (UserInterfaceRepositoryActionBase item in mapper.MapAsync(menuAction, this, Repository).ConfigureAwait(false)) + await foreach (UserInterfaceRepositoryActionBase item in mapper.MapAsync(in menuAction, this, Repository).ConfigureAwait(false)) { items.Add(item); } @@ -184,7 +211,7 @@ public async Task RenderStringAsync(string text) private DisposableContextScriptObject PushNewContext() { - return new DisposableContextScriptObject(this, Env, _contextActionMappers); + return new DisposableContextScriptObject(this, _contextActionMappers); } public async Task EvaluateAsync(string? text) diff --git a/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs b/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs index 45eb8cf7..ee4d79eb 100644 --- a/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs +++ b/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs @@ -7,25 +7,64 @@ namespace RepoM.ActionMenu.Core.Model; using RepoM.ActionMenu.Core.Yaml.Model.ActionContext; using RepoM.ActionMenu.Interface.ActionMenuFactory; using RepoM.ActionMenu.Interface.YamlModel; +using Scriban; +using Scriban.Parsing; using Scriban.Runtime; -internal sealed class DisposableContextScriptObject : ScriptObject, IScope +internal sealed class DisposableContextScriptObject : IScriptObject, IScope { private readonly ActionMenuGenerationContext _context; - private readonly EnvSetScriptObject _envSetScriptObject; private readonly IContextActionProcessor[] _mappers; private int _envCounter; + private readonly IScriptObject _inner; - internal DisposableContextScriptObject(ActionMenuGenerationContext context, EnvSetScriptObject envSetScriptObject, IContextActionProcessor[] mappers) + internal DisposableContextScriptObject(ActionMenuGenerationContext context, IContextActionProcessor[] mappers) : this(context, mappers, new ScriptObject(), 0) + { + } + + private DisposableContextScriptObject(ActionMenuGenerationContext context, IContextActionProcessor[] mappers, IScriptObject so, int envCounter) { - _envCounter = 0; _context = context ?? throw new ArgumentNullException(nameof(context)); - _envSetScriptObject = envSetScriptObject ?? throw new ArgumentNullException(nameof(envSetScriptObject)); _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _context.PushGlobal(this); + _envCounter = envCounter; + _inner = so; + } + + public void Dispose() + { + if (_envCounter != 0) + { + while (_envCounter > 0) + { + _ = _context.Env.Pop(); + _envCounter--; + } + } + + _context.PopGlobal(); + } + + public Task AddContextActionAsync(IContextAction contextItem) + { + return AddContextActionInnerAsync(contextItem); } - public async Task AddContextActionAsync(IContextAction contextItem) + /// + /// Clone DisposableContextScriptObject using a new context. + /// + /// The context. + /// DisposableContextScriptObject + public DisposableContextScriptObject CloneUsingNewContext(ActionMenuGenerationContext context) + { + return new DisposableContextScriptObject( + context, + _mappers, + _inner.Clone(true), + _envCounter); + } + + private async Task AddContextActionInnerAsync(IContextAction contextItem) { var enabled = await IsActionEnabled(contextItem).ConfigureAwait(false); if (!enabled) @@ -33,7 +72,7 @@ public async Task AddContextActionAsync(IContextAction contextItem) return; } - IContextActionProcessor? mapper = Array.Find(_mappers, mapper => mapper.CanProcess(contextItem)); + IContextActionProcessor? mapper = Array.Find(_mappers, mapper => mapper.CanProcess(in contextItem)); if (mapper == null) { throw new Exception("Cannot find mapper"); @@ -41,35 +80,83 @@ public async Task AddContextActionAsync(IContextAction contextItem) await mapper.ProcessAsync(contextItem, _context, this).ConfigureAwait(false); } - - public void Dispose() + + private async Task IsActionEnabled(IContextAction contextItem) { - if (_envCounter != 0) + // action does not implement interface and is therefore always enabled. + if (contextItem is not IEnabled ea) { - while (_envCounter > 0) - { - _ = _envSetScriptObject.Pop(); - _envCounter--; - } + return true; } - _context.PopGlobal(); + return await ea.Enabled.EvaluateAsync(_context).ConfigureAwait(false); } - public void PushEnvironmentVariable(IDictionary envVars) + #region IScope + void IScope.SetValue(string member, object? value, bool @readonly) { - _envSetScriptObject.Push(new EnvScriptObject(envVars)); + _inner.SetValue(member, value, @readonly); + } + + void IScope.PushEnvironmentVariable(IDictionary envVars) + { + _context.Env.Push(new EnvScriptObject(envVars)); _envCounter++; } - private async Task IsActionEnabled(IContextAction contextItem) + Task IScope.AddContextActionAsync(IContextAction contextItem) { - // action does not implement interface and is therfore always enabled. - if (contextItem is not IEnabled ea) - { - return true; - } + return AddContextActionInnerAsync(contextItem); + } + #endregion - return await ea.Enabled.EvaluateAsync(_context).ConfigureAwait(false); + #region IScriptObject + int IScriptObject.Count => _inner.Count; + + bool IScriptObject.IsReadOnly + { + get => _inner.IsReadOnly; + set => _inner.IsReadOnly = value; + } + + IScriptObject IScriptObject.Clone(bool deep) + { + return _inner.Clone(deep); + } + + IEnumerable IScriptObject.GetMembers() + { + return _inner.GetMembers(); + } + + bool IScriptObject.Contains(string member) + { + return _inner.Contains(member); + } + + bool IScriptObject.TryGetValue(TemplateContext context, SourceSpan span, string member, out object value) + { + return _inner.TryGetValue(context, span, member, out value); + } + + bool IScriptObject.CanWrite(string member) + { + return _inner.CanWrite(member); + } + + bool IScriptObject.TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly) + { + return _inner.TrySetValue(context, span, member, value, readOnly); + } + + bool IScriptObject.Remove(string member) + { + return _inner.Remove(member); + } + + void IScriptObject.SetReadOnly(string member, bool readOnly) + { + _inner.SetReadOnly(member, readOnly); } + #endregion } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs b/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs index 37ce93f7..8ad15adf 100644 --- a/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs +++ b/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs @@ -10,20 +10,26 @@ namespace RepoM.ActionMenu.Core.Model; internal class RepoMScriptObject : ScriptObject, IContextRegistration { - public override IScriptObject Clone(bool deep) + private void RegisterCustomFunction(string name, IScriptCustomFunction func) + { + RegisterVariableInner(name, func); + } + + private void RegisterVariableInner(string name, object value) { - // Not sure if this clone is okay. - // needs testing - IScriptObject result = base.Clone(deep); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(value); - if (result is RepoMScriptObject repoMScriptObject) + var names = name.Split(','); + + foreach (var subName in names) { - return repoMScriptObject; + SetValue(subName, value, true); } - - throw new NotImplementedException("Could not clone"); } + #region IContextRegistration + IContextRegistration IContextRegistration.CreateOrGetSubRegistration(string key) { if (this.TryGetValue(key, out var value)) @@ -55,7 +61,7 @@ bool IContextRegistration.ContainsKey(string key) { return ContainsKey(key); } - + void IContextRegistration.RegisterConstant(string name, object value) { RegisterVariableInner(name, value); @@ -126,23 +132,7 @@ void IContextRegistration.RegisterVariable(string name, object value) RegisterVariableInner(name, value); } - private void RegisterCustomFunction(string name, IScriptCustomFunction func) - { - RegisterVariableInner(name, func); - } - - private void RegisterVariableInner(string name, object value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(name); - ArgumentNullException.ThrowIfNull(value); - - var names = name.Split(','); - - foreach (var subName in names) - { - SetValue(subName, value, true); - } - } + #endregion } #pragma warning restore S2436 \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs index fc8776ce..873ca69a 100644 --- a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs +++ b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs @@ -67,18 +67,13 @@ public async IAsyncEnumerable CreateMenuAsync ActionMenuRoot actions = await LoadAsync(filename).ConfigureAwait(false); // process context (vars + methods) - _logger.LogTrace("CreateActionMenuGenerationContext AddRepositoryContextAsync"); await context.AddRepositoryContextAsync(actions.Context).ConfigureAwait(false); // process actions - _logger.LogTrace("CreateActionMenuGenerationContext foreach AddActionMenusAsync"); await foreach (UserInterfaceRepositoryActionBase item in context.AddActionMenusAsync(actions.ActionMenu).ConfigureAwait(false)) { - _logger.LogTrace("CreateActionMenuGenerationContext foreach inner"); yield return item; } - - _logger.LogTrace("CreateMenuAsync Done"); } public async Task> GetTagsAsync(IRepository repository, string filename) diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/ContextActionProcessorBase.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/ContextActionProcessorBase.cs index 69bf163e..c8cc3d43 100644 --- a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/ContextActionProcessorBase.cs +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/ContextActionProcessorBase.cs @@ -6,7 +6,7 @@ namespace RepoM.ActionMenu.Core.Yaml.Model.ActionContext; public abstract class ContextActionProcessorBase : IContextActionProcessor where T : IContextAction { - public bool CanProcess(IContextAction action) + public bool CanProcess(in IContextAction action) { return action is T; } diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/IContextActionProcessor.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/IContextActionProcessor.cs index 755e709a..00a7cf38 100644 --- a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/IContextActionProcessor.cs +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionContext/IContextActionProcessor.cs @@ -6,7 +6,7 @@ namespace RepoM.ActionMenu.Core.Yaml.Model.ActionContext; internal interface IContextActionProcessor { - bool CanProcess(IContextAction action); + bool CanProcess(in IContextAction action); Task ProcessAsync(IContextAction action, IContextMenuActionMenuGenerationContext context, IScope scope); } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs b/src/RepoM.ActionMenu.Interface/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs index 92036daf..3cfa75a9 100644 --- a/src/RepoM.ActionMenu.Interface/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs +++ b/src/RepoM.ActionMenu.Interface/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs @@ -14,14 +14,10 @@ public DeferredSubActionsUserInterfaceRepositoryAction( string name, IRepository repository, IActionMenuGenerationContext actionMenuGenerationContext, - bool captureScope, Func> resolveFunction) : base(name, repository) { - _context = captureScope - ? actionMenuGenerationContext.Clone() - : actionMenuGenerationContext; - + _context = actionMenuGenerationContext.Clone(); _getFunction = resolveFunction; } diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs b/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs index 48833509..d910c311 100644 --- a/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs +++ b/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs @@ -7,12 +7,12 @@ namespace RepoM.ActionMenu.Interface.YamlModel; public abstract class ActionToRepositoryActionMapperBase : IActionToRepositoryActionMapper where T : IMenuAction { - public bool CanMap(IMenuAction action) + public bool CanMap(in IMenuAction action) { return action is T; } - public IAsyncEnumerable MapAsync(IMenuAction action, IActionMenuGenerationContext context, IRepository repository) + public IAsyncEnumerable MapAsync(in IMenuAction action, in IActionMenuGenerationContext context, in IRepository repository) { return MapAsync((T)action, context, repository); } diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs b/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs index b0bcb602..6ba5dacc 100644 --- a/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs +++ b/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs @@ -7,7 +7,7 @@ namespace RepoM.ActionMenu.Interface.YamlModel; public interface IActionToRepositoryActionMapper { - bool CanMap(IMenuAction action); + bool CanMap(in IMenuAction action); - IAsyncEnumerable MapAsync(IMenuAction action, IActionMenuGenerationContext context, IRepository repository); + IAsyncEnumerable MapAsync(in IMenuAction action, in IActionMenuGenerationContext context, in IRepository repository); } \ No newline at end of file diff --git a/src/RepoM.App/Controls/AcrylicMenuItem.cs b/src/RepoM.App/Controls/AcrylicMenuItem.cs index e143b472..7505bb61 100644 --- a/src/RepoM.App/Controls/AcrylicMenuItem.cs +++ b/src/RepoM.App/Controls/AcrylicMenuItem.cs @@ -1,64 +1,22 @@ namespace RepoM.App.Controls; using System; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; +using RepoM.ActionMenu.Interface.UserInterface; using RepoM.App.Services; public class AcrylicMenuItem : MenuItem { private RoutedEventHandler? _clickEvtHandler; private RoutedEventHandler? _subMenuOpenedEventHandler; + private string _header = string.Empty; private static readonly Brush _solidColorBrush = new SolidColorBrush(Color.FromArgb(80, 0, 0, 0)); + private bool _isEnabled = default; - protected override void OnSubmenuOpened(RoutedEventArgs e) - { - base.OnSubmenuOpened(e); - - Dispatcher.BeginInvoke((Action)BlurSubMenu); - } - - private void BlurSubMenu() - { - DependencyObject firstSubItem = ItemContainerGenerator.ContainerFromIndex(0); - - if (firstSubItem == null) - { - return; - } - - if (VisualTreeHelper.GetParent(firstSubItem) is not Visual container) - { - return; - } - - DependencyObject? parent = container; - var borderIndex = 0; - - while (parent != null) - { - if (parent is Border b) - { - // only put color on the first border (transparent colors will add up otherwise) - b.Background = borderIndex == 0 - ? _solidColorBrush - : Brushes.Transparent; - - borderIndex++; - } - - parent = VisualTreeHelper.GetParent(parent); - } - - AcrylicHelper.EnableBlur(container); - } - - public void SoftReset() - { - ClearClick(); - ClearSubMenuOpened(); - } + public Task DataTask { get; private set; } = Task.FromResult(Array.Empty()); public void SetClick(RoutedEventHandler routedEventHandler) { @@ -84,7 +42,7 @@ public void SetSubMenuOpened(RoutedEventHandler routedEventHandler) SubmenuOpened += routedEventHandler; _subMenuOpenedEventHandler = routedEventHandler; } - + public void ClearSubMenuOpened() { if (_subMenuOpenedEventHandler == null) @@ -105,4 +63,89 @@ public void ClearItems() Items.Clear(); } + + public void SetHeader(string name) + { + if (name.Equals(_header)) + { + return; + } + + Header = name; + _header = name; + } + + public void SetEnabled(bool repositoryActionCanExecute) + { + if (_isEnabled == repositoryActionCanExecute) + { + return; + } + + IsEnabled = _isEnabled = repositoryActionCanExecute; + } + + public void LoadData(DeferredSubActionsUserInterfaceRepositoryAction deferredRepositoryAction) + { + ClearData(); + DataTask = Task.Run(async () => await deferredRepositoryAction.GetAsync()); + } + + public void ClearData() + { + try + { + DataTask.Dispose(); + } + catch (Exception) + { + // swallow + } + + DataTask = Task.FromResult(Array.Empty()); + } + + protected override void OnSubmenuOpened(RoutedEventArgs e) + { + base.OnSubmenuOpened(e); + + Dispatcher.BeginInvoke((Action)BlurSubMenu); + } + + private void BlurSubMenu() + { + DependencyObject firstSubItem = ItemContainerGenerator.ContainerFromIndex(0); + + if (firstSubItem == null) + { + return; + } + + if (VisualTreeHelper.GetParent(firstSubItem) is not Visual container) + { + return; + } + + DependencyObject? parent = container; + var borderIndex = 0; + + while (parent != null) + { + if (parent is Border b) + { + // only put color on the first border (transparent colors will add up otherwise) + b.Background = borderIndex == 0 + ? _solidColorBrush + : Brushes.Transparent; + + borderIndex++; + } + + parent = VisualTreeHelper.GetParent(parent); + } + + AcrylicHelper.EnableBlur(container); + } + + } \ No newline at end of file diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index 9049fcbc..b7614d8f 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -16,7 +16,6 @@ namespace RepoM.App; using RepoM.ActionMenu.Interface.UserInterface; using RepoM.Api.Common; using RepoM.Api.Git; -using RepoM.Api.RepositoryActions; using RepoM.App.Controls; using RepoM.App.Plugins; using RepoM.App.RepositoryActions; @@ -49,7 +48,10 @@ public partial class MainWindow private readonly ILogger _logger; private readonly IUserMenuActionMenuFactory _userMenuActionFactory; private readonly IAppDataPathProvider _appDataPathProvider; - + private readonly object _separator = new(); + private readonly object _singleItem = new(); + private readonly object _menuItem = new(); + public MainWindow( IRepositoryInformationAggregator aggregator, IRepositoryMonitor repositoryMonitor, @@ -210,15 +212,14 @@ private async void LstRepositories_ContextMenuOpening(object? sender, ContextMen return; } - // var currentCursor = ((FrameworkElement)e.Source).Cursor; - // ((FrameworkElement)e.Source).Cursor = Cursors.AppStarting; - var lstRepositoriesContextMenuOpening = await LstRepositoriesContextMenuOpeningWrapperAsync(((FrameworkElement)e.Source).ContextMenu).ConfigureAwait(true); + ContextMenu ctxMenu = ((FrameworkElement)e.Source).ContextMenu!; + var lstRepositoriesContextMenuOpening = await LstRepositoriesContextMenuOpeningWrapperAsync(ctxMenu).ConfigureAwait(true); if (!lstRepositoriesContextMenuOpening) { e.Handled = true; } } - + private async Task LstRepositoriesContextMenuOpeningWrapperAsync(ContextMenu ctxMenu) { try @@ -252,38 +253,58 @@ private async Task LstRepositoriesContextMenuOpeningAsync(ContextMenu ctxM return false; } - if (ctxMenu.Items.Count == 0) + int AddItemMenuAndSeparator(int count) { - for (int i = 0; i < 50; i++) + ctxMenu.Items.Add(new AcrylicMenuItem { - ctxMenu.Items.Add(new AcrylicMenuItem - { - Header = string.Empty, - Visibility = Visibility.Collapsed, - }); - ctxMenu.Items.Add(new Separator - { - Visibility = Visibility.Collapsed, - }); - } + Header = string.Empty, + Visibility = Visibility.Collapsed, + Tag = _singleItem, + IsEnabled = default, + }); + ctxMenu.Items.Add(new AcrylicMenuItem + { + Header = string.Empty, + Visibility = Visibility.Collapsed, + Items = { new Separator(), }, + IsEnabled = default, + Tag = _menuItem, + }); + ctxMenu.Items.Add(new Separator + { + Visibility = Visibility.Collapsed, + Tag = _separator, + }); + + return count + 3; } - int j = -1; - bool lastVisibleSeparator = false; + var index = -1; + var lastVisibleSeparator = false; + + var ctxMenuItemsCount = ctxMenu.Items.Count; await foreach (UserInterfaceRepositoryActionBase action in _userMenuActionFactory.CreateMenuAsync(vm.Repository).ConfigureAwait(true)) { - j++; + index++; if (action is UserInterfaceSeparatorRepositoryAction) { - while (ctxMenu.Items[j] is AcrylicMenuItem ami) + while (ctxMenuItemsCount > index && ctxMenu.Items[index] is AcrylicMenuItem ami) + { + if (ami.Visibility != Visibility.Collapsed) + { + ami.Visibility = Visibility.Collapsed; + } + index++; + } + + if (ctxMenuItemsCount <= index) { - ami.Visibility = Visibility.Collapsed; - ami.SoftReset(); - j++; + ctxMenuItemsCount = AddItemMenuAndSeparator(ctxMenuItemsCount); + index+=2; } - if (ctxMenu.Items[j] is Separator s) + if (ctxMenu.Items[index] is Separator s) { s.Visibility = lastVisibleSeparator ? Visibility.Collapsed @@ -294,49 +315,85 @@ private async Task LstRepositoriesContextMenuOpeningAsync(ContextMenu ctxM /* should never happen */ } - - - if (action is DeferredSubActionsUserInterfaceRepositoryAction or UserInterfaceRepositoryAction) + + if (action is /*DeferredSubActionsUserInterfaceRepositoryAction or */UserInterfaceRepositoryAction uira) { - while (ctxMenu.Items[j] is /*not AcrylicMenuItem*/ Separator s) + if (HasSubItems(uira)) { - s.Visibility = Visibility.Collapsed; - j++; - } + while (ctxMenuItemsCount > index && (ctxMenu.Items[index] is Separator s || (ctxMenu.Items[index] is AcrylicMenuItem ami1 && ami1.Tag == _singleItem))) + { + var ctrl = (Control)ctxMenu.Items[index]!; + if (ctrl.Visibility != Visibility.Collapsed) + { + ctrl.Visibility = Visibility.Collapsed; + } + index++; + } - var acrylicMenuItem = (AcrylicMenuItem)ctxMenu.Items[j]; - lastVisibleSeparator = false; + if (ctxMenuItemsCount <= index) + { + ctxMenuItemsCount = AddItemMenuAndSeparator(ctxMenuItemsCount); + index += 1; + } - if (action is UserInterfaceRepositoryAction repositoryAction) - { - acrylicMenuItem.Header = repositoryAction.Name; - acrylicMenuItem.IsEnabled = repositoryAction.CanExecute; - acrylicMenuItem.ClearItems(); - SetClick(acrylicMenuItem, repositoryAction, vm); - SetSubMenu(acrylicMenuItem, repositoryAction); - acrylicMenuItem.Visibility = Visibility.Visible; - continue; + var acrylicMenuItem = (AcrylicMenuItem)ctxMenu.Items[index]!; + if (acrylicMenuItem.Visibility != Visibility.Visible) + { + acrylicMenuItem.Visibility = Visibility.Visible; + } + + lastVisibleSeparator = false; + + acrylicMenuItem.SetHeader(uira.Name); + acrylicMenuItem.SetEnabled(uira.CanExecute); + SetSubMenu(acrylicMenuItem, uira); } + else + { + while (ctxMenuItemsCount > index && (ctxMenu.Items[index] is Separator s || (ctxMenu.Items[index] is AcrylicMenuItem ami1 && ami1.Tag == _menuItem))) + { + var ctrl = (Control)ctxMenu.Items[index]!; + if (ctrl.Visibility != Visibility.Collapsed) + { + ctrl.Visibility = Visibility.Collapsed; + } + index++; + } - /* should never happen */ + if (ctxMenuItemsCount <= index) + { + ctxMenuItemsCount = AddItemMenuAndSeparator(ctxMenuItemsCount); + index += 0; + } + + var acrylicMenuItem = (AcrylicMenuItem)ctxMenu.Items[index]!; + if (acrylicMenuItem.Visibility != Visibility.Visible) + { + acrylicMenuItem.Visibility = Visibility.Visible; + } + lastVisibleSeparator = false; + + acrylicMenuItem.SetHeader(uira.Name ); + acrylicMenuItem.SetEnabled(uira.CanExecute); + SetClick(acrylicMenuItem, uira, vm); + } } } if (!lastVisibleSeparator) { - j++; + index++; } - var len = ctxMenu.Items.Count; - while (j < len) + while (index < ctxMenuItemsCount) { - if (ctxMenu.Items[j] is AcrylicMenuItem ami) + var ctrl = (Control)ctxMenu.Items[index]!; + if (ctrl.Visibility != Visibility.Collapsed) { - ami.SoftReset(); + ctrl.Visibility = Visibility.Collapsed; } - ((Control)ctxMenu.Items[j]).Visibility = Visibility.Collapsed; - j++; + index++; } return true; @@ -397,6 +454,7 @@ private async void LstRepositories_KeyDown(object? sender, KeyEventArgs e) ContextMenu? ctxMenu = ((FrameworkElement)e.Source).ContextMenu; if (ctxMenu == null) { + e.Handled = true; return; } @@ -559,131 +617,61 @@ private void ShowUpdateIfAvailable() parent.ColumnDefinitions[Grid.GetColumn(UpdateButton)].Width = App.AvailableUpdate == null ? new GridLength(0) : GridLength.Auto; } - private Control? /*MenuItem*/ CreateMenuItem(RepositoryActionBase action, RepositoryViewModel? affectedViews = null) + private Control? CreateMenuItemAsync(UserInterfaceRepositoryActionBase action, RepositoryViewModel? affectedViews = null) { - if (action is RepositorySeparatorAction) + if (action is UserInterfaceSeparatorRepositoryAction) { return new Separator(); } - if (action is not RepositoryAction repositoryAction) + if (action is not UserInterfaceRepositoryAction repositoryAction) { // throw?? return null; } - Action clickAction = (object clickSender, object clickArgs) => - { - if (repositoryAction?.Action is null or NullRepositoryCommand) - { - return; - } - - // run actions in the UI async to not block it - if (repositoryAction.ExecutionCausesSynchronizing) - { - Task.Run(() => SetVmSynchronizing(affectedViews, true)) - .ContinueWith(t => _executor.Execute(action.Repository, action.Action)) - .ContinueWith(t => SetVmSynchronizing(affectedViews, false)); - } - else - { - Task.Run(() => _executor.Execute(action.Repository, action.Action)); - } - }; - var item = new AcrylicMenuItem { Header = repositoryAction.Name, IsEnabled = repositoryAction.CanExecute, }; - item.SetClick(new RoutedEventHandler(clickAction)); - - // this is a deferred submenu. We want to make sure that the context menu can pop up - // fast, while submenus are not evaluated yet. We don't want to make the context menu - // itself slow because the creation of the submenu items takes some time. - if (repositoryAction is DeferredSubActionsRepositoryAction deferredRepositoryAction && deferredRepositoryAction.DeferredSubActionsEnumerator != null) - { - // this is a template submenu item to enable submenus under the current - // menu item. this item gets removed when the real subitems are created - item.Items.Add(string.Empty); - - void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) - { - item.SubmenuOpened -= SelfDetachingEventHandler; - item.Items.Clear(); - - foreach (RepositoryActionBase subAction in deferredRepositoryAction.DeferredSubActionsEnumerator()) - { - Control? controlItem = CreateMenuItem(subAction); - if (controlItem != null) - { - item.Items.Add(controlItem); - } - } - } - - item.SubmenuOpened += SelfDetachingEventHandler; - } - else if (repositoryAction.SubActions != null) - { - foreach (RepositoryActionBase subAction in repositoryAction.SubActions) - { - Control? controlItem = CreateMenuItem(subAction); - if (controlItem != null) - { - item.Items.Add(controlItem); - } - } - } - + SetClick(item, repositoryAction, affectedViews); + SetSubMenu(item, repositoryAction); return item; } - private Control? CreateMenuItemNewStyleAsync(UserInterfaceRepositoryActionBase action, RepositoryViewModel? affectedViews = null) + private bool HasSubItems(UserInterfaceRepositoryAction repositoryAction) { - if (action is UserInterfaceSeparatorRepositoryAction) - { - return new Separator(); - } - - if (action is not UserInterfaceRepositoryAction repositoryAction) + if (repositoryAction is DeferredSubActionsUserInterfaceRepositoryAction) { - // throw?? - return null; + return true; } - var item = new AcrylicMenuItem - { - Header = repositoryAction.Name, - IsEnabled = repositoryAction.CanExecute, - }; - SetClick(item, repositoryAction, affectedViews); - SetSubMenu(item, repositoryAction); - return item; + return repositoryAction.SubActions != null; } private void SetSubMenu(AcrylicMenuItem item, UserInterfaceRepositoryAction repositoryAction) { - item.ClearItems(); - // this is a deferred submenu. We want to make sure that the context menu can pop up // fast, while submenus are not evaluated yet. We don't want to make the context menu // itself slow because the creation of the submenu items takes some time. if (repositoryAction is DeferredSubActionsUserInterfaceRepositoryAction deferredRepositoryAction) { - // this is a template submenu item to enable submenus under the current - // menu item. this item gets removed when the real subitems are created - item.Items.Add("Loading.."); + if (item.Items.Count == 0) + { + // this is a template submenu item to enable submenus under the current + // menu item. this item gets removed when the real subitems are created + item.Items.Add(new Separator()); + } - async void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) + async void SelfDetachingEventHandler(object sender, RoutedEventArgs evtArgs) { item.ClearSubMenuOpened(); item.ClearItems(); - foreach (UserInterfaceRepositoryActionBase subAction in await deferredRepositoryAction.GetAsync().ConfigureAwait(true)) + foreach (UserInterfaceRepositoryActionBase subAction in await item.DataTask.ConfigureAwait(true)) { - Control? controlItem = CreateMenuItemNewStyleAsync(subAction); + Control? controlItem = CreateMenuItemAsync(subAction); if (controlItem == null) { continue; @@ -706,16 +694,15 @@ async void SelfDetachingEventHandler(object _, RoutedEventArgs evtArgs) { item.Items.RemoveAt(count - 1); } + + item.ClearData(); } + item.LoadData(deferredRepositoryAction); item.SetSubMenuOpened(SelfDetachingEventHandler); } else if (repositoryAction.SubActions != null) { - // this is a template submenu item to enable submenus under the current - // menu item. this item gets removed when the real subitems are created - item.Items.Add("Loading.."); - async void SelfDetachingEventHandler1(object _, RoutedEventArgs evtArgs) { item.ClearSubMenuOpened(); @@ -723,7 +710,7 @@ async void SelfDetachingEventHandler1(object _, RoutedEventArgs evtArgs) foreach (UserInterfaceRepositoryActionBase subAction in repositoryAction.SubActions) { - Control? controlItem = CreateMenuItemNewStyleAsync(subAction); + Control? controlItem = CreateMenuItemAsync(subAction); if (controlItem == null) { continue; @@ -748,6 +735,13 @@ async void SelfDetachingEventHandler1(object _, RoutedEventArgs evtArgs) } } + if (item.Items.Count == 0) + { + // this is a template submenu item to enable submenus under the current + // menu item. this item gets removed when the real subitems are created + item.Items.Add(new Separator()); + } + item.SetSubMenuOpened(SelfDetachingEventHandler1); } } diff --git a/src/RepoM.App/Services/UserMenuActionMenuFactory.cs b/src/RepoM.App/Services/UserMenuActionMenuFactory.cs index 81f07f48..5ed2d3dc 100644 --- a/src/RepoM.App/Services/UserMenuActionMenuFactory.cs +++ b/src/RepoM.App/Services/UserMenuActionMenuFactory.cs @@ -17,6 +17,7 @@ internal class UserMenuActionMenuFactory : IUserMenuActionMenuFactory private readonly IFileSystem _fileSystem; private readonly IAppDataPathProvider _appDataPathProvider; private readonly IUserInterfaceActionMenuFactory _factory; + private readonly string _fullFilename; public UserMenuActionMenuFactory( IFileSystem fileSystem, @@ -26,12 +27,13 @@ public UserMenuActionMenuFactory( _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _appDataPathProvider = appDataPathProvider ?? throw new ArgumentNullException(nameof(appDataPathProvider)); _factory = userInterfaceActionMenuFactory ?? throw new ArgumentNullException(nameof(userInterfaceActionMenuFactory)); + + _fullFilename = System.IO.Path.Combine(_appDataPathProvider.AppDataPath, FILENAME); } public async IAsyncEnumerable CreateMenuAsync(IRepository repository) { - var fullFilename = System.IO.Path.Combine(_appDataPathProvider.AppDataPath, FILENAME); - var fileExists = _fileSystem.File.Exists(fullFilename); + var fileExists = _fileSystem.File.Exists(_fullFilename); if (!fileExists) { @@ -54,7 +56,7 @@ public async IAsyncEnumerable CreateMenuAsync yield break; } - await foreach (UserInterfaceRepositoryActionBase item in _factory.CreateMenuAsync(repository, fullFilename).ConfigureAwait(false)) + await foreach (UserInterfaceRepositoryActionBase item in _factory.CreateMenuAsync(repository, _fullFilename).ConfigureAwait(false)) { yield return item; } diff --git a/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs index 4a24b61a..529d65d2 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs +++ b/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs @@ -27,10 +27,6 @@ public class DisposableContextScriptObjectTests private readonly IActionToRepositoryActionMapper[] _mapper = []; private readonly IActionMenuDeserializer _deserializer = A.Fake(); private readonly ActionMenuGenerationContext _context; - private readonly EnvSetScriptObject _env = new(new EnvScriptObject(new Dictionary() - { - { "x", "y" }, - })); private readonly IContextActionProcessor[] _mappers; private readonly DisposableContextScriptObject _sut; @@ -44,7 +40,7 @@ public DisposableContextScriptObjectTests() _context = new ActionMenuGenerationContext(_templateParser, _fileSystem, _functionsArray, _mapper, _deserializer, _mappers); _context.Initialize(_repository); - _sut = new DisposableContextScriptObject(_context, _env, _mappers); + _sut = new DisposableContextScriptObject(_context, _mappers); } [Fact] @@ -53,14 +49,12 @@ public void Ctor_ShouldThrow_WhenArgumentNull() // arrange // act - Func act1 = () => new DisposableContextScriptObject(_context, _env, null!); - Func act2 = () => new DisposableContextScriptObject(_context, null!, _mappers); - Func act3 = () => new DisposableContextScriptObject(null!, _env, _mappers); + Func act1 = () => new DisposableContextScriptObject(_context, null!); + Func act2 = () => new DisposableContextScriptObject(null!, _mappers); // assert act1.Should().Throw(); act2.Should().Throw(); - act3.Should().Throw(); } [Fact] @@ -135,13 +129,13 @@ public async Task AddContextActionAsync_ShouldUseMapperForProcessing_WhenMapperF public void PushEnvironmentVariable_ShouldAddVariables() { // arrange - var env = new EnvSetScriptObject( - new EnvScriptObject( - new Dictionary() - { - { "x", "y" }, - })); - var sut = new DisposableContextScriptObject(_context, env, _mappers); + var env = new EnvScriptObject( + new Dictionary + { + { "x", "y" }, + }); + _context.Env.Push(env); + IScope sut = new DisposableContextScriptObject(_context, _mappers); // assume env.Count.Should().Be(1); @@ -163,13 +157,13 @@ public void PushEnvironmentVariable_ShouldAddVariables() public void PushEnvironmentVariable_ShouldAddVariables_Distinct() { // arrange - var env = new EnvSetScriptObject( - new EnvScriptObject( - new Dictionary() - { - { "x", "y" }, - })); - var sut = new DisposableContextScriptObject(_context, env, _mappers); + var env = new EnvScriptObject( + new Dictionary + { + { "x", "y" }, + }); + _context.Env.Push(env); + IScope sut = new DisposableContextScriptObject(_context, _mappers); // assume env.Count.Should().Be(1); @@ -191,14 +185,14 @@ public void PushEnvironmentVariable_ShouldAddVariables_Distinct() public void Dispose_ShouldPopAllPushedEnvironmentVariables() { // arrange - var env = new EnvSetScriptObject( - new EnvScriptObject( - new Dictionary() - { - { "x1", "y" }, - { "x2", "yy" }, - })); - var sut = new DisposableContextScriptObject(_context, env, _mappers); + var env = new EnvScriptObject( + new Dictionary + { + { "x1", "y" }, + { "x2", "yy" }, + }); + _context.Env.Push(env); + IScope sut = new DisposableContextScriptObject(_context, _mappers); sut.PushEnvironmentVariable( new Dictionary { From dae60b5413d95ccafd6c3ae15159ee2a086987eb Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Fri, 11 Oct 2024 09:28:55 +0200 Subject: [PATCH 06/12] Fix --- .../EnvironmentsCacheDecorator.cs | 20 +++++ .../Abstractions/IEnvironment.cs | 8 ++ .../Abstractions/SystemEnvironments.cs | 36 ++++++++ .../ActionMenu/Context/EnvSetScriptObject.cs | 4 + src/RepoM.ActionMenu.Core/Bootstrapper.cs | 4 + .../Model/ActionMenuGenerationContext.cs | 8 +- .../UserInterfaceActionMenuFactory.cs | 8 +- .../DisposableContextScriptObjectTests.cs | 86 ++++++++++++------- 8 files changed, 137 insertions(+), 37 deletions(-) create mode 100644 src/RepoM.ActionMenu.Core/Abstractions/EnvironmentsCacheDecorator.cs create mode 100644 src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs create mode 100644 src/RepoM.ActionMenu.Core/Abstractions/SystemEnvironments.cs diff --git a/src/RepoM.ActionMenu.Core/Abstractions/EnvironmentsCacheDecorator.cs b/src/RepoM.ActionMenu.Core/Abstractions/EnvironmentsCacheDecorator.cs new file mode 100644 index 00000000..311f11c9 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Abstractions/EnvironmentsCacheDecorator.cs @@ -0,0 +1,20 @@ +namespace RepoM.ActionMenu.Core.Abstractions; + +using System; +using System.Collections.Generic; + +internal sealed class EnvironmentsCacheDecorator: IEnvironment +{ + private readonly IEnvironment _decoratee; + private Dictionary? _cachedValue; + + public EnvironmentsCacheDecorator(IEnvironment decoratee) + { + _decoratee = decoratee ?? throw new ArgumentNullException(nameof(decoratee)); + } + + public Dictionary GetEnvironmentVariables() + { + return _cachedValue ??= _decoratee.GetEnvironmentVariables(); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs b/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs new file mode 100644 index 00000000..348062ea --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Abstractions; + +using System.Collections.Generic; + +internal interface IEnvironment +{ + Dictionary GetEnvironmentVariables(); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Abstractions/SystemEnvironments.cs b/src/RepoM.ActionMenu.Core/Abstractions/SystemEnvironments.cs new file mode 100644 index 00000000..bf6a6ad9 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Abstractions/SystemEnvironments.cs @@ -0,0 +1,36 @@ +namespace RepoM.ActionMenu.Core.Abstractions; + +using System; +using System.Collections; +using System.Collections.Generic; + +internal sealed class SystemEnvironments : IEnvironment +{ + private SystemEnvironments() + { + } + + public static SystemEnvironments Instance { get; } = new (); + + public Dictionary GetEnvironmentVariables() + { + var env = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (DictionaryEntry item in Environment.GetEnvironmentVariables()) + { + if (item.Key is not string key || string.IsNullOrEmpty(key)) + { + continue; + } + + if (item.Value is not string value) + { + continue; + } + + env.Add(key.Trim(), value); + } + + return env; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs index 25f6c615..8ef55f63 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs @@ -12,6 +12,10 @@ internal sealed class EnvSetScriptObject : IScriptObject, IDisposable { private FastStack _stack = new(10); + public EnvSetScriptObject(IDictionary envVars) : this(new EnvScriptObject(envVars)) + { + } + public EnvSetScriptObject(EnvScriptObject @base) { _ = @base ?? throw new ArgumentNullException(nameof(@base)); diff --git a/src/RepoM.ActionMenu.Core/Bootstrapper.cs b/src/RepoM.ActionMenu.Core/Bootstrapper.cs index 017ef592..791d506d 100644 --- a/src/RepoM.ActionMenu.Core/Bootstrapper.cs +++ b/src/RepoM.ActionMenu.Core/Bootstrapper.cs @@ -5,6 +5,7 @@ namespace RepoM.ActionMenu.Core; using System.Diagnostics; using System.Linq; using System.Reflection; +using RepoM.ActionMenu.Core.Abstractions; using RepoM.ActionMenu.Core.ConfigReader; using RepoM.ActionMenu.Core.Misc; using RepoM.ActionMenu.Core.Model; @@ -55,6 +56,9 @@ private static void RegisterPrivateTypes(Container container) container.RegisterSingleton(); container.RegisterDecorator(Lifestyle.Singleton); + + container.RegisterInstance(SystemEnvironments.Instance); + container.RegisterDecorator(Lifestyle.Singleton); } /// diff --git a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs index 505eeb60..efc369d4 100644 --- a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs +++ b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs @@ -5,6 +5,7 @@ namespace RepoM.ActionMenu.Core.Model; using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Abstractions; using RepoM.ActionMenu.Core.ActionMenu.Context; using RepoM.ActionMenu.Core.Misc; using RepoM.ActionMenu.Core.Yaml.Model.ActionContext; @@ -23,6 +24,7 @@ namespace RepoM.ActionMenu.Core.Model; internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerationContext, IContextMenuActionMenuGenerationContext { private readonly ITemplateParser _templateParser; + private readonly IEnvironment _environment; private readonly ITemplateContextRegistration[] _functionsArray; private readonly IActionMenuDeserializer _deserializer; private readonly IActionToRepositoryActionMapper[] _repositoryActionMappers; @@ -35,12 +37,14 @@ internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerat public ActionMenuGenerationContext( ITemplateParser templateParser, IFileSystem fileSystem, + IEnvironment environment, ITemplateContextRegistration[] functionsArray, IActionToRepositoryActionMapper[] repositoryActionMappers, IActionMenuDeserializer deserializer, IContextActionProcessor[] contextActionMappers) { _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser)); + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); _functionsArray = functionsArray ?? throw new ArgumentNullException(nameof(functionsArray)); FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _repositoryActionMappers = repositoryActionMappers ?? throw new ArgumentNullException(nameof(repositoryActionMappers)); @@ -102,6 +106,7 @@ public IActionMenuGenerationContext Clone() var result = new ActionMenuGenerationContext( _templateParser, FileSystem, + _environment, _functionsArray, _repositoryActionMappers, _deserializer, @@ -115,8 +120,7 @@ internal void Initialize(IRepository repository) { Repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _rootScriptObject = CreateAndInitRepoMScriptObject( - new EnvSetScriptObject(EnvScriptObject.Instance)); + _rootScriptObject = CreateAndInitRepoMScriptObject(new EnvSetScriptObject(_environment.GetEnvironmentVariables())); foreach (ITemplateContextRegistration contextRegistration in _functionsArray) { diff --git a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs index 873ca69a..ec07bb32 100644 --- a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs +++ b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs @@ -7,6 +7,7 @@ namespace RepoM.ActionMenu.Core.Services; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using RepoM.ActionMenu.Core; +using RepoM.ActionMenu.Core.Abstractions; using RepoM.ActionMenu.Core.ConfigReader; using RepoM.ActionMenu.Core.Misc; using RepoM.ActionMenu.Core.Model; @@ -25,6 +26,7 @@ namespace RepoM.ActionMenu.Core.Services; internal class UserInterfaceActionMenuFactory : IUserInterfaceActionMenuFactory { private readonly IFileSystem _fileSystem; + private readonly IEnvironment _environment; private readonly ITemplateParser _templateParser; private readonly ITemplateContextRegistration[] _plugins; private readonly IActionToRepositoryActionMapper[] _mappers; @@ -34,7 +36,8 @@ internal class UserInterfaceActionMenuFactory : IUserInterfaceActionMenuFactory private readonly IContextActionProcessor[] _contextActionMappers; public UserInterfaceActionMenuFactory( - IFileSystem fileSystem, + IFileSystem fileSystem, + IEnvironment environment, ITemplateParser templateParser, IEnumerable plugins, IEnumerable mappers, @@ -43,6 +46,7 @@ public UserInterfaceActionMenuFactory( ILogger logger) { _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser)); _plugins = plugins.ToArray(); _mappers = mappers.ToArray(); @@ -101,7 +105,7 @@ private async Task CreateActionMenuGenerationContex await Task.Yield(); _logger.LogTrace("CreateActionMenuGenerationContext ActionMenuGenerationContext ctor"); - var actionMenuGenerationContext = new ActionMenuGenerationContext(_templateParser, _fileSystem, _plugins, _mappers, _deserializer, _contextActionMappers); + var actionMenuGenerationContext = new ActionMenuGenerationContext(_templateParser, _fileSystem, _environment, _plugins, _mappers, _deserializer, _contextActionMappers); actionMenuGenerationContext.Initialize(repository); return actionMenuGenerationContext; } diff --git a/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs index 529d65d2..683d12d6 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs +++ b/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs @@ -7,7 +7,6 @@ namespace RepoM.ActionMenu.Core.Tests.Model; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; -using RepoM.ActionMenu.Core.ActionMenu.Context; using RepoM.ActionMenu.Core.Misc; using RepoM.ActionMenu.Core.Model; using RepoM.ActionMenu.Core.Yaml.Model.ActionContext; @@ -17,6 +16,7 @@ namespace RepoM.ActionMenu.Core.Tests.Model; using RepoM.ActionMenu.Interface.YamlModel.Templating; using RepoM.Core.Plugin.Repository; using Xunit; +using IEnvironment = RepoM.ActionMenu.Core.Abstractions.IEnvironment; public class DisposableContextScriptObjectTests { @@ -26,21 +26,25 @@ public class DisposableContextScriptObjectTests private readonly ITemplateContextRegistration[] _functionsArray = []; private readonly IActionToRepositoryActionMapper[] _mapper = []; private readonly IActionMenuDeserializer _deserializer = A.Fake(); - private readonly ActionMenuGenerationContext _context; + private ActionMenuGenerationContext _context; private readonly IContextActionProcessor[] _mappers; private readonly DisposableContextScriptObject _sut; + private readonly IEnvironment _environment = A.Fake(); public DisposableContextScriptObjectTests() { + A.CallTo(() => _environment.GetEnvironmentVariables()).Returns(new Dictionary() + { + { "x", "y" }, + }); + _mappers = [ A.Fake(), A.Fake(), ]; - _context = new ActionMenuGenerationContext(_templateParser, _fileSystem, _functionsArray, _mapper, _deserializer, _mappers); - _context.Initialize(_repository); - - _sut = new DisposableContextScriptObject(_context, _mappers); + _context = CreateContext(); + _sut = CreateSut(); } [Fact] @@ -129,16 +133,17 @@ public async Task AddContextActionAsync_ShouldUseMapperForProcessing_WhenMapperF public void PushEnvironmentVariable_ShouldAddVariables() { // arrange - var env = new EnvScriptObject( - new Dictionary + A.CallTo(() => _environment.GetEnvironmentVariables()) + .Returns(new Dictionary { { "x", "y" }, }); - _context.Env.Push(env); - IScope sut = new DisposableContextScriptObject(_context, _mappers); + + _context = CreateContext(); + IScope sut = CreateSut(); // assume - env.Count.Should().Be(1); + _context.Env.Count.Should().Be(1); // act sut.PushEnvironmentVariable( @@ -149,24 +154,25 @@ public void PushEnvironmentVariable_ShouldAddVariables() }); // assert - env.Count.Should().Be(3); - env.GetMembers().Should().BeEquivalentTo("x", "x1", "x2"); + _context.Env.Count.Should().Be(3); + _context.Env.GetMembers().Should().BeEquivalentTo("x", "x1", "x2"); } [Fact] public void PushEnvironmentVariable_ShouldAddVariables_Distinct() { // arrange - var env = new EnvScriptObject( - new Dictionary - { - { "x", "y" }, - }); - _context.Env.Push(env); - IScope sut = new DisposableContextScriptObject(_context, _mappers); + A.CallTo(() => _environment.GetEnvironmentVariables()) + .Returns(new Dictionary + { + { "x", "y" }, + }); + + _context = CreateContext(); + IScope sut = CreateSut(); // assume - env.Count.Should().Be(1); + _context.Env.Count.Should().Be(1); // act sut.PushEnvironmentVariable( @@ -177,22 +183,24 @@ public void PushEnvironmentVariable_ShouldAddVariables_Distinct() }); // assert - env.Count.Should().Be(2); - env.GetMembers().Should().BeEquivalentTo("x", "x1"); + _context.Env.Count.Should().Be(2); + _context.Env.GetMembers().Should().BeEquivalentTo("x", "x1"); } [Fact] public void Dispose_ShouldPopAllPushedEnvironmentVariables() { // arrange - var env = new EnvScriptObject( - new Dictionary - { - { "x1", "y" }, - { "x2", "yy" }, - }); - _context.Env.Push(env); - IScope sut = new DisposableContextScriptObject(_context, _mappers); + A.CallTo(() => _environment.GetEnvironmentVariables()) + .Returns(new Dictionary + { + { "x1", "y" }, + { "x2", "yy" }, + }); + + _context = CreateContext(); + IScope sut = CreateSut(); + sut.PushEnvironmentVariable( new Dictionary { @@ -207,13 +215,25 @@ public void Dispose_ShouldPopAllPushedEnvironmentVariables() }); // assume - env.Count.Should().Be(5); + _context.Env.Count.Should().Be(5); // act sut.Dispose(); // assert - env.Count.Should().Be(2); + _context.Env.Count.Should().Be(2); + } + + private ActionMenuGenerationContext CreateContext() + { + var context = new ActionMenuGenerationContext(_templateParser, _fileSystem, _environment, _functionsArray, _mapper, _deserializer, _mappers); + context.Initialize(_repository); + return context; + } + + private DisposableContextScriptObject CreateSut() + { + return new DisposableContextScriptObject(_context, _mappers); } } From 036ac2f5f78df9f551a5231b4e371c271645f779 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Fri, 11 Oct 2024 20:25:29 +0200 Subject: [PATCH 07/12] update --- docs/mdsource/repom.generated.source.md | 1 + .../Abstractions/IEnvironment.cs | 3 + .../Abstractions/OperatingSystem.cs | 19 ++ .../ActionMenu/Context/EnvScriptObject.cs | 37 +--- .../Folder/RepositoryActionFolderV1.cs | 6 +- src/RepoM.ActionMenu.Core/Bootstrapper.cs | 3 + .../Model/ActionMenuGenerationContext.cs | 31 ++- .../UserInterfaceActionMenuFactory.cs | 19 +- .../Yaml/Model/ActionMenus/IDeferred.cs | 8 - src/RepoM.App/MainWindow.xaml.cs | 6 +- .../EnvironmentsCacheDecoratorTests.cs | 97 +++++++++ .../Abstractions/OperatingSystemTests.cs | 52 +++++ .../Abstractions/SystemEnvironmentsTests.cs | 22 ++ .../Context/EnvScriptObjectTests.cs | 195 ++++++++++++++++++ .../Docs/folder@1-scenario01.testfile.yaml | 1 + .../DisposableContextScriptObjectTests.cs | 8 +- .../UserMenuActionMenuFactoryTests.cs | 29 +++ 17 files changed, 461 insertions(+), 76 deletions(-) create mode 100644 src/RepoM.ActionMenu.Core/Abstractions/OperatingSystem.cs delete mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs create mode 100644 tests/RepoM.ActionMenu.Core.Tests/Abstractions/EnvironmentsCacheDecoratorTests.cs create mode 100644 tests/RepoM.ActionMenu.Core.Tests/Abstractions/OperatingSystemTests.cs create mode 100644 tests/RepoM.ActionMenu.Core.Tests/Abstractions/SystemEnvironmentsTests.cs create mode 100644 tests/RepoM.ActionMenu.Core.Tests/ActionMenu/Context/EnvScriptObjectTests.cs create mode 100644 tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs diff --git a/docs/mdsource/repom.generated.source.md b/docs/mdsource/repom.generated.source.md index d59cf0e3..803758ef 100644 --- a/docs/mdsource/repom.generated.source.md +++ b/docs/mdsource/repom.generated.source.md @@ -75,6 +75,7 @@ Properties: - `name`: Name of the menu item. ([Text](repository_action_types.md#text)) - `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate)) - `context`: The context in which the action is available. ([Context](repository_action_types.md#context)) +- `is-deferred`: Whether the folder is deferred. ([Predicate](repository_action_types.md#predicate)) ### Example diff --git a/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs b/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs index 348062ea..343f8740 100644 --- a/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs +++ b/src/RepoM.ActionMenu.Core/Abstractions/IEnvironment.cs @@ -2,6 +2,9 @@ namespace RepoM.ActionMenu.Core.Abstractions; using System.Collections.Generic; +/// +/// Abstraction of the environment. +/// internal interface IEnvironment { Dictionary GetEnvironmentVariables(); diff --git a/src/RepoM.ActionMenu.Core/Abstractions/OperatingSystem.cs b/src/RepoM.ActionMenu.Core/Abstractions/OperatingSystem.cs new file mode 100644 index 00000000..da6b51ed --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Abstractions/OperatingSystem.cs @@ -0,0 +1,19 @@ +namespace RepoM.ActionMenu.Core.Abstractions; + +using System; +using System.IO.Abstractions; + +internal class OperatingSystem +{ + public OperatingSystem(IFileSystem fileSystem, IEnvironment environment) + { + FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + Environment = environment ?? throw new ArgumentNullException(nameof(environment)); + } + + /// + public IFileSystem FileSystem { get; } + + /// + public IEnvironment Environment { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs index 48a9df2b..f2402b34 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs @@ -1,7 +1,6 @@ namespace RepoM.ActionMenu.Core.ActionMenu.Context; using System; -using System.Collections; using System.Collections.Generic; using Scriban; using Scriban.Parsing; @@ -11,33 +10,17 @@ internal sealed class EnvScriptObject : IScriptObject { private readonly IDictionary _env; - private static EnvScriptObject Create() + public EnvScriptObject(IDictionary envVars) { - var env = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (DictionaryEntry item in Environment.GetEnvironmentVariables()) // difficult to test. - { - if (item.Key is not string key || string.IsNullOrEmpty(key)) - { - continue; - } - - if (item.Value is not string value) - { - continue; - } - - env.Add(key.Trim(), value); - } - - return new EnvScriptObject(env); + _env = envVars ?? throw new ArgumentNullException(nameof(envVars)); } - public static EnvScriptObject Instance { get; } = Create(); + public int Count => _env.Count; - public EnvScriptObject(IDictionary envVars) + public bool IsReadOnly { - _env = envVars; + get => true; + set => _ = value; } public IEnumerable GetMembers() @@ -91,12 +74,4 @@ IScriptObject IScriptObject.Clone(bool deep) { return Clone(); } - - public int Count => _env.Count; - - public bool IsReadOnly - { - get => true; - set => _ = value; - } } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs index fc2aa070..bbf09770 100644 --- a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs +++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs @@ -13,7 +13,7 @@ namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Folder; /// /// [RepositoryAction(TYPE_VALUE)] -internal sealed class RepositoryActionFolderV1 : IMenuAction, IName, IMenuActions, IContext, IDeferred +internal sealed class RepositoryActionFolderV1 : IMenuAction, IName, IMenuActions, IContext { public const string TYPE_VALUE = "folder@1"; internal const string EXAMPLE_1 = TYPE_VALUE + "-scenario01"; @@ -38,7 +38,9 @@ public string Type /// public Context? Context { get; set; } - // Not documented as it is not implemented yet. Deferred with cloning the context doesn't work yet (https://github.com/coenm/RepoM/issues/85) + /// + /// Whether the folder is deferred. + /// [Predicate(false)] public Predicate IsDeferred { get; set; } = new ScribanPredicate(); diff --git a/src/RepoM.ActionMenu.Core/Bootstrapper.cs b/src/RepoM.ActionMenu.Core/Bootstrapper.cs index 791d506d..dcfd07f6 100644 --- a/src/RepoM.ActionMenu.Core/Bootstrapper.cs +++ b/src/RepoM.ActionMenu.Core/Bootstrapper.cs @@ -15,6 +15,7 @@ namespace RepoM.ActionMenu.Core; using RepoM.ActionMenu.Interface.YamlModel; using RepoM.Core.Plugin.RepositoryOrdering.Configuration; using SimpleInjector; +using OperatingSystem = Abstractions.OperatingSystem; public static class Bootstrapper { @@ -59,6 +60,8 @@ private static void RegisterPrivateTypes(Container container) container.RegisterInstance(SystemEnvironments.Instance); container.RegisterDecorator(Lifestyle.Singleton); + + container.RegisterSingleton(); } /// diff --git a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs index efc369d4..6dac3e6f 100644 --- a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs +++ b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs @@ -5,7 +5,6 @@ namespace RepoM.ActionMenu.Core.Model; using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; -using RepoM.ActionMenu.Core.Abstractions; using RepoM.ActionMenu.Core.ActionMenu.Context; using RepoM.ActionMenu.Core.Misc; using RepoM.ActionMenu.Core.Yaml.Model.ActionContext; @@ -19,15 +18,16 @@ namespace RepoM.ActionMenu.Core.Model; using Scriban.Runtime; using FileFunctions = RepoM.ActionMenu.Core.ActionMenu.Context.FileFunctions; using IRepository = RepoM.Core.Plugin.Repository.IRepository; +using OperatingSystem = RepoM.ActionMenu.Core.Abstractions.OperatingSystem; using RepositoryFunctions = RepoM.ActionMenu.Core.ActionMenu.Context.RepositoryFunctions; internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerationContext, IContextMenuActionMenuGenerationContext { private readonly ITemplateParser _templateParser; - private readonly IEnvironment _environment; + private readonly OperatingSystem _operatingSystem; private readonly ITemplateContextRegistration[] _functionsArray; - private readonly IActionMenuDeserializer _deserializer; - private readonly IActionToRepositoryActionMapper[] _repositoryActionMappers; + private readonly IActionMenuDeserializer _deserializer; + private readonly IActionToRepositoryActionMapper[] _repositoryActionMappers; private readonly IContextActionProcessor[] _contextActionMappers; private RepoMScriptObject _rootScriptObject = null!; private EnvSetScriptObject? _env; @@ -35,18 +35,16 @@ internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerat private DisposableContextScriptObject _repositoryActionsScriptContext = null!; public ActionMenuGenerationContext( - ITemplateParser templateParser, - IFileSystem fileSystem, - IEnvironment environment, + ITemplateParser templateParser, + OperatingSystem operatingSystem, ITemplateContextRegistration[] functionsArray, IActionToRepositoryActionMapper[] repositoryActionMappers, IActionMenuDeserializer deserializer, IContextActionProcessor[] contextActionMappers) { _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser)); - _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _operatingSystem = operatingSystem ?? throw new ArgumentNullException(nameof(operatingSystem)); _functionsArray = functionsArray ?? throw new ArgumentNullException(nameof(functionsArray)); - FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _repositoryActionMappers = repositoryActionMappers ?? throw new ArgumentNullException(nameof(repositoryActionMappers)); _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); _contextActionMappers = contextActionMappers ?? throw new ArgumentNullException(nameof(contextActionMappers)); @@ -54,8 +52,8 @@ public ActionMenuGenerationContext( public IRepository Repository { get; private set; } = null!; - public IFileSystem FileSystem { get; } - + public IFileSystem FileSystem => _operatingSystem.FileSystem; + public EnvSetScriptObject Env => _env ??= (EnvSetScriptObject)_rootScriptObject["env"]; public async Task AddRepositoryContextAsync(Context? reposContext) @@ -105,8 +103,7 @@ public IActionMenuGenerationContext Clone() { var result = new ActionMenuGenerationContext( _templateParser, - FileSystem, - _environment, + _operatingSystem, _functionsArray, _repositoryActionMappers, _deserializer, @@ -120,7 +117,7 @@ internal void Initialize(IRepository repository) { Repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _rootScriptObject = CreateAndInitRepoMScriptObject(new EnvSetScriptObject(_environment.GetEnvironmentVariables())); + _rootScriptObject = CreateAndInitRepoMScriptObject(new EnvSetScriptObject(_operatingSystem.Environment.GetEnvironmentVariables())); foreach (ITemplateContextRegistration contextRegistration in _functionsArray) { @@ -144,10 +141,10 @@ private void InitializeFrom(ActionMenuGenerationContext @this) PushGlobal(_rootScriptObject); - // -2 because _rootScriptObject and RepositoryActionsScriptContext are already added - if (@this.GlobalCount -2 != @this._globals.Count) + const int OFFSET = 2; // Because _rootScriptObject and RepositoryActionsScriptContext are already added + if (@this.GlobalCount != @this._globals.Count + OFFSET) { - throw new Exception(); + throw new InvalidOperationException($"Could not clone TemplateContext because of a mismatch between global counts. GlobalCount was {@this.GlobalCount} and expected was {@this._globals.Count + OFFSET}"); } _repositoryActionsScriptContext = new DisposableContextScriptObject(this, _contextActionMappers); diff --git a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs index ec07bb32..bdf3fcd8 100644 --- a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs +++ b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs @@ -2,12 +2,10 @@ namespace RepoM.ActionMenu.Core.Services; using System; using System.Collections.Generic; -using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using RepoM.ActionMenu.Core; -using RepoM.ActionMenu.Core.Abstractions; using RepoM.ActionMenu.Core.ConfigReader; using RepoM.ActionMenu.Core.Misc; using RepoM.ActionMenu.Core.Model; @@ -22,11 +20,11 @@ namespace RepoM.ActionMenu.Core.Services; using RepoM.ActionMenu.Interface.UserInterface; using RepoM.ActionMenu.Interface.YamlModel; using RepoM.Core.Plugin.Repository; +using OperatingSystem = RepoM.ActionMenu.Core.Abstractions.OperatingSystem; internal class UserInterfaceActionMenuFactory : IUserInterfaceActionMenuFactory { - private readonly IFileSystem _fileSystem; - private readonly IEnvironment _environment; + private readonly OperatingSystem _operatingSystem; private readonly ITemplateParser _templateParser; private readonly ITemplateContextRegistration[] _plugins; private readonly IActionToRepositoryActionMapper[] _mappers; @@ -36,8 +34,7 @@ internal class UserInterfaceActionMenuFactory : IUserInterfaceActionMenuFactory private readonly IContextActionProcessor[] _contextActionMappers; public UserInterfaceActionMenuFactory( - IFileSystem fileSystem, - IEnvironment environment, + OperatingSystem operatingSystem, ITemplateParser templateParser, IEnumerable plugins, IEnumerable mappers, @@ -45,8 +42,7 @@ public UserInterfaceActionMenuFactory( IFileReader fileReader, ILogger logger) { - _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); - _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _operatingSystem = operatingSystem ?? throw new ArgumentNullException(nameof(operatingSystem)); _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser)); _plugins = plugins.ToArray(); _mappers = mappers.ToArray(); @@ -70,10 +66,8 @@ public async IAsyncEnumerable CreateMenuAsync ActionMenuRoot actions = await LoadAsync(filename).ConfigureAwait(false); - // process context (vars + methods) await context.AddRepositoryContextAsync(actions.Context).ConfigureAwait(false); - // process actions await foreach (UserInterfaceRepositoryActionBase item in context.AddActionMenusAsync(actions.ActionMenu).ConfigureAwait(false)) { yield return item; @@ -84,7 +78,6 @@ public async Task> GetTagsAsync(IRepository repository, stri { ActionMenuGenerationContext context = await CreateActionMenuGenerationContext(repository).ConfigureAwait(false); - // load yaml TagsRoot actions = await LoadTagsAsync(filename).ConfigureAwait(false); if (actions.Tags == null) @@ -92,10 +85,8 @@ public async Task> GetTagsAsync(IRepository repository, stri return []; } - // process context (vars + methods) await context.AddRepositoryContextAsync(actions.Context).ConfigureAwait(false); - // process tags return await context.GetTagsAsync(actions.Tags).ConfigureAwait(false); } @@ -105,7 +96,7 @@ private async Task CreateActionMenuGenerationContex await Task.Yield(); _logger.LogTrace("CreateActionMenuGenerationContext ActionMenuGenerationContext ctor"); - var actionMenuGenerationContext = new ActionMenuGenerationContext(_templateParser, _fileSystem, _environment, _plugins, _mappers, _deserializer, _contextActionMappers); + var actionMenuGenerationContext = new ActionMenuGenerationContext(_templateParser, _operatingSystem, _plugins, _mappers, _deserializer, _contextActionMappers); actionMenuGenerationContext.Initialize(repository); return actionMenuGenerationContext; } diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs deleted file mode 100644 index 37514e9a..00000000 --- a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; - -using RepoM.ActionMenu.Interface.YamlModel.Templating; - -internal interface IDeferred -{ - Predicate IsDeferred { get; } -} \ No newline at end of file diff --git a/src/RepoM.App/MainWindow.xaml.cs b/src/RepoM.App/MainWindow.xaml.cs index b7614d8f..c40d1e03 100644 --- a/src/RepoM.App/MainWindow.xaml.cs +++ b/src/RepoM.App/MainWindow.xaml.cs @@ -320,7 +320,7 @@ int AddItemMenuAndSeparator(int count) { if (HasSubItems(uira)) { - while (ctxMenuItemsCount > index && (ctxMenu.Items[index] is Separator s || (ctxMenu.Items[index] is AcrylicMenuItem ami1 && ami1.Tag == _singleItem))) + while (ctxMenuItemsCount > index && (ctxMenu.Items[index] is Separator || (ctxMenu.Items[index] is AcrylicMenuItem ami1 && ami1.Tag == _singleItem))) { var ctrl = (Control)ctxMenu.Items[index]!; if (ctrl.Visibility != Visibility.Collapsed) @@ -350,7 +350,7 @@ int AddItemMenuAndSeparator(int count) } else { - while (ctxMenuItemsCount > index && (ctxMenu.Items[index] is Separator s || (ctxMenu.Items[index] is AcrylicMenuItem ami1 && ami1.Tag == _menuItem))) + while (ctxMenuItemsCount > index && (ctxMenu.Items[index] is Separator || (ctxMenu.Items[index] is AcrylicMenuItem ami1 && ami1.Tag == _menuItem))) { var ctrl = (Control)ctxMenu.Items[index]!; if (ctrl.Visibility != Visibility.Collapsed) @@ -640,7 +640,7 @@ private void ShowUpdateIfAvailable() return item; } - private bool HasSubItems(UserInterfaceRepositoryAction repositoryAction) + private static bool HasSubItems(UserInterfaceRepositoryAction repositoryAction) { if (repositoryAction is DeferredSubActionsUserInterfaceRepositoryAction) { diff --git a/tests/RepoM.ActionMenu.Core.Tests/Abstractions/EnvironmentsCacheDecoratorTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Abstractions/EnvironmentsCacheDecoratorTests.cs new file mode 100644 index 00000000..d7d791b9 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/Abstractions/EnvironmentsCacheDecoratorTests.cs @@ -0,0 +1,97 @@ +namespace RepoM.ActionMenu.Core.Tests.Abstractions; + +using System; +using System.Collections.Generic; +using FakeItEasy; +using FluentAssertions; +using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces; +using RepoM.ActionMenu.Core.Abstractions; +using Xunit; +using IEnvironment = RepoM.ActionMenu.Core.Abstractions.IEnvironment; + +public class EnvironmentsCacheDecoratorTests +{ + private readonly IEnvironment _decoratee = A.Fake(); + private readonly EnvironmentsCacheDecorator _sut; + + public EnvironmentsCacheDecoratorTests() + { + _sut = new EnvironmentsCacheDecorator(_decoratee); + } + + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new EnvironmentsCacheDecorator(null!); + + // assert + act1.Should().Throw(); + } + + [Fact] + public void Ctor_ShouldNotCallDecoratee() + { + // arrange + + // act + _ = new EnvironmentsCacheDecorator(_decoratee); + + // assert + A.CallTo(_decoratee).MustNotHaveHappened(); + } + + [Fact] + public void GetEnvironmentVariables_ShouldForwardToDecoratee_WhenFirstCall() + { + // arrange + + // act + _ = _sut.GetEnvironmentVariables(); + + // assert + A.CallTo(() => _decoratee.GetEnvironmentVariables()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void GetEnvironmentVariables_ShouldReturnDecorateeResult_WhenFirstCall() + { + // arrange + var envVars = new Dictionary + { + { "dummy", Guid.NewGuid().ToString() }, + }; + + A.CallTo(() => _decoratee.GetEnvironmentVariables()).Returns(envVars); + + // act + var result = _sut.GetEnvironmentVariables(); + + // assert + result.Should().BeSameAs(envVars); + } + + [Fact] + public void GetEnvironmentVariables_ShouldReturnCachedValue_WhenCalledForSecondTime() + { + // arrange + var envVars = new Dictionary + { + { "dummy", Guid.NewGuid().ToString() }, + }; + + A.CallTo(() => _decoratee.GetEnvironmentVariables()).Returns(envVars); + Dictionary result1 = _sut.GetEnvironmentVariables(); + + Fake.ClearRecordedCalls(_decoratee); + + // act + Dictionary result2 = _sut.GetEnvironmentVariables(); + + // assert + result2.Should().BeSameAs(result1); + A.CallTo(_decoratee).MustNotHaveHappened(); + } +} \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/Abstractions/OperatingSystemTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Abstractions/OperatingSystemTests.cs new file mode 100644 index 00000000..9ca076ed --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/Abstractions/OperatingSystemTests.cs @@ -0,0 +1,52 @@ +namespace RepoM.ActionMenu.Core.Tests.Abstractions; + +using System; +using System.IO.Abstractions; +using FakeItEasy; +using FluentAssertions; +using RepoM.ActionMenu.Core.Abstractions; +using Xunit; +using OperatingSystem = RepoM.ActionMenu.Core.Abstractions.OperatingSystem; + +public class OperatingSystemTests +{ + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + System.Func act1 = () => new OperatingSystem(A.Dummy(), null!); + System.Func act2 = () => new OperatingSystem(null!, A.Dummy()); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + } + + [Fact] + public void Ctor_ShouldExposeFileSystemUsingProperty() + { + // arrange + var fs = A.Dummy(); + + // act + var sut = new OperatingSystem(fs, A.Dummy()); + + // assert + sut.FileSystem.Should().BeSameAs(fs); + } + + [Fact] + public void Ctor_ShouldExposeEnvironmentUsingProperty() + { + // arrange + var env = A.Dummy(); + + // act + var sut = new OperatingSystem(A.Dummy(), env); + + // assert + sut.Environment.Should().BeSameAs(env); + } +} \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/Abstractions/SystemEnvironmentsTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Abstractions/SystemEnvironmentsTests.cs new file mode 100644 index 00000000..c5277e55 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/Abstractions/SystemEnvironmentsTests.cs @@ -0,0 +1,22 @@ +namespace RepoM.ActionMenu.Core.Tests.Abstractions; + +using System.Collections.Generic; +using FluentAssertions; +using RepoM.ActionMenu.Core.Abstractions; +using Xunit; + +public class SystemEnvironmentsTests +{ + [Fact] + public void GetEnvironmentVariables_ShouldReturnEnvironmentVariables() + { + // arrange + SystemEnvironments sut = SystemEnvironments.Instance; + + // act + Dictionary result = sut.GetEnvironmentVariables(); + + // assert + result.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/Context/EnvScriptObjectTests.cs b/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/Context/EnvScriptObjectTests.cs new file mode 100644 index 00000000..e5b82a90 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/Context/EnvScriptObjectTests.cs @@ -0,0 +1,195 @@ +namespace RepoM.ActionMenu.Core.Tests.ActionMenu.Context; + +using System; +using System.Collections.Generic; +using FakeItEasy; +using FluentAssertions; +using RepoM.ActionMenu.Core.ActionMenu.Context; +using Scriban.Parsing; +using Scriban; +using Scriban.Runtime; +using Xunit; + +public class EnvScriptObjectTests +{ + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new EnvScriptObject(null!); + + // assert + act1.Should().Throw(); + } + + [Fact] + public void Count_ShouldReturn0_WhenDictionaryIsEmpty() + { + // arrange + var sut = new EnvScriptObject(new Dictionary()); + + // act + var result = sut.Count; + + // assert + result.Should().Be(0); + } + + [Fact] + public void Count_ShouldReturnDictionaryCount() + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "dummy1", "dummy1" }, + { "dummy2", "dummy2" }, + }); + + // act + var result = sut.Count; + + // assert + result.Should().Be(2); + } + + [Fact] + public void GetMembers_ShouldReturnMembers() + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + IEnumerable result = sut.GetMembers(); + + // assert + result.Should().BeEquivalentTo("member_dummy1", "member_dummy2"); + } + + [Fact] + public void Contains_ShouldReturnMemberExistence() + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + var result1 = sut.Contains("member1"); + var result2 = sut.Contains("member_dummy1"); + + // assert + result1.Should().BeFalse(); + result2.Should().BeTrue(); + } + + [Fact] + public void TryGetValue_ShouldReturnValue_WhenExists() + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + var result1 = sut.TryGetValue(A.Dummy(), A.Dummy(), "member1", out var value1); + var result2 = sut.TryGetValue(A.Dummy(), A.Dummy(), "member_dummy1", out var value2); + + // assert + result1.Should().BeFalse(); + value1.Should().BeNull(); + result2.Should().BeTrue(); + value2.Should().Be("dummy1"); + } + + [Fact] + public void CanWrite_ShouldReturnFalse() + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + var result1 = sut.CanWrite("member1"); + var result2 = sut.CanWrite("member_dummy1"); + + // assert + result1.Should().BeFalse(); + result2.Should().BeFalse(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TrySetValue_ShouldReturnFalseAndNotUpdateValues(bool readOnly) + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + var result1 = sut.TrySetValue(A.Dummy(), A.Dummy(), "member1", "value", readOnly); + var result2 = sut.TrySetValue(A.Dummy(), A.Dummy(), "member_dummy1", "value2", readOnly); + + // assert + result1.Should().BeFalse(); + result2.Should().BeFalse(); + } + + + [Fact] + public void Remove_ShouldNeverRemove() + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + var result1 = sut.Remove("member1"); + var result2 = sut.Remove("member_dummy1"); + + // assert + result1.Should().BeFalse(); + result2.Should().BeFalse(); + sut.GetMembers().Should().BeEquivalentTo("member_dummy1", "member_dummy2"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Clone_ShouldReturnCloned(bool deepClone) + { + // arrange + var sut = new EnvScriptObject(new Dictionary + { + { "member_dummy1", "dummy1" }, + { "member_dummy2", "dummy2" }, + }); + + // act + IScriptObject clone = ((IScriptObject)sut).Clone(deepClone); + + // assert + sut.GetMembers().Should().BeEquivalentTo("member_dummy1", "member_dummy2"); + clone.GetMembers().Should().BeEquivalentTo("member_dummy1", "member_dummy2"); + clone.Should().NotBeSameAs(sut); + } +} \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/IntegrationTests/Docs/folder@1-scenario01.testfile.yaml b/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/IntegrationTests/Docs/folder@1-scenario01.testfile.yaml index 2c4b80b6..17746b90 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/IntegrationTests/Docs/folder@1-scenario01.testfile.yaml +++ b/tests/RepoM.ActionMenu.Core.Tests/ActionMenu/IntegrationTests/Docs/folder@1-scenario01.testfile.yaml @@ -7,6 +7,7 @@ action-menu: - type: folder@1 name: My folder active: true + is-deferred: false actions: - type: url@1 name: 'Browse to remote {{ repository.remotes[0].key }}' diff --git a/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs index 683d12d6..c9d7142f 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs +++ b/tests/RepoM.ActionMenu.Core.Tests/Model/DisposableContextScriptObjectTests.cs @@ -226,7 +226,13 @@ public void Dispose_ShouldPopAllPushedEnvironmentVariables() private ActionMenuGenerationContext CreateContext() { - var context = new ActionMenuGenerationContext(_templateParser, _fileSystem, _environment, _functionsArray, _mapper, _deserializer, _mappers); + var context = new ActionMenuGenerationContext( + _templateParser, + new Core.Abstractions.OperatingSystem(_fileSystem, _environment), + _functionsArray, + _mapper, + _deserializer, + _mappers); context.Initialize(_repository); return context; } diff --git a/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs b/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs new file mode 100644 index 00000000..0fca07bc --- /dev/null +++ b/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs @@ -0,0 +1,29 @@ +namespace RepoM.App.Tests.Services; + +using System; +using System.IO.Abstractions; +using FakeItEasy; +using FluentAssertions; +using RepoM.ActionMenu.Core; +using RepoM.App.Services; +using RepoM.Core.Plugin.Common; +using Xunit; + +public class UserMenuActionMenuFactoryTests +{ + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new UserMenuActionMenuFactory(A.Dummy(), A.Dummy(), null!); + Func act2 = () => new UserMenuActionMenuFactory(A.Dummy(), null!, A.Dummy()); + Func act3 = () => new UserMenuActionMenuFactory(null!, A.Dummy(), A.Dummy()); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + act3.Should().Throw(); + } +} \ No newline at end of file From d23435f74c71601b46af045289c014e53716af6f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 11 Oct 2024 18:26:25 +0000 Subject: [PATCH 08/12] Docs changes --- docs/repom.generated.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/repom.generated.md b/docs/repom.generated.md index 81443a09..81ad6cc4 100644 --- a/docs/repom.generated.md +++ b/docs/repom.generated.md @@ -118,6 +118,7 @@ Properties: - `name`: Name of the menu item. ([Text](repository_action_types.md#text)) - `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate)) - `context`: The context in which the action is available. ([Context](repository_action_types.md#context)) +- `is-deferred`: Whether the folder is deferred. ([Predicate](repository_action_types.md#predicate)) ### Example @@ -129,6 +130,7 @@ action-menu: - type: folder@1 name: My folder active: true + is-deferred: false actions: - type: url@1 name: 'Browse to remote {{ repository.remotes[0].key }}' @@ -138,7 +140,7 @@ action-menu: name: 'wiki' url: '{{ repository.remotes[0].url }}/wiki' ``` -snippet source | anchor +snippet source | anchor From de494c1e4b3e142183c32c5fe3830649a305b327 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Fri, 11 Oct 2024 20:53:51 +0200 Subject: [PATCH 09/12] add tests --- .../UserMenuActionMenuFactoryTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs b/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs index 0fca07bc..c2fa4fee 100644 --- a/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs +++ b/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs @@ -1,16 +1,33 @@ namespace RepoM.App.Tests.Services; using System; +using System.Collections.Generic; using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; using RepoM.ActionMenu.Core; +using RepoM.ActionMenu.Interface.UserInterface; using RepoM.App.Services; using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.Repository; +using SimpleInjector.Diagnostics; using Xunit; public class UserMenuActionMenuFactoryTests { + private IUserInterfaceActionMenuFactory _factory; + private IAppDataPathProvider _appDataPathProvider; + private IFileSystem _fileSystem; + + public UserMenuActionMenuFactoryTests() + { + _factory = A.Fake(); + _appDataPathProvider = A.Fake(); + _fileSystem = A.Fake(); + } + [Fact] public void Ctor_ShouldThrow_WhenArgumentNull() { @@ -26,4 +43,21 @@ public void Ctor_ShouldThrow_WhenArgumentNull() act2.Should().Throw(); act3.Should().Throw(); } + + [Fact] + public async Task CreateMenuAsync_ShouldReturnErrorMenu_WhenFileNotExists() + { + // arrange + A.CallTo(() => _fileSystem.File.Exists(A._)).Returns(false); + A.CallTo(() => _appDataPathProvider.AppDataPath).Returns("C:\\AppData"); + + var sut = new UserMenuActionMenuFactory(_fileSystem, _appDataPathProvider, _factory); + + // act + List result = await sut.CreateMenuAsync(A.Dummy()).ToListAsync(); + + // assert + result.Should().HaveCount(3); + A.CallTo(_factory).MustNotHaveHappened(); + } } \ No newline at end of file From c872a5b8b830f344c1f04c5845cf9163efbf34a7 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Fri, 11 Oct 2024 21:32:53 +0200 Subject: [PATCH 10/12] add extra test --- .../Model/ActionMenuGenerationContextTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs diff --git a/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs new file mode 100644 index 00000000..4b505a23 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs @@ -0,0 +1,38 @@ +namespace RepoM.ActionMenu.Core.Tests.Model; + +using FakeItEasy; +using FluentAssertions; +using System.IO.Abstractions; +using System; +using RepoM.ActionMenu.Core.Abstractions; +using RepoM.ActionMenu.Core.Misc; +using Xunit; +using RepoM.ActionMenu.Core.Model; + +public class ActionMenuGenerationContextTests +{ + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + var os = new Core.Abstractions.OperatingSystem(A.Dummy(), A.Dummy()); + var tp = A.Dummy(); + var amd = A.Dummy(); + + // act + Func act1 = () => new ActionMenuGenerationContext(tp, os, [], [], amd, null!); + Func act2 = () => new ActionMenuGenerationContext(tp, os, [], [], null!, []); + Func act3 = () => new ActionMenuGenerationContext(tp, os, [], null!, amd, []); + Func act4 = () => new ActionMenuGenerationContext(tp, os, null!, [], amd, []); + Func act5 = () => new ActionMenuGenerationContext(tp, null!, [], [], amd, []); + Func act6 = () => new ActionMenuGenerationContext(null!, os, [], [], amd, []); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + act3.Should().Throw(); + act4.Should().Throw(); + act5.Should().Throw(); + act6.Should().Throw(); + } +} \ No newline at end of file From 49cbbe083a2d2fc37ba10f25208f61898f9b6cca Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Fri, 11 Oct 2024 21:37:49 +0200 Subject: [PATCH 11/12] test --- .../Model/ActionMenuGenerationContextTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs b/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs index 4b505a23..1e64349c 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs +++ b/tests/RepoM.ActionMenu.Core.Tests/Model/ActionMenuGenerationContextTests.cs @@ -4,6 +4,8 @@ namespace RepoM.ActionMenu.Core.Tests.Model; using FluentAssertions; using System.IO.Abstractions; using System; +using System.Linq; +using System.Threading.Tasks; using RepoM.ActionMenu.Core.Abstractions; using RepoM.ActionMenu.Core.Misc; using Xunit; @@ -35,4 +37,23 @@ public void Ctor_ShouldThrow_WhenArgumentNull() act5.Should().Throw(); act6.Should().Throw(); } + + [Fact] + public async Task AddActionMenusAsync_ShouldDoNothing_WhenArgumentIsNull() + { + // arrange + var sut = new ActionMenuGenerationContext( + A.Dummy(), + A.Dummy(), + [], + [], + A.Dummy(), + []); + + // act + var result = await sut.AddActionMenusAsync(null!).ToListAsync(); + + // assert + result.Should().BeEmpty(); + } } \ No newline at end of file From 60c3eb3c251d4ae79fecffe6ec93411d988a3f59 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Fri, 11 Oct 2024 22:22:02 +0200 Subject: [PATCH 12/12] Add test --- .../UserMenuActionMenuFactoryTests.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs b/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs index c2fa4fee..e6f4d939 100644 --- a/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs +++ b/tests/RepoM.App.Tests/Services/UserMenuActionMenuFactoryTests.cs @@ -12,14 +12,13 @@ namespace RepoM.App.Tests.Services; using RepoM.App.Services; using RepoM.Core.Plugin.Common; using RepoM.Core.Plugin.Repository; -using SimpleInjector.Diagnostics; using Xunit; public class UserMenuActionMenuFactoryTests { - private IUserInterfaceActionMenuFactory _factory; - private IAppDataPathProvider _appDataPathProvider; - private IFileSystem _fileSystem; + private readonly IUserInterfaceActionMenuFactory _factory; + private readonly IAppDataPathProvider _appDataPathProvider; + private readonly IFileSystem _fileSystem; public UserMenuActionMenuFactoryTests() { @@ -60,4 +59,27 @@ public async Task CreateMenuAsync_ShouldReturnErrorMenu_WhenFileNotExists() result.Should().HaveCount(3); A.CallTo(_factory).MustNotHaveHappened(); } + + [Fact] + public async Task CreateMenuAsync_ShouldReturnFactoryResult_WhenFileExists() + { + // arrange + var repository = A.Dummy(); + var actions = new List() + { + new UserInterfaceRepositoryAction("test", repository), + }; + A.CallTo(() => _fileSystem.File.Exists(A._)).Returns(true); + A.CallTo(() => _appDataPathProvider.AppDataPath).Returns("C:\\AppData\\"); + A.CallTo(() => _factory.CreateMenuAsync(repository, System.IO.Path.Combine("C:\\AppData\\", "RepositoryActionsV2.yaml"))).Returns(actions.ToAsyncEnumerable()); + + var sut = new UserMenuActionMenuFactory(_fileSystem, _appDataPathProvider, _factory); + + // act + List result = await sut.CreateMenuAsync(repository).ToListAsync(); + + // assert + result.Should().BeEquivalentTo(actions); + A.CallTo(() => _factory.CreateMenuAsync(repository, System.IO.Path.Combine("C:\\AppData\\", "RepositoryActionsV2.yaml"))).MustHaveHappenedOnceExactly(); + } } \ No newline at end of file