diff --git a/.azuredevops/Pipelines/build.yaml b/.azuredevops/Pipelines/build.yaml
index 8a56f038..05d13267 100644
--- a/.azuredevops/Pipelines/build.yaml
+++ b/.azuredevops/Pipelines/build.yaml
@@ -46,9 +46,9 @@ stages:
displayName: Update git submodules
- task: UseDotNet@2
- displayName: "Use dotnet sdk 8.0.x"
+ displayName: "Use dotnet sdk 8.0.100"
inputs:
- version: 8.0.x
+ version: 8.0.100
includePreviewVersions: false
- script: dotnet --info
diff --git a/.azuredevops/Pipelines/pull-request.yaml b/.azuredevops/Pipelines/pull-request.yaml
index ea4da240..32c41ad4 100644
--- a/.azuredevops/Pipelines/pull-request.yaml
+++ b/.azuredevops/Pipelines/pull-request.yaml
@@ -43,9 +43,9 @@ stages:
includePreviewVersions: false
- task: UseDotNet@2
- displayName: "Use dotnet sdk 8.0.x"
+ displayName: "Use dotnet sdk 8.0.100"
inputs:
- version: 8.0.x
+ version: 8.0.100
includePreviewVersions: false
- script: dotnet --info
@@ -58,7 +58,7 @@ stages:
checkLatest: true
- pwsh: |
- dotnet tool install --global --version 25.1.0 MarkdownSnippets.Tool
+ dotnet tool install --global --version 25.1.0 MarkdownSnippets.Tool
mdsnippets
$changedLines = git status --porcelain=v1
diff --git a/Directory.Build.props b/Directory.Build.props
index eaad5b05..4233c121 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,5 +10,7 @@
3.6.133all
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a043cd57..bc7395c9 100644
--- a/README.md
+++ b/README.md
@@ -14,16 +14,9 @@ It's populating itself as you work with git. It does not get in the way and only
RepoM will not compete with your favourite git clients, so keep them. It's not about working within a repository: It's a new way to use all of your repositories to make your daily work easier.
-📦 [Check the Releases page](https://github.com/coenm/RepoM/releases) to **download** the latest version and see **what's new**!
-
-## Credits
-
-RepoM is a fork of [RepoZ](https://github.com/awaescher/RepoZ), which was created by [Andreas Wäscher](https://github.com/awaescher).
-RepoZ contains functionality that has been stripped in RepoM like supporting MacOS, releasing versions using chocolatey, the commandline sidekick (`grr``), and performing actions at multiple repostitories at once.
-
## The Hub
-The hub provides a quick overview of your repositories including their current branch and a short status information. Additionally, it offers some shortcuts like revealing a repository in the Windows Explorer, opening a command line tool in a given repository and checking out git branches.
+The hub provides a quick overview of your repositories including their current branch, a short status information, and optionally some provided tags. Additionally, it offers some shortcuts like revealing a repository in the Windows Explorer, opening a command line tool in a given repository, checking out git branches and lots of other predefined or customizable actions.

@@ -38,10 +31,18 @@ For Windows, use the hotkeys Ctrl+Alt+R to show
+
+## Configuration
+
+
+
+
## Context Menu
The main functionality of RepoM are the quick actions to execute per repository. For instance, you can quickly naviate to the repository by directly opening the windows explorer or by opening a command prompt. This context menu is user and repostirory specific and can be defined using yaml. This way, you can add an context menu item (action) for opening the repository in Visual Studio (for a C# project) and for an other repository you can add the action to open a repository in Eclipse.
+To read more about the context menu, click here.
+
These actions are defined in the `RepositoryActions.yaml` located in your `%APPDATA%\RepoM\` folder. More information can be found [here](docs/RepositoryActions.md).
## Tagging
@@ -77,3 +78,7 @@ RepoM uses plugins to extend functionality. At this moment, when a plugin is ava
- [WebBrowser](docs/RepoM.Plugin.WebBrowser.md)
- [WindowsExplorerGitInfo](docs/RepoM.Plugin.WindowsExplorerGitInfo.md)
+## Credits
+
+RepoM is a fork of [RepoZ](https://github.com/awaescher/RepoZ), which was created by [Andreas Wäscher](https://github.com/awaescher).
+RepoZ contains functionality that has been stripped in RepoM like supporting MacOS, releasing versions using chocolatey, the commandline sidekick (`grr``), and performing actions at multiple repostitories at once.
diff --git a/README.source.md b/README.source.md
index 5a040b6d..bc7395c9 100644
--- a/README.source.md
+++ b/README.source.md
@@ -14,16 +14,9 @@ It's populating itself as you work with git. It does not get in the way and only
RepoM will not compete with your favourite git clients, so keep them. It's not about working within a repository: It's a new way to use all of your repositories to make your daily work easier.
-📦 [Check the Releases page](https://github.com/coenm/RepoM/releases) to **download** the latest version and see **what's new**!
-
-## Credits
-
-RepoM is a fork of [RepoZ](https://github.com/awaescher/RepoZ), which was created by [Andreas Wäscher](https://github.com/awaescher).
-RepoZ contains functionality that has been stripped in RepoM like supporting MacOS, releasing versions using chocolatey, the commandline sidekick (`grr``), and performing actions at multiple repostitories at once.
-
## The Hub
-The hub provides a quick overview of your repositories including their current branch and a short status information. Additionally, it offers some shortcuts like revealing a repository in the Windows Explorer, opening a command line tool in a given repository and checking out git branches.
+The hub provides a quick overview of your repositories including their current branch, a short status information, and optionally some provided tags. Additionally, it offers some shortcuts like revealing a repository in the Windows Explorer, opening a command line tool in a given repository, checking out git branches and lots of other predefined or customizable actions.

@@ -38,10 +31,18 @@ For Windows, use the hotkeys Ctrl+Alt+R to show
+
+## Configuration
+
+
+
+
## Context Menu
The main functionality of RepoM are the quick actions to execute per repository. For instance, you can quickly naviate to the repository by directly opening the windows explorer or by opening a command prompt. This context menu is user and repostirory specific and can be defined using yaml. This way, you can add an context menu item (action) for opening the repository in Visual Studio (for a C# project) and for an other repository you can add the action to open a repository in Eclipse.
+To read more about the context menu, click here.
+
These actions are defined in the `RepositoryActions.yaml` located in your `%APPDATA%\RepoM\` folder. More information can be found [here](docs/RepositoryActions.md).
## Tagging
@@ -76,4 +77,8 @@ RepoM uses plugins to extend functionality. At this moment, when a plugin is ava
- [Statistics](docs/RepoM.Plugin.Statistics.md)
- [WebBrowser](docs/RepoM.Plugin.WebBrowser.md)
- [WindowsExplorerGitInfo](docs/RepoM.Plugin.WindowsExplorerGitInfo.md)
-
\ No newline at end of file
+
+## Credits
+
+RepoM is a fork of [RepoZ](https://github.com/awaescher/RepoZ), which was created by [Andreas Wäscher](https://github.com/awaescher).
+RepoZ contains functionality that has been stripped in RepoM like supporting MacOS, releasing versions using chocolatey, the commandline sidekick (`grr``), and performing actions at multiple repostitories at once.
diff --git a/RepoM.sln b/RepoM.sln
index 87aa8f73..b40e5596 100644
--- a/RepoM.sln
+++ b/RepoM.sln
@@ -55,6 +55,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Plugin.WebBrowser", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Plugin.WebBrowser.Tests", "tests\RepoM.Plugin.WebBrowser.Tests\RepoM.Plugin.WebBrowser.Tests.csproj", "{E976587E-F48E-4647-A307-91EFEC8F571C}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Core", "src\RepoM.ActionMenu.Core\RepoM.ActionMenu.Core.csproj", "{2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Interface", "src\RepoM.ActionMenu.Interface\RepoM.ActionMenu.Interface.csproj", "{2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Core.Tests", "tests\RepoM.ActionMenu.Core.Tests\RepoM.ActionMenu.Core.Tests.csproj", "{4E22232B-1C6E-473A-AC82-F151C09D90CB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ActionMenu", "ActionMenu", "{D44E8C7C-76D4-4677-AB2C-4E4F32E93413}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.CodeGen", "src\RepoM.ActionMenu.CodeGen\RepoM.ActionMenu.CodeGen.csproj", "{F493CFD2-1352-4D17-9A93-2B18D31F6F2C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.CodeGen.Tests", "tests\RepoM.ActionMenu.CodeGen.Tests\RepoM.ActionMenu.CodeGen.Tests.csproj", "{0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Core.TestLib", "tests\RepoM.ActionMenu.Core.TestLib\RepoM.ActionMenu.Core.TestLib.csproj", "{A803579D-E713-468B-9EBB-A014E473FFCD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoM.ActionMenu.CodeGenDummyLibrary", "tests\RepoM.ActionMenu.CodeGenDummyLibrary\RepoM.ActionMenu.CodeGenDummyLibrary.csproj", "{998B5CA0-158D-4B01-A343-07ED534D02B0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -569,6 +585,146 @@ Global
{E976587E-F48E-4647-A307-91EFEC8F571C}.Release|x64.Build.0 = Release|Any CPU
{E976587E-F48E-4647-A307-91EFEC8F571C}.Release|x86.ActiveCfg = Release|Any CPU
{E976587E-F48E-4647-A307-91EFEC8F571C}.Release|x86.Build.0 = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM.Build.0 = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x64.Build.0 = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x86.Build.0 = Debug|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM.ActiveCfg = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM.Build.0 = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM64.Build.0 = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x64.ActiveCfg = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x64.Build.0 = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x86.ActiveCfg = Release|Any CPU
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x86.Build.0 = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM.Build.0 = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x64.Build.0 = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x86.Build.0 = Debug|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM.ActiveCfg = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM.Build.0 = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM64.Build.0 = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x64.ActiveCfg = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x64.Build.0 = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x86.ActiveCfg = Release|Any CPU
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x86.Build.0 = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM.Build.0 = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x64.Build.0 = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x86.Build.0 = Debug|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM.ActiveCfg = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM.Build.0 = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM64.Build.0 = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x64.ActiveCfg = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x64.Build.0 = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x86.ActiveCfg = Release|Any CPU
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x86.Build.0 = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM.Build.0 = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x64.Build.0 = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x86.Build.0 = Debug|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM.ActiveCfg = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM.Build.0 = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM64.Build.0 = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x64.ActiveCfg = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x64.Build.0 = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x86.ActiveCfg = Release|Any CPU
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x86.Build.0 = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|ARM.Build.0 = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|x64.Build.0 = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Debug|x86.Build.0 = Debug|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|ARM.ActiveCfg = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|ARM.Build.0 = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|ARM64.Build.0 = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|x64.ActiveCfg = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|x64.Build.0 = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|x86.ActiveCfg = Release|Any CPU
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9}.Release|x86.Build.0 = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|ARM.Build.0 = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|x64.Build.0 = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Debug|x86.Build.0 = Debug|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|ARM.ActiveCfg = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|ARM.Build.0 = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|ARM64.Build.0 = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|x64.ActiveCfg = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|x64.Build.0 = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|x86.ActiveCfg = Release|Any CPU
+ {A803579D-E713-468B-9EBB-A014E473FFCD}.Release|x86.Build.0 = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|ARM.Build.0 = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|x64.Build.0 = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Debug|x86.Build.0 = Debug|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|ARM.ActiveCfg = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|ARM.Build.0 = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|ARM64.Build.0 = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|x64.ActiveCfg = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|x64.Build.0 = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|x86.ActiveCfg = Release|Any CPU
+ {998B5CA0-158D-4B01-A343-07ED534D02B0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -593,6 +749,13 @@ Global
{A2264CB6-39DE-4EA2-8C1D-BA28115351E7} = {D6E372DC-10D3-4997-9DFC-568B4666635A}
{23D796D1-1902-4357-9C95-4FB120A24A6E} = {D6E372DC-10D3-4997-9DFC-568B4666635A}
{E976587E-F48E-4647-A307-91EFEC8F571C} = {D6E372DC-10D3-4997-9DFC-568B4666635A}
+ {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
+ {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
+ {4E22232B-1C6E-473A-AC82-F151C09D90CB} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
+ {F493CFD2-1352-4D17-9A93-2B18D31F6F2C} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
+ {0C3FF659-8C0E-49D8-9A87-1F93ADE665F9} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
+ {A803579D-E713-468B-9EBB-A014E473FFCD} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
+ {998B5CA0-158D-4B01-A343-07ED534D02B0} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1765ABAA-0652-4DA5-ABBF-05396F2957D7}
diff --git a/RepoM.sln.DotSettings b/RepoM.sln.DotSettings
index cbce9ef1..24b37438 100644
--- a/RepoM.sln.DotSettings
+++ b/RepoM.sln.DotSettings
@@ -9,5 +9,6 @@
TrueTrueTrue
+ TrueTrueTrue
\ No newline at end of file
diff --git a/docs/mdsource/ActionList.source.md b/docs/mdsource/ActionList.source.md.old
similarity index 100%
rename from docs/mdsource/ActionList.source.md
rename to docs/mdsource/ActionList.source.md.old
diff --git a/docs/mdsource/RepoM.Plugin.AzureDevOps.source.md b/docs/mdsource/RepoM.Plugin.AzureDevOps.source.md.old
similarity index 100%
rename from docs/mdsource/RepoM.Plugin.AzureDevOps.source.md
rename to docs/mdsource/RepoM.Plugin.AzureDevOps.source.md.old
diff --git a/docs/mdsource/RepoM.Plugin.Clipboard.source.md b/docs/mdsource/RepoM.Plugin.Clipboard.source.md.old
similarity index 100%
rename from docs/mdsource/RepoM.Plugin.Clipboard.source.md
rename to docs/mdsource/RepoM.Plugin.Clipboard.source.md.old
diff --git a/docs/mdsource/RepoM.Plugin.Heidi.source.md b/docs/mdsource/RepoM.Plugin.Heidi.source.md.old
similarity index 100%
rename from docs/mdsource/RepoM.Plugin.Heidi.source.md
rename to docs/mdsource/RepoM.Plugin.Heidi.source.md.old
diff --git a/docs/mdsource/RepoM.Plugin.SonarCloud.source.md b/docs/mdsource/RepoM.Plugin.SonarCloud.source.md.old
similarity index 100%
rename from docs/mdsource/RepoM.Plugin.SonarCloud.source.md
rename to docs/mdsource/RepoM.Plugin.SonarCloud.source.md.old
diff --git a/docs/mdsource/RepoM.Plugin.WebBrowser.source.md b/docs/mdsource/RepoM.Plugin.WebBrowser.source.md.old
similarity index 100%
rename from docs/mdsource/RepoM.Plugin.WebBrowser.source.md
rename to docs/mdsource/RepoM.Plugin.WebBrowser.source.md.old
diff --git a/docs/mdsource/RepositoryActions.source.md b/docs/mdsource/RepositoryActions.source.md.old
similarity index 100%
rename from docs/mdsource/RepositoryActions.source.md
rename to docs/mdsource/RepositoryActions.source.md.old
diff --git a/docs/snippets/file.find_files.verified.yaml b/docs/snippets/file.find_files.verified.yaml
new file mode 100644
index 00000000..222d6a00
--- /dev/null
+++ b/docs/snippets/file.find_files.verified.yaml
@@ -0,0 +1,2 @@
+- C:\Project\My Repositories\my-solution.sln
+- C:\Project\My Repositories\src\test solution.sln
diff --git a/docs_new/mdsource/plugin_repom.plugin.azuredevops.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.azuredevops.generated.source.md
new file mode 100644
index 00000000..7e3578ef
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.azuredevops.generated.source.md
@@ -0,0 +1,40 @@
+# AzureDevOps
+
+The AzureDevops module enables integration with one azure devops environment. The integration currently focuses on Pull Requests.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.AzureDevOps
+- PluginName: AzureDevOps
+- PluginDescription: Integration with Azure Devops providing fetching and creating pull requests.
+- PluginMarkdownDescription: The AzureDevops module enables integration with one azure devops environment. The integration currently focuses on Pull Requests.
+
+This module contains the following methods, variables and/or constants:
+
+## azure-devops-create-pr@1
+
+Action menu item to create a pull request in Azure Devops.
+
+Properties:
+
+- `name`: Menu item title. ([Text](repository_action_types.md#text))
+- `project-id`: The azure devops project id. ([Text](repository_action_types.md#text))
+- `pr-title`: Pull Request title. When not provided, the title will be defined based on the branch name.
+Title will be the last part of the branchname split on `/`, so `feature/123-testBranch` will result in title `123-testBranch` ([Text](repository_action_types.md#text))
+- `to-branch`: Name of the branch the pull request should be merged into. For instance `develop`, or `main`. ([Text](repository_action_types.md#text))
+- `reviewer-ids`: List of reviewer ids. The id should be a valid Azure DevOps user id (i.e. GUID). (List)
+- `draft-pr`: Boolean specifying if th PR should be marked as draft. ([Predicate](repository_action_types.md#predicate))
+- `include-work-items`: Boolean specifying if workitems should be included in the PR. RepoM will try to resolve the workitems by looping through the commit messages. ([Predicate](repository_action_types.md#predicate))
+- `open-in-browser`: Boolean specifying if the Pull request should be opened in the browser after creation. ([Predicate](repository_action_types.md#predicate))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+- `auto-complete`: Auto complete options. Please take a look at the same for more information (AutoCompleteOptionsV1, optional)
+
+### Example
+
+snippet: azure-devops-create-pr@1-scenario01
+
+snippet: azure-devops-create-pr@1-scenario02
+
+snippet: azure-devops-create-pr@1-scenario03
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.clipboard.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.clipboard.generated.source.md
new file mode 100644
index 00000000..a1b5a67c
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.clipboard.generated.source.md
@@ -0,0 +1,30 @@
+# Clipboard
+
+This module provides a repository actions to copy specific (evaluated) text to the clipboard using the action provider type `clipboard-copy`.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.Clipboard
+- PluginName: Clipboard
+- PluginDescription: Provides a 'copy to clipboard' action.
+- PluginMarkdownDescription: This module provides a repository actions to copy specific (evaluated) text to the clipboard using the action provider type `clipboard-copy`.
+
+This module contains the following methods, variables and/or constants:
+
+## clipboard-copy@1
+
+This action makes it possible to copy text to the clipboard.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `text`: The text to copy to the clipboard. ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+
+### Example
+
+snippet: clipboard-copy@1-scenario01
+
+snippet: clipboard-copy@1-scenario02
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.everythingfilesearch.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.everythingfilesearch.generated.source.md
new file mode 100644
index 00000000..c67a6659
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.everythingfilesearch.generated.source.md
@@ -0,0 +1,11 @@
+# Everything
+
+This module integrates with VoidTool Everything in order to locate git repositories on your system. Using Everything cache, this process will be much faster then locating git repositories the default way.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.EverythingFileSearch
+- PluginName: Everything
+- PluginDescription: Uses VoidTool Everything as file search provider.
+- PluginMarkdownDescription: This module integrates with VoidTool Everything in order to locate git repositories on your system. Using Everything cache, this process will be much faster then locating git repositories the default way.
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.heidi.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.heidi.generated.source.md
new file mode 100644
index 00000000..48a6cda1
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.heidi.generated.source.md
@@ -0,0 +1,11 @@
+# HeidiSQL
+
+This module integrates with a portable [HeidiSQL](https://www.heidisql.com/) installation. The portable Heidi DB saves its database configuration in a portable configuration file. This module monitors this file and makes it possible to use this configuration in the action menu.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.Heidi
+- PluginName: HeidiSQL
+- PluginDescription: Contains context variables to be used in the action menu. The variables are extracted from the portable Heidi DB configuration.
+- PluginMarkdownDescription: This module integrates with a portable [HeidiSQL](https://www.heidisql.com/) installation. The portable Heidi DB saves its database configuration in a portable configuration file. This module monitors this file and makes it possible to use this configuration in the action menu.
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.lucenequeryparser.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.lucenequeryparser.generated.source.md
new file mode 100644
index 00000000..9d7a0f50
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.lucenequeryparser.generated.source.md
@@ -0,0 +1,11 @@
+# LuceneQueryParser
+
+Contains a custom query parser based on Lucene syntax for repository filtering.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.LuceneQueryParser
+- PluginName: LuceneQueryParser
+- PluginDescription: Contains a custom query parser based on Lucene syntax for repository filtering.
+- PluginMarkdownDescription: \
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.sonarcloud.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.sonarcloud.generated.source.md
new file mode 100644
index 00000000..70c3136d
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.sonarcloud.generated.source.md
@@ -0,0 +1,28 @@
+# SonarCloud
+
+This module integrates with SonarCloud. Currently, the only functionality is to star a given repository in SonarCloud using the repository action.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.SonarCloud
+- PluginName: SonarCloud
+- PluginDescription: Providing a repository action to mark a repository as favorite in SonarCloud
+- PluginMarkdownDescription: This module integrates with SonarCloud. Currently, the only functionality is to star a given repository in SonarCloud using the repository action.
+
+This module contains the following methods, variables and/or constants:
+
+## sonarcloud-set-favorite@1
+
+Action to mark a repository as favorite within SonarCloud.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `project`: The SonarCloud project key. ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+
+### Example
+
+snippet: sonarcloud-set-favorite@1-scenario01
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.statistics.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.statistics.generated.source.md
new file mode 100644
index 00000000..807b2c48
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.statistics.generated.source.md
@@ -0,0 +1,11 @@
+# Statistics
+
+Provides functionality to keep track how may times an action is performed on a given repository. These numbers can be accessed using variable providers. The plugin also contains functionality to use these statistics in orderings.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.Statistics
+- PluginName: Statistics
+- PluginDescription: Provides functionality to keep track how may times an action is performed on a given repository. These numbers can be accessed using variable providers. The plugin also contains functionality to use these statistics in orderings.
+- PluginMarkdownDescription: \
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.webbrowser.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.webbrowser.generated.source.md
new file mode 100644
index 00000000..6f72aeb0
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.webbrowser.generated.source.md
@@ -0,0 +1,29 @@
+# WebBrowser
+
+Provides functionality to start a web browser from an action with profile information.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.WebBrowser
+- PluginName: WebBrowser
+- PluginDescription: Provides functionality to start a web browser from an action with profile information.
+- PluginMarkdownDescription: \
+
+This module contains the following methods, variables and/or constants:
+
+## browser@1
+
+Action opening a webbrowser with the provided url.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `url`: The url to browse to. ([Text](repository_action_types.md#text))
+- `profile`: profile name used to select browser and browser profile ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+
+### Example
+
+snippet: webbrowser-browser@1-scenario01
+
diff --git a/docs_new/mdsource/plugin_repom.plugin.windowsexplorergitinfo.generated.source.md b/docs_new/mdsource/plugin_repom.plugin.windowsexplorergitinfo.generated.source.md
new file mode 100644
index 00000000..e96d440f
--- /dev/null
+++ b/docs_new/mdsource/plugin_repom.plugin.windowsexplorergitinfo.generated.source.md
@@ -0,0 +1,11 @@
+# WindowsExplorerTitle
+
+As an extra goodie for Windows users, RepoM automatically detects open File Explorer windows and adds a status appendix to their title if they are in context of a git repository.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.WindowsExplorerGitInfo
+- PluginName: WindowsExplorerTitle
+- PluginDescription: Contains a hook updating Explorer views in Windows with the current git status.
+- PluginMarkdownDescription: As an extra goodie for Windows users, RepoM automatically detects open File Explorer windows and adds a status appendix to their title if they are in context of a git repository.
+
diff --git a/docs_new/mdsource/repom.generated.source.md b/docs_new/mdsource/repom.generated.source.md
new file mode 100644
index 00000000..ad0cc65b
--- /dev/null
+++ b/docs_new/mdsource/repom.generated.source.md
@@ -0,0 +1,151 @@
+# RepoM Core Repository Actions
+
+This module contains the following methods, variables and/or constants:
+
+## browse-repository@1
+
+Action to open the default webbrowser and go to the origin remote webinterface. When multiple remotes are available a sub menu is created for each remote.
+
+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))
+- `first-only`: Single menu for the first remote. ([Predicate](repository_action_types.md#predicate))
+
+## command@1
+
+Action to excute a command (related to the repository)
+
+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))
+- `command`: The command to execute. ([Text](repository_action_types.md#text))
+- `arguments`: Arguments for the command. ([Text](repository_action_types.md#text))
+
+## executable@1
+
+Action to excute an application with additional arguments. This action is almost identical to the `command@1` action. When no existing executables are provided, the action will not show.
+
+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))
+- `executable`: The executable. ([Text](repository_action_types.md#text))
+- `arguments`: Arguments for the executable. ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+
+## folder@1
+
+Action to create a folder (sub menu) in the context menu of the repository allowing you to order actions.
+
+Properties:
+
+- `actions`: List of actions. (ActionMenu, optional)
+- `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))
+
+## foreach@1
+
+Action to create repeated actions based on a variable.
+
+Properties:
+
+- `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))
+- `iteration-context`: Additional context added for each iteration. ([Context](repository_action_types.md#context))
+- `enumerable`: The list of items to enumerate on. (Variable)
+- `variable`: The name of the variable to access to current enumeration of the items. For each iteration, the variable `{var.name}` has the value of the current iteration. (string?, optional)
+- `skip`: Predicate to skip the current item. ([Predicate](repository_action_types.md#predicate))
+- `actions`: List of repeated actions. (List)
+
+## git-checkout@1
+
+This action will create a menu and sub menus with all local and remote branches for an easy checkout.
+
+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))
+
+## git-fetch@1
+
+Action to execute a `git fetch` command.
+
+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))
+
+## git-pull@1
+
+Action to execute a `git pull` command.
+
+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))
+
+## git-push@1
+
+Action to execute a `git push` command.
+
+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))
+
+## ignore-repository@1
+
+Action to ignore the current repository. This repository will be added to the list of ignored repositories and will never show in RepoM.
+To undo this action, clear all ignored repositories or manually edit the ignored repositories file (when RepoM is not running).
+
+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))
+
+## just-text@1
+
+Textual action to display some text in the action menu.
+
+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))
+- `enabled`: Show the menu as enabled (clickable) or disabled. ([Predicate](repository_action_types.md#predicate))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+
+## pin-repository@1
+
+Action to pin (or unpin) the current repository. Pinning is not persistant and all pinned repositories will be cleared when RepoM exits.
+Pinning a repository allowed custom filtering, ordering and searching.
+
+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))
+- `mode`: The pin mode `[Toggle, Pin, UnPin]`. (Nullable, optional)
+
+## separator@1
+
+Creates a visual separator in the action menu.
+
+Properties:
+
+- `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))
+
+## url@1
+
+Action to open the url in the default browser.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `url`: The URL to browse to. ([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))
diff --git a/docs_new/mdsource/repository_action_types.md b/docs_new/mdsource/repository_action_types.md
new file mode 100644
index 00000000..7bb57e96
--- /dev/null
+++ b/docs_new/mdsource/repository_action_types.md
@@ -0,0 +1,103 @@
+# Repository Action types
+
+## Context
+
+The context type enables you to create or load variables and add some custom scriban methods in order to render [Text](#text) or calculate [Predicats](#predicate).
+Currenlty, RepoM supports the following types of context actions which can be added as item to the context.
+
+- `evaluate-variable@1` A scriban template which, when evaluated, returns a value to be stored as variable.
+- `evaluate-script@1` A scriban template which, when evaluated adds 'content' like variables or methods, to the current scriban context.
+- `load-file@1` Loads a file (only `.env` or `.yaml`) and processes the content. An environment file is read and all environment variables are stored and accessable.
+- `render-variable@1` Renders a scriban template and stores the outcomming text as variable.
+- `set-variable@1` Sets a variable with static content.
+
+## Text
+
+Text is a scriban template for rendering textual content. These scriban templates can contain mixed content of text and scriban code blocks which are enclosed by `{{` and `}}`.
+
+### Examples
+
+For this example, the current repository branch name is `feature/abcdefghi` and the date is the 21st of december in 2023.
+
+| Input | Output (string) |
+|---|---|
+| `static text` | static text |
+| `static {{ 1 + 2 }} text` | static 3 text |
+| `today is {{ date.now \| date.to_string "%Y-%m-%d" }}` | today is 2023-12-21 |
+| `this is {{ 42 < 11 && true }}!` | this is false! |
+| `Create PR ({{ repository.branch \| string.replace "feature/" "" \| string.truncate 4 ".." }})` | Create PR abcd.. |
+
+### Internals
+
+Text uses the following scriban lexer and parser options:
+
+
+
+```cs
+public static readonly ParserOptions DefaultParserOptions = new()
+{
+ ExpressionDepthLimit = 100,
+ LiquidFunctionsToScriban = false,
+ ParseFloatAsDecimal = default,
+};
+```
+snippet source | anchor
+
+
+
+
+```cs
+public static readonly LexerOptions MixedLexer = new()
+{
+ FrontMatterMarker = LexerOptions.DefaultFrontMatterMarker,
+ Lang = ScriptLang.Default,
+ Mode = ScriptMode.Default,
+};
+```
+snippet source | anchor
+
+
+## Predicate
+
+A predicate is a scriban expression resulting in a boolean. Beause it is an expression, RepoM uses the pure scripting mode of scriban (`ScriptOnly` lexer mode) without templating (`{{` and `}}`). In other words, a Predicate is not about rendering text but evaluating a boolean expression.
+
+### Examples
+
+| Input | Output (boolean) |
+|---|---|
+| `true` | true |
+| `false` | false |
+| `1` | true |
+| `0` | false |
+| `1 == 2` | false |
+| `a = [1, 2, 3]; a.size > 10` | false |
+| `file.file_exists(repository.path + "\readme.md")` | true or false depending if file exists |
+
+### Internals
+
+Predicate uses the following scriban lexer and parser options:
+
+
+
+```cs
+public static readonly ParserOptions DefaultParserOptions = new()
+{
+ ExpressionDepthLimit = 100,
+ LiquidFunctionsToScriban = false,
+ ParseFloatAsDecimal = default,
+};
+```
+snippet source | anchor
+
+
+
+
+```cs
+public static readonly LexerOptions ScriptOnlyLexer = new()
+{
+ Lang = ScriptLang.Default,
+ Mode = ScriptMode.ScriptOnly,
+};
+```
+snippet source | anchor
+
diff --git a/docs_new/mdsource/repository_action_types.source.md b/docs_new/mdsource/repository_action_types.source.md
new file mode 100644
index 00000000..978f2832
--- /dev/null
+++ b/docs_new/mdsource/repository_action_types.source.md
@@ -0,0 +1,60 @@
+# Repository Action types
+
+## Context
+
+The context type enables you to create or load variables and add some custom scriban methods in order to render [Text](#text) or calculate [Predicats](#predicate).
+Currenlty, RepoM supports the following types of context actions which can be added as item to the context.
+
+- `evaluate-variable@1` A scriban template which, when evaluated, returns a value to be stored as variable.
+- `evaluate-script@1` A scriban template which, when evaluated adds 'content' like variables or methods, to the current scriban context.
+- `load-file@1` Loads a file (only `.env` or `.yaml`) and processes the content. An environment file is read and all environment variables are stored and accessable.
+- `render-variable@1` Renders a scriban template and stores the outcomming text as variable.
+- `set-variable@1` Sets a variable with static content.
+
+## Text
+
+Text is a scriban template for rendering textual content. These scriban templates can contain mixed content of text and scriban code blocks which are enclosed by `{{` and `}}`.
+
+### Examples
+
+For this example, the current repository branch name is `feature/abcdefghi` and the date is the 21st of december in 2023.
+
+| Input | Output (string) |
+|---|---|
+| `static text` | static text |
+| `static {{ 1 + 2 }} text` | static 3 text |
+| `today is {{ date.now \| date.to_string "%Y-%m-%d" }}` | today is 2023-12-21 |
+| `this is {{ 42 < 11 && true }}!` | this is false! |
+| `Create PR ({{ repository.branch \| string.replace "feature/" "" \| string.truncate 4 ".." }})` | Create PR abcd.. |
+
+### Internals
+
+Text uses the following scriban lexer and parser options:
+
+snippet: DefaultLexerAndParserOptions_DefaultParserOptions
+
+snippet: DefaultLexerAndParserOptions_MixedLexer
+
+## Predicate
+
+A predicate is a scriban expression resulting in a boolean. Beause it is an expression, RepoM uses the pure scripting mode of scriban (`ScriptOnly` lexer mode) without templating (`{{` and `}}`). In other words, a Predicate is not about rendering text but evaluating a boolean expression.
+
+### Examples
+
+| Input | Output (boolean) |
+|---|---|
+| `true` | true |
+| `false` | false |
+| `1` | true |
+| `0` | false |
+| `1 == 2` | false |
+| `a = [1, 2, 3]; a.size > 10` | false |
+| `file.file_exists(repository.path + "\readme.md")` | true or false depending if file exists |
+
+### Internals
+
+Predicate uses the following scriban lexer and parser options:
+
+snippet: DefaultLexerAndParserOptions_DefaultParserOptions
+
+snippet: DefaultLexerAndParserOptions_ScriptOnlyLexer
diff --git a/docs_new/mdsource/script_variables_azure_devops.generated.source.md b/docs_new/mdsource/script_variables_azure_devops.generated.source.md
new file mode 100644
index 00000000..6a552843
--- /dev/null
+++ b/docs_new/mdsource/script_variables_azure_devops.generated.source.md
@@ -0,0 +1,44 @@
+# `azure_devops`
+
+Provides Azure Devops functions through `azure_devops`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`azure_devops.get_pull_requests`](#get_pull_requests)
+
+## get_pull_requests
+
+`azure_devops.get_pull_requests(projectId)`
+
+Get pull requests for the given project. The result is an enumeration of PullRequest.
+
+Argument:
+
+- `projectId`: The azure devops project id. Cannot be null or empty.
+
+### Returns
+
+Returns an enumeration of pull requests for the selected repository (or an empty enumeration when no pull requests are found).
+
+### Example
+
+#### Usage
+
+Get all pull requests for the selected repository in a given devops project:
+
+
+```
+devops_project_id = "805ACF64-0F06-47EC-96BF-E830895E2740";
+prs = azure_devops.get_pull_requests(devops_project_id);
+```
+
+#### Result
+
+As a result, the variable `prs` could contain two pull requests with the following dummy data:
+
+snippet: azure_devops.get_pull_requests
+
+#### RepositoryAction sample
+
+snippet: azure-devops-get-pull-requests@actionmenu01
+
diff --git a/docs_new/mdsource/script_variables_file.generated.source.md b/docs_new/mdsource/script_variables_file.generated.source.md
new file mode 100644
index 00000000..e00de116
--- /dev/null
+++ b/docs_new/mdsource/script_variables_file.generated.source.md
@@ -0,0 +1,112 @@
+# `file`
+
+Provides file related action menu functions and variables accessable through `file`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`file.dir_exists`](#dir_exists)
+- [`file.file_exists`](#file_exists)
+- [`file.find_files`](#find_files)
+
+## dir_exists
+
+`file.dir_exists(path)`
+
+Checks if the specified directory path exists on the disk.
+
+Argument:
+
+- `path`: Absolute path to a directory.
+
+### Returns
+
+`true` if the specified directory path exists on the disk, `false` otherwise.
+
+### Example
+
+#### Usage
+
+Check if directory exists
+
+
+```
+exists = file.dir_exists('C:\Project\');
+exists = file.dir_exists('C:\Project');
+exists = file.dir_exists('C:/Project/');
+```
+
+#### RepositoryAction sample
+
+snippet: dir_exists@actionmenu01
+
+
+## file_exists
+
+`file.file_exists(path)`
+
+Checks if the specified file path exists on the disk.
+
+Argument:
+
+- `path`: Absolute path to a file.
+
+### Returns
+
+`true` if the specified file path exists on the disk, `false` otherwise.
+
+### Example
+
+#### Usage
+
+Check if file exists
+
+
+```
+exists = file.file_exists('C:\Project\my-solution.sln');
+```
+
+#### RepositoryAction sample
+
+snippet: file_exists@actionmenu01
+
+
+## find_files
+
+`file.find_files(rootPath,searchPattern)`
+
+Find files in a given directory based on the search pattern. Resulting filenames are absolute path based.
+
+Arguments:
+
+- `rootPath`: The root folder.
+- `searchPattern`: The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions.
+
+### Returns
+
+Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern.
+
+### Example
+
+#### Usage
+
+Locate all solution files in the given directory.
+
+
+```
+solution_files = file.find_files('C:\Project\', '*.sln');
+```
+
+#### Result
+
+As a result, the variable `solution_files` is an enumerable of strings, for example:
+
+
+```yaml
+- C:\Project\My Repositories\my-solution.sln
+- C:\Project\My Repositories\src\test solution.sln
+```
+
+#### RepositoryAction sample
+
+snippet: find_files@actionmenu01
+
diff --git a/docs_new/mdsource/script_variables_heidi.generated.source.md b/docs_new/mdsource/script_variables_heidi.generated.source.md
new file mode 100644
index 00000000..15fab21a
--- /dev/null
+++ b/docs_new/mdsource/script_variables_heidi.generated.source.md
@@ -0,0 +1,37 @@
+# `heidi`
+
+Provides variables provided by the Heidi module. The variables are accessable through `heidi`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`heidi.databases`](#databases)
+
+## databases
+
+`heidi.databases`
+
+Gets all known databases configured in the Heidi configuration related to the selected repository.
+
+### Returns
+
+An enumerable of database configuration objects as shown in the example below.
+
+### Example
+
+Get all database configurations for the current repository:
+
+
+```
+databases = heidi.databases;
+```
+
+#### Result
+
+As a result, the variable `databases` could contain the following dummy database configuration:
+
+snippet: heidi.databases@actionmenu01
+
+#### RepositoryAction sample
+
+snippet: heidi.databases@actionmenu02
+
diff --git a/docs_new/mdsource/script_variables_repository.generated.source.md b/docs_new/mdsource/script_variables_repository.generated.source.md
new file mode 100644
index 00000000..166f3eb4
--- /dev/null
+++ b/docs_new/mdsource/script_variables_repository.generated.source.md
@@ -0,0 +1,115 @@
+# `repository`
+
+Provides action menu functions and variables for the current repository through `repository`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`repository.branch`](#branch)
+- [`repository.branches`](#branches)
+- [`repository.linux_path`](#linux_path)
+- [`repository.local_branches`](#local_branches)
+- [`repository.location`](#location)
+- [`repository.name`](#name)
+- [`repository.path`](#path)
+- [`repository.remotes`](#remotes)
+- [`repository.windows_path`](#windows_path)
+
+## branch
+
+`repository.branch`
+
+Gets the current branch of the repository
+
+### Returns
+
+The name of the current branch.
+
+## branches
+
+`repository.branches`
+
+Gets the current branch of the repository
+
+### Returns
+
+The name of the current branch.
+
+## linux_path
+
+`repository.linux_path`
+
+Gets the path of the repository in linux style (i.e. use `\`). The path does NOT end with a backslash.
+
+### Returns
+
+The backslash based path of the repository without the last backslash.
+
+## local_branches
+
+`repository.local_branches`
+
+Gets the local branches
+
+### Returns
+
+All local branches.
+
+## location
+
+`repository.location`
+
+Gets the Location of the repository.
+
+### Returns
+
+The path of the repository.
+
+## name
+
+`repository.name`
+
+Gets the name of the repository.
+
+### Returns
+
+The name of the repository.
+
+### Example
+
+#### Usage
+
+
+```
+repository.name
+```
+
+
+## path
+
+`repository.path`
+
+Gets the path of the repository. The path is windows or linux based (depending on the running OS) and does NOT end with a (back)slash.
+
+### Returns
+
+The repository path.
+
+## remotes
+
+`repository.remotes`
+
+Gets the remotes.
+
+### Returns
+
+Remotes.
+
+## windows_path
+
+`repository.windows_path`
+
+Gets the path of the repository in windows style (i.e. use `/`). The path does NOT end with a slash.
+
+### Returns
+
+The path of the repository.
diff --git a/docs_new/mdsource/script_variables_sonarcloud.generated.source.md b/docs_new/mdsource/script_variables_sonarcloud.generated.source.md
new file mode 100644
index 00000000..6c18ff13
--- /dev/null
+++ b/docs_new/mdsource/script_variables_sonarcloud.generated.source.md
@@ -0,0 +1,42 @@
+# `sonarcloud`
+
+Provides a sonar cloud method providing the favorite status of the current repository.
+
+This module contains the following methods, variables and/or constants:
+
+- [`sonarcloud.is_favorite`](#is_favorite)
+
+## is_favorite
+
+`sonarcloud.is_favorite(id)`
+
+Get favorite status of repository related to the id.
+
+Argument:
+
+- `id`: The sonarcloud id related to the repository.
+
+### Returns
+
+`true` when the repository is set as favorite in SonarCloud, `false`, otherwise.
+
+### Example
+
+#### Usage
+
+Gets SonarClouds favorite status of the repository:
+
+
+```
+sonarcloud_repository_id = "RepoM";
+is_favorite = sonarcloud.is_favorite(sonarcloud_repository_id);
+```
+
+#### Result
+
+As a result, the boolean variable `is_favorite` is set.
+
+#### RepositoryAction sample
+
+snippet: sonarcloud-is_favorite@actionmenu01
+
diff --git a/docs_new/mdsource/script_variables_statistics.generated.source.md b/docs_new/mdsource/script_variables_statistics.generated.source.md
new file mode 100644
index 00000000..d2fa9c4f
--- /dev/null
+++ b/docs_new/mdsource/script_variables_statistics.generated.source.md
@@ -0,0 +1,48 @@
+# `statistics`
+
+Provides statistical information accessible through `statistics`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`statistics.count`](#count)
+- [`statistics.overall_count`](#overall_count)
+
+## count
+
+`statistics.count`
+
+Gets the number of actions performed on the current repository.
+
+### Returns
+
+Number of actions performed on the current repository.
+
+### Example
+
+#### Usage
+
+
+```
+repo_call_count = statistics.count;
+```
+
+
+## overall_count
+
+`statistics.overall_count`
+
+Gets the number of actions performed on all repositories known in RepoM.
+
+### Returns
+
+Number of actions performed on any known repository.
+
+### Example
+
+#### Usage
+
+
+```
+repo_call_count = statistics.overall_count;
+```
+
diff --git a/docs_new/plugin_repom.plugin.azuredevops.generated.md b/docs_new/plugin_repom.plugin.azuredevops.generated.md
new file mode 100644
index 00000000..5192725a
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.azuredevops.generated.md
@@ -0,0 +1,83 @@
+# AzureDevOps
+
+The AzureDevops module enables integration with one azure devops environment. The integration currently focuses on Pull Requests.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.AzureDevOps
+- PluginName: AzureDevOps
+- PluginDescription: Integration with Azure Devops providing fetching and creating pull requests.
+- PluginMarkdownDescription: The AzureDevops module enables integration with one azure devops environment. The integration currently focuses on Pull Requests.
+
+This module contains the following methods, variables and/or constants:
+
+## azure-devops-create-pr@1
+
+Action menu item to create a pull request in Azure Devops.
+
+Properties:
+
+- `name`: Menu item title. ([Text](repository_action_types.md#text))
+- `project-id`: The azure devops project id. ([Text](repository_action_types.md#text))
+- `pr-title`: Pull Request title. When not provided, the title will be defined based on the branch name.
+Title will be the last part of the branchname split on `/`, so `feature/123-testBranch` will result in title `123-testBranch` ([Text](repository_action_types.md#text))
+- `to-branch`: Name of the branch the pull request should be merged into. For instance `develop`, or `main`. ([Text](repository_action_types.md#text))
+- `reviewer-ids`: List of reviewer ids. The id should be a valid Azure DevOps user id (i.e. GUID). (List)
+- `draft-pr`: Boolean specifying if th PR should be marked as draft. ([Predicate](repository_action_types.md#predicate))
+- `include-work-items`: Boolean specifying if workitems should be included in the PR. RepoM will try to resolve the workitems by looping through the commit messages. ([Predicate](repository_action_types.md#predicate))
+- `open-in-browser`: Boolean specifying if the Pull request should be opened in the browser after creation. ([Predicate](repository_action_types.md#predicate))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+- `auto-complete`: Auto complete options. Please take a look at the same for more information (AutoCompleteOptionsV1, optional)
+
+### Example
+
+
+
+```yaml
+- type: azure-devops-create-pr@1
+ project-id: "{{ project_id }}"
+ name: Create feature to develop ({{ repository.branch | string.replace "feature/" "" | string.strip | string.truncate 20 ".." }})
+ pr-title: 'Release {{ now }}'
+ to-branch: develop
+ reviewer-ids:
+ - "{{ devops_guid_reviewer_1 }}"
+ - "33333333-F973-4BE7-B39A-A9F85B18C75E"
+ draft-pr: false
+ include-work-items: true
+ open-in-browser: true
+ auto-complete:
+ merge-strategy: Squash
+ delete-source-branch: true
+ transition-work-items: true
+ active: 'repository.branch | string.starts_with "feature/"'
+```
+snippet source | anchor
+
+
+
+
+```yaml
+- type: azure-devops-create-pr@1
+ project-id: "{{ project_id }}"
+ name: Complete feature
+ pr-title: 'Feature {{ repository.branch | string.replace "feature/" "" }}'
+ to-branch: develop
+ reviewer-ids:
+ - "{{ devops_guid_reviewer_1 }}"
+ draft-pr: repository.banch == "develop"
+ active: true
+```
+snippet source | anchor
+
+
+
+
+```yaml
+- type: azure-devops-create-pr@1
+ project-id: "{{ project_id }}"
+ to-branch: develop
+```
+snippet source | anchor
+
+
diff --git a/docs_new/plugin_repom.plugin.clipboard.generated.md b/docs_new/plugin_repom.plugin.clipboard.generated.md
new file mode 100644
index 00000000..a8317675
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.clipboard.generated.md
@@ -0,0 +1,47 @@
+# Clipboard
+
+This module provides a repository actions to copy specific (evaluated) text to the clipboard using the action provider type `clipboard-copy`.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.Clipboard
+- PluginName: Clipboard
+- PluginDescription: Provides a 'copy to clipboard' action.
+- PluginMarkdownDescription: This module provides a repository actions to copy specific (evaluated) text to the clipboard using the action provider type `clipboard-copy`.
+
+This module contains the following methods, variables and/or constants:
+
+## clipboard-copy@1
+
+This action makes it possible to copy text to the clipboard.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `text`: The text to copy to the clipboard. ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+
+### Example
+
+
+
+```yaml
+- type: clipboard-copy@1
+ name: Copy static text when feature branch
+ text: 'static text'
+ active: 'repository.branch | string.starts_with "feature/"'
+```
+snippet source | anchor
+
+
+
+
+```yaml
+- type: clipboard-copy@1
+ name: Copy git checkout command to clipboard
+ text: 'git checkout -b branch {{ repository.branch }}'
+```
+snippet source | anchor
+
+
diff --git a/docs_new/plugin_repom.plugin.everythingfilesearch.generated.md b/docs_new/plugin_repom.plugin.everythingfilesearch.generated.md
new file mode 100644
index 00000000..c67a6659
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.everythingfilesearch.generated.md
@@ -0,0 +1,11 @@
+# Everything
+
+This module integrates with VoidTool Everything in order to locate git repositories on your system. Using Everything cache, this process will be much faster then locating git repositories the default way.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.EverythingFileSearch
+- PluginName: Everything
+- PluginDescription: Uses VoidTool Everything as file search provider.
+- PluginMarkdownDescription: This module integrates with VoidTool Everything in order to locate git repositories on your system. Using Everything cache, this process will be much faster then locating git repositories the default way.
+
diff --git a/docs_new/plugin_repom.plugin.heidi.generated.md b/docs_new/plugin_repom.plugin.heidi.generated.md
new file mode 100644
index 00000000..48a6cda1
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.heidi.generated.md
@@ -0,0 +1,11 @@
+# HeidiSQL
+
+This module integrates with a portable [HeidiSQL](https://www.heidisql.com/) installation. The portable Heidi DB saves its database configuration in a portable configuration file. This module monitors this file and makes it possible to use this configuration in the action menu.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.Heidi
+- PluginName: HeidiSQL
+- PluginDescription: Contains context variables to be used in the action menu. The variables are extracted from the portable Heidi DB configuration.
+- PluginMarkdownDescription: This module integrates with a portable [HeidiSQL](https://www.heidisql.com/) installation. The portable Heidi DB saves its database configuration in a portable configuration file. This module monitors this file and makes it possible to use this configuration in the action menu.
+
diff --git a/docs_new/plugin_repom.plugin.lucenequeryparser.generated.md b/docs_new/plugin_repom.plugin.lucenequeryparser.generated.md
new file mode 100644
index 00000000..9d7a0f50
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.lucenequeryparser.generated.md
@@ -0,0 +1,11 @@
+# LuceneQueryParser
+
+Contains a custom query parser based on Lucene syntax for repository filtering.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.LuceneQueryParser
+- PluginName: LuceneQueryParser
+- PluginDescription: Contains a custom query parser based on Lucene syntax for repository filtering.
+- PluginMarkdownDescription: \
+
diff --git a/docs_new/plugin_repom.plugin.sonarcloud.generated.md b/docs_new/plugin_repom.plugin.sonarcloud.generated.md
new file mode 100644
index 00000000..05662ce6
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.sonarcloud.generated.md
@@ -0,0 +1,37 @@
+# SonarCloud
+
+This module integrates with SonarCloud. Currently, the only functionality is to star a given repository in SonarCloud using the repository action.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.SonarCloud
+- PluginName: SonarCloud
+- PluginDescription: Providing a repository action to mark a repository as favorite in SonarCloud
+- PluginMarkdownDescription: This module integrates with SonarCloud. Currently, the only functionality is to star a given repository in SonarCloud using the repository action.
+
+This module contains the following methods, variables and/or constants:
+
+## sonarcloud-set-favorite@1
+
+Action to mark a repository as favorite within SonarCloud.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `project`: The SonarCloud project key. ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+
+### Example
+
+
+
+```yaml
+- type: sonarcloud-set-favorite@1
+ name: Star repository on SonarCloud
+ project: "{{ my_sonarcloud_repository_id }}"
+ active: "!sonarcloud.is_favorite(my_sonarcloud_repository_id)"
+```
+snippet source | anchor
+
+
diff --git a/docs_new/plugin_repom.plugin.statistics.generated.md b/docs_new/plugin_repom.plugin.statistics.generated.md
new file mode 100644
index 00000000..807b2c48
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.statistics.generated.md
@@ -0,0 +1,11 @@
+# Statistics
+
+Provides functionality to keep track how may times an action is performed on a given repository. These numbers can be accessed using variable providers. The plugin also contains functionality to use these statistics in orderings.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.Statistics
+- PluginName: Statistics
+- PluginDescription: Provides functionality to keep track how may times an action is performed on a given repository. These numbers can be accessed using variable providers. The plugin also contains functionality to use these statistics in orderings.
+- PluginMarkdownDescription: \
+
diff --git a/docs_new/plugin_repom.plugin.webbrowser.generated.md b/docs_new/plugin_repom.plugin.webbrowser.generated.md
new file mode 100644
index 00000000..0da84f58
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.webbrowser.generated.md
@@ -0,0 +1,39 @@
+# WebBrowser
+
+Provides functionality to start a web browser from an action with profile information.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.WebBrowser
+- PluginName: WebBrowser
+- PluginDescription: Provides functionality to start a web browser from an action with profile information.
+- PluginMarkdownDescription: \
+
+This module contains the following methods, variables and/or constants:
+
+## browser@1
+
+Action opening a webbrowser with the provided url.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `url`: The url to browse to. ([Text](repository_action_types.md#text))
+- `profile`: profile name used to select browser and browser profile ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+- `active`: Whether the menu item is enabled. ([Predicate](repository_action_types.md#predicate))
+
+### Example
+
+
+
+```yaml
+- type: browser@1
+ name: My Github
+ url: https://github.com/coenm
+ profile: '{{ my_profile }}'
+ active: true
+```
+snippet source | anchor
+
+
diff --git a/docs_new/plugin_repom.plugin.windowsexplorergitinfo.generated.md b/docs_new/plugin_repom.plugin.windowsexplorergitinfo.generated.md
new file mode 100644
index 00000000..e96d440f
--- /dev/null
+++ b/docs_new/plugin_repom.plugin.windowsexplorergitinfo.generated.md
@@ -0,0 +1,11 @@
+# WindowsExplorerTitle
+
+As an extra goodie for Windows users, RepoM automatically detects open File Explorer windows and adds a status appendix to their title if they are in context of a git repository.
+
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: RepoM.Plugin.WindowsExplorerGitInfo
+- PluginName: WindowsExplorerTitle
+- PluginDescription: Contains a hook updating Explorer views in Windows with the current git status.
+- PluginMarkdownDescription: As an extra goodie for Windows users, RepoM automatically detects open File Explorer windows and adds a status appendix to their title if they are in context of a git repository.
+
diff --git a/docs_new/repom.generated.md b/docs_new/repom.generated.md
new file mode 100644
index 00000000..ad0cc65b
--- /dev/null
+++ b/docs_new/repom.generated.md
@@ -0,0 +1,151 @@
+# RepoM Core Repository Actions
+
+This module contains the following methods, variables and/or constants:
+
+## browse-repository@1
+
+Action to open the default webbrowser and go to the origin remote webinterface. When multiple remotes are available a sub menu is created for each remote.
+
+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))
+- `first-only`: Single menu for the first remote. ([Predicate](repository_action_types.md#predicate))
+
+## command@1
+
+Action to excute a command (related to the repository)
+
+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))
+- `command`: The command to execute. ([Text](repository_action_types.md#text))
+- `arguments`: Arguments for the command. ([Text](repository_action_types.md#text))
+
+## executable@1
+
+Action to excute an application with additional arguments. This action is almost identical to the `command@1` action. When no existing executables are provided, the action will not show.
+
+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))
+- `executable`: The executable. ([Text](repository_action_types.md#text))
+- `arguments`: Arguments for the executable. ([Text](repository_action_types.md#text))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+
+## folder@1
+
+Action to create a folder (sub menu) in the context menu of the repository allowing you to order actions.
+
+Properties:
+
+- `actions`: List of actions. (ActionMenu, optional)
+- `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))
+
+## foreach@1
+
+Action to create repeated actions based on a variable.
+
+Properties:
+
+- `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))
+- `iteration-context`: Additional context added for each iteration. ([Context](repository_action_types.md#context))
+- `enumerable`: The list of items to enumerate on. (Variable)
+- `variable`: The name of the variable to access to current enumeration of the items. For each iteration, the variable `{var.name}` has the value of the current iteration. (string?, optional)
+- `skip`: Predicate to skip the current item. ([Predicate](repository_action_types.md#predicate))
+- `actions`: List of repeated actions. (List)
+
+## git-checkout@1
+
+This action will create a menu and sub menus with all local and remote branches for an easy checkout.
+
+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))
+
+## git-fetch@1
+
+Action to execute a `git fetch` command.
+
+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))
+
+## git-pull@1
+
+Action to execute a `git pull` command.
+
+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))
+
+## git-push@1
+
+Action to execute a `git push` command.
+
+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))
+
+## ignore-repository@1
+
+Action to ignore the current repository. This repository will be added to the list of ignored repositories and will never show in RepoM.
+To undo this action, clear all ignored repositories or manually edit the ignored repositories file (when RepoM is not running).
+
+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))
+
+## just-text@1
+
+Textual action to display some text in the action menu.
+
+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))
+- `enabled`: Show the menu as enabled (clickable) or disabled. ([Predicate](repository_action_types.md#predicate))
+- `context`: The context in which the action is available. ([Context](repository_action_types.md#context))
+
+## pin-repository@1
+
+Action to pin (or unpin) the current repository. Pinning is not persistant and all pinned repositories will be cleared when RepoM exits.
+Pinning a repository allowed custom filtering, ordering and searching.
+
+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))
+- `mode`: The pin mode `[Toggle, Pin, UnPin]`. (Nullable, optional)
+
+## separator@1
+
+Creates a visual separator in the action menu.
+
+Properties:
+
+- `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))
+
+## url@1
+
+Action to open the url in the default browser.
+
+Properties:
+
+- `name`: Name of the menu item. ([Text](repository_action_types.md#text))
+- `url`: The URL to browse to. ([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))
diff --git a/docs_new/repository_action_types.md b/docs_new/repository_action_types.md
new file mode 100644
index 00000000..7bb57e96
--- /dev/null
+++ b/docs_new/repository_action_types.md
@@ -0,0 +1,103 @@
+# Repository Action types
+
+## Context
+
+The context type enables you to create or load variables and add some custom scriban methods in order to render [Text](#text) or calculate [Predicats](#predicate).
+Currenlty, RepoM supports the following types of context actions which can be added as item to the context.
+
+- `evaluate-variable@1` A scriban template which, when evaluated, returns a value to be stored as variable.
+- `evaluate-script@1` A scriban template which, when evaluated adds 'content' like variables or methods, to the current scriban context.
+- `load-file@1` Loads a file (only `.env` or `.yaml`) and processes the content. An environment file is read and all environment variables are stored and accessable.
+- `render-variable@1` Renders a scriban template and stores the outcomming text as variable.
+- `set-variable@1` Sets a variable with static content.
+
+## Text
+
+Text is a scriban template for rendering textual content. These scriban templates can contain mixed content of text and scriban code blocks which are enclosed by `{{` and `}}`.
+
+### Examples
+
+For this example, the current repository branch name is `feature/abcdefghi` and the date is the 21st of december in 2023.
+
+| Input | Output (string) |
+|---|---|
+| `static text` | static text |
+| `static {{ 1 + 2 }} text` | static 3 text |
+| `today is {{ date.now \| date.to_string "%Y-%m-%d" }}` | today is 2023-12-21 |
+| `this is {{ 42 < 11 && true }}!` | this is false! |
+| `Create PR ({{ repository.branch \| string.replace "feature/" "" \| string.truncate 4 ".." }})` | Create PR abcd.. |
+
+### Internals
+
+Text uses the following scriban lexer and parser options:
+
+
+
+```cs
+public static readonly ParserOptions DefaultParserOptions = new()
+{
+ ExpressionDepthLimit = 100,
+ LiquidFunctionsToScriban = false,
+ ParseFloatAsDecimal = default,
+};
+```
+snippet source | anchor
+
+
+
+
+```cs
+public static readonly LexerOptions MixedLexer = new()
+{
+ FrontMatterMarker = LexerOptions.DefaultFrontMatterMarker,
+ Lang = ScriptLang.Default,
+ Mode = ScriptMode.Default,
+};
+```
+snippet source | anchor
+
+
+## Predicate
+
+A predicate is a scriban expression resulting in a boolean. Beause it is an expression, RepoM uses the pure scripting mode of scriban (`ScriptOnly` lexer mode) without templating (`{{` and `}}`). In other words, a Predicate is not about rendering text but evaluating a boolean expression.
+
+### Examples
+
+| Input | Output (boolean) |
+|---|---|
+| `true` | true |
+| `false` | false |
+| `1` | true |
+| `0` | false |
+| `1 == 2` | false |
+| `a = [1, 2, 3]; a.size > 10` | false |
+| `file.file_exists(repository.path + "\readme.md")` | true or false depending if file exists |
+
+### Internals
+
+Predicate uses the following scriban lexer and parser options:
+
+
+
+```cs
+public static readonly ParserOptions DefaultParserOptions = new()
+{
+ ExpressionDepthLimit = 100,
+ LiquidFunctionsToScriban = false,
+ ParseFloatAsDecimal = default,
+};
+```
+snippet source | anchor
+
+
+
+
+```cs
+public static readonly LexerOptions ScriptOnlyLexer = new()
+{
+ Lang = ScriptLang.Default,
+ Mode = ScriptMode.ScriptOnly,
+};
+```
+snippet source | anchor
+
diff --git a/docs_new/script_variables_azure_devops.generated.md b/docs_new/script_variables_azure_devops.generated.md
new file mode 100644
index 00000000..1cdb849f
--- /dev/null
+++ b/docs_new/script_variables_azure_devops.generated.md
@@ -0,0 +1,75 @@
+# `azure_devops`
+
+Provides Azure Devops functions through `azure_devops`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`azure_devops.get_pull_requests`](#get_pull_requests)
+
+## get_pull_requests
+
+`azure_devops.get_pull_requests(projectId)`
+
+Get pull requests for the given project. The result is an enumeration of PullRequest.
+
+Argument:
+
+- `projectId`: The azure devops project id. Cannot be null or empty.
+
+### Returns
+
+Returns an enumeration of pull requests for the selected repository (or an empty enumeration when no pull requests are found).
+
+### Example
+
+#### Usage
+
+Get all pull requests for the selected repository in a given devops project:
+
+
+```
+devops_project_id = "805ACF64-0F06-47EC-96BF-E830895E2740";
+prs = azure_devops.get_pull_requests(devops_project_id);
+```
+
+#### Result
+
+As a result, the variable `prs` could contain two pull requests with the following dummy data:
+
+
+
+```yaml
+- repository-id: b1a0619a-cb69-4bf6-9b97-6c62481d9bff
+ name: some pr1
+ url: https://my-url/pr1
+- repository-id: f99e85ee-2c23-414b-8804-6a6c34f8c349
+ name: other pr - bug
+ url: https://my-url/pr3
+```
+snippet source | anchor
+
+
+#### RepositoryAction sample
+
+
+
+```yaml
+context:
+- type: evaluate-script@1
+ content: |-
+ devops_project_id = "805ACF64-0F06-47EC-96BF-E830895E2740";
+ prs = azure_devops.get_pull_requests(devops_project_id);
+
+action-menu:
+- type: foreach@1
+ active: 'array.size(prs) > 1'
+ enumerable: prs
+ variable: pr
+ actions:
+ - type: url@1
+ name: '{{ pr.name }}'
+ url: '{{ pr.url }}'
+```
+snippet source | anchor
+
+
diff --git a/docs_new/script_variables_file.generated.md b/docs_new/script_variables_file.generated.md
new file mode 100644
index 00000000..f7e11acb
--- /dev/null
+++ b/docs_new/script_variables_file.generated.md
@@ -0,0 +1,199 @@
+# `file`
+
+Provides file related action menu functions and variables accessable through `file`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`file.dir_exists`](#dir_exists)
+- [`file.file_exists`](#file_exists)
+- [`file.find_files`](#find_files)
+
+## dir_exists
+
+`file.dir_exists(path)`
+
+Checks if the specified directory path exists on the disk.
+
+Argument:
+
+- `path`: Absolute path to a directory.
+
+### Returns
+
+`true` if the specified directory path exists on the disk, `false` otherwise.
+
+### Example
+
+#### Usage
+
+Check if directory exists
+
+
+```
+exists = file.dir_exists('C:\Project\');
+exists = file.dir_exists('C:\Project');
+exists = file.dir_exists('C:/Project/');
+```
+
+#### RepositoryAction sample
+
+
+
+```yaml
+context:
+
+# create a variable to store the path to the Visual Studio Code executable
+- type: evaluate-script@1
+ content: |-
+ exe_vs_code = env.LocalAppData + "/Programs/Microsoft VS Code/code.exe";
+
+# create a variable to store the path to the documentation directory
+# based on the remote name
+- type: render-variable@1
+ name: repo_docs_directory
+ value: 'G:\\My Drive\\RepoDocs\\github.com\\{{ remote_name_origin }}'
+
+action-menu:
+
+# If the document directory exists ..
+- type: folder@1
+ name: Documentation
+ active: file.dir_exists(repo_docs_directory)
+ is-deferred: true
+ actions:
+ # .. show the menu item to open it in Visual Studio Code
+ - type: executable@1
+ name: Open in Visual Studio Code
+ executable: '{{ exe_vs_code }}'
+ arguments: '"{{ repo_docs_directory }}"'
+ # .. and a menu item to open it in Windows File Explorer
+ - type: command@1
+ name: Open in Windows File Explorer
+ command: '"{{ repo_docs_directory }}"'
+
+# if the directory does not exists, create a menu item to create it
+- type: command@1
+ name: Create Documentation directory
+ command: cmd
+ arguments: /k mkdir "{{ repo_docs_directory }}"
+ active: '!file.dir_exists(repo_docs_directory)'
+```
+snippet source | anchor
+
+
+
+## file_exists
+
+`file.file_exists(path)`
+
+Checks if the specified file path exists on the disk.
+
+Argument:
+
+- `path`: Absolute path to a file.
+
+### Returns
+
+`true` if the specified file path exists on the disk, `false` otherwise.
+
+### Example
+
+#### Usage
+
+Check if file exists
+
+
+```
+exists = file.file_exists('C:\Project\my-solution.sln');
+```
+
+#### RepositoryAction sample
+
+
+
+```yaml
+action-menu:
+# Show menu item to edit the .editorconfig file if it exists.
+- type: executable@1
+ name: Edit .editorconfig in Visual Studio Code
+ executable: '{{ env.LocalAppData }}/Programs/Microsoft VS Code/code.exe'
+ arguments: '"{{ repository.linux_path }}/.editorconfig"'
+ active: 'file.file_exists(repository.linux_path + "/.editorconfig")'
+```
+snippet source | anchor
+
+
+
+## find_files
+
+`file.find_files(rootPath,searchPattern)`
+
+Find files in a given directory based on the search pattern. Resulting filenames are absolute path based.
+
+Arguments:
+
+- `rootPath`: The root folder.
+- `searchPattern`: The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions.
+
+### Returns
+
+Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern.
+
+### Example
+
+#### Usage
+
+Locate all solution files in the given directory.
+
+
+```
+solution_files = file.find_files('C:\Project\', '*.sln');
+```
+
+#### Result
+
+As a result, the variable `solution_files` is an enumerable of strings, for example:
+
+
+```yaml
+- C:\Project\My Repositories\my-solution.sln
+- C:\Project\My Repositories\src\test solution.sln
+```
+
+#### RepositoryAction sample
+
+
+
+```yaml
+context:
+- type: evaluate-script@1
+ content: |-
+ func get_filename(path)
+ ret path | string.split("\\") | array.last
+ end
+
+ solution_files = file.find_files(repository.path, "*.sln");
+
+action-menu:
+# Open in visual studio when only one sln file was found in the repo.
+- type: command@1
+ name: Open in Visual Studio
+ command: '{{ array.first(solution_files) }}'
+ active: 'array.size(solution_files) == 1'
+
+# Use folder to choose sln file when multiple sln files found.
+- type: folder@1
+ name: Open in Visual Studio
+ active: 'array.size(solution_files) > 1'
+ actions:
+ - type: foreach@1
+ enumerable: solution_files
+ variable: sln
+ actions:
+ - type: command@1
+ name: '{{ get_filename(sln) }}'
+ command: '{{ sln }}'
+```
+snippet source | anchor
+
+
diff --git a/docs_new/script_variables_heidi.generated.md b/docs_new/script_variables_heidi.generated.md
new file mode 100644
index 00000000..b4b48a29
--- /dev/null
+++ b/docs_new/script_variables_heidi.generated.md
@@ -0,0 +1,91 @@
+# `heidi`
+
+Provides variables provided by the Heidi module. The variables are accessable through `heidi`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`heidi.databases`](#databases)
+
+## databases
+
+`heidi.databases`
+
+Gets all known databases configured in the Heidi configuration related to the selected repository.
+
+### Returns
+
+An enumerable of database configuration objects as shown in the example below.
+
+### Example
+
+Get all database configurations for the current repository:
+
+
+```
+databases = heidi.databases;
+```
+
+#### Result
+
+As a result, the variable `databases` could contain the following dummy database configuration:
+
+
+
+```yaml
+- metadata:
+ name: heidi-key
+ order: 1
+ tags:
+ - Test
+ - Dev
+ database:
+ key: MyDomainDb1
+ host: database.my-domain.com
+ user: coenm
+ password: myS3cr3t!
+ port: 2345
+ uses-windows-authentication: false
+ database-type:
+ name: MariaDB/MySQL
+ protocol: named pipe
+ library: MSOLEDBSQL
+ comment: HeidiSQL Comment
+ databases:
+ - database1
+ - database2
+```
+snippet source | anchor
+
+
+#### RepositoryAction sample
+
+
+
+```yaml
+context:
+- type: evaluate-script@1
+ content: |-
+ databases = heidi.databases;
+ exe_heidi_sql = "C:/Program Files/HeidiSQL/heidisql.exe";
+ exe_ssms ="C:/Program Files (x86)/Microsoft SQL Server Management Studio 18/Common7/IDE/Ssms.exe";
+
+action-menu:
+- type: foreach@1
+ active: 'array.size(databases) > 0'
+ enumerable: databases
+ variable: db
+ actions:
+ # open in Heidi Sql
+ - type: executable@1
+ name: Open {{ db.metadata.name }} in HeidiSQL
+ executable: '{{ exe_heidi_sql }}'
+ arguments: --description "{{ db.database.key }}"
+ # open in SQL Server Management Studio
+ - type: executable@1
+ name: Open {{ db.metadata.name }} in SQL Server Management Studio
+ executable: '{{ exe_ssms }}'
+ arguments: -S "{{ db.database.host }}" -d "{{ array.first db.database.databases }}" -U "{{ db.database.user }}"
+```
+snippet source | anchor
+
+
diff --git a/docs_new/script_variables_repository.generated.md b/docs_new/script_variables_repository.generated.md
new file mode 100644
index 00000000..166f3eb4
--- /dev/null
+++ b/docs_new/script_variables_repository.generated.md
@@ -0,0 +1,115 @@
+# `repository`
+
+Provides action menu functions and variables for the current repository through `repository`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`repository.branch`](#branch)
+- [`repository.branches`](#branches)
+- [`repository.linux_path`](#linux_path)
+- [`repository.local_branches`](#local_branches)
+- [`repository.location`](#location)
+- [`repository.name`](#name)
+- [`repository.path`](#path)
+- [`repository.remotes`](#remotes)
+- [`repository.windows_path`](#windows_path)
+
+## branch
+
+`repository.branch`
+
+Gets the current branch of the repository
+
+### Returns
+
+The name of the current branch.
+
+## branches
+
+`repository.branches`
+
+Gets the current branch of the repository
+
+### Returns
+
+The name of the current branch.
+
+## linux_path
+
+`repository.linux_path`
+
+Gets the path of the repository in linux style (i.e. use `\`). The path does NOT end with a backslash.
+
+### Returns
+
+The backslash based path of the repository without the last backslash.
+
+## local_branches
+
+`repository.local_branches`
+
+Gets the local branches
+
+### Returns
+
+All local branches.
+
+## location
+
+`repository.location`
+
+Gets the Location of the repository.
+
+### Returns
+
+The path of the repository.
+
+## name
+
+`repository.name`
+
+Gets the name of the repository.
+
+### Returns
+
+The name of the repository.
+
+### Example
+
+#### Usage
+
+
+```
+repository.name
+```
+
+
+## path
+
+`repository.path`
+
+Gets the path of the repository. The path is windows or linux based (depending on the running OS) and does NOT end with a (back)slash.
+
+### Returns
+
+The repository path.
+
+## remotes
+
+`repository.remotes`
+
+Gets the remotes.
+
+### Returns
+
+Remotes.
+
+## windows_path
+
+`repository.windows_path`
+
+Gets the path of the repository in windows style (i.e. use `/`). The path does NOT end with a slash.
+
+### Returns
+
+The path of the repository.
diff --git a/docs_new/script_variables_sonarcloud.generated.md b/docs_new/script_variables_sonarcloud.generated.md
new file mode 100644
index 00000000..da3903a6
--- /dev/null
+++ b/docs_new/script_variables_sonarcloud.generated.md
@@ -0,0 +1,58 @@
+# `sonarcloud`
+
+Provides a sonar cloud method providing the favorite status of the current repository.
+
+This module contains the following methods, variables and/or constants:
+
+- [`sonarcloud.is_favorite`](#is_favorite)
+
+## is_favorite
+
+`sonarcloud.is_favorite(id)`
+
+Get favorite status of repository related to the id.
+
+Argument:
+
+- `id`: The sonarcloud id related to the repository.
+
+### Returns
+
+`true` when the repository is set as favorite in SonarCloud, `false`, otherwise.
+
+### Example
+
+#### Usage
+
+Gets SonarClouds favorite status of the repository:
+
+
+```
+sonarcloud_repository_id = "RepoM";
+is_favorite = sonarcloud.is_favorite(sonarcloud_repository_id);
+```
+
+#### Result
+
+As a result, the boolean variable `is_favorite` is set.
+
+#### RepositoryAction sample
+
+
+
+```yaml
+context:
+- type: evaluate-script@1
+ content: |-
+ sonarcloud_repository_id = "RepoM";
+ is_favorite = sonarcloud.is_favorite(sonarcloud_repository_id);
+
+action-menu:
+- type: url@1
+ name: 'Open SonarClouds favorite in browser'
+ url: 'https://sonarcloud.io/project/overview?id={{ sonarcloud_repository_id }}'
+ active: is_favorite
+```
+snippet source | anchor
+
+
diff --git a/docs_new/script_variables_statistics.generated.md b/docs_new/script_variables_statistics.generated.md
new file mode 100644
index 00000000..d2fa9c4f
--- /dev/null
+++ b/docs_new/script_variables_statistics.generated.md
@@ -0,0 +1,48 @@
+# `statistics`
+
+Provides statistical information accessible through `statistics`.
+
+This module contains the following methods, variables and/or constants:
+
+- [`statistics.count`](#count)
+- [`statistics.overall_count`](#overall_count)
+
+## count
+
+`statistics.count`
+
+Gets the number of actions performed on the current repository.
+
+### Returns
+
+Number of actions performed on the current repository.
+
+### Example
+
+#### Usage
+
+
+```
+repo_call_count = statistics.count;
+```
+
+
+## overall_count
+
+`statistics.overall_count`
+
+Gets the number of actions performed on all repositories known in RepoM.
+
+### Returns
+
+Number of actions performed on any known repository.
+
+### Example
+
+#### Usage
+
+
+```
+repo_call_count = statistics.overall_count;
+```
+
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..501e79a8
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "8.0.100",
+ "rollForward": "latestFeature"
+ }
+}
\ No newline at end of file
diff --git a/mdsnippets.json b/mdsnippets.json
index 8abb9067..fb0ced03 100644
--- a/mdsnippets.json
+++ b/mdsnippets.json
@@ -3,7 +3,7 @@
"LinkFormat": "GitHub",
"TocLevel": 3,
"ExcludeDirectories": [ "_ReSharper.Caches", "packages", "build"],
- "MaxWidth": 120,
+ "MaxWidth": 160,
"TreatMissingAsWarning": true,
"WriteHeader": false,
"ReadOnly": false,
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 964fd760..70624792 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -5,10 +5,6 @@
true
-
-
-
-
diff --git a/src/RepoM.ActionMenu.CodeGen/.editorconfig b/src/RepoM.ActionMenu.CodeGen/.editorconfig
new file mode 100644
index 00000000..a95b88e2
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/.editorconfig
@@ -0,0 +1,16 @@
+root = false
+
+[*.cs]
+
+###############################
+# Diagnostics #
+###############################
+
+# CS8321: Local function '...' is declared but never used
+dotnet_diagnostic.CS8321.severity = none
+
+###############################
+# ReSharper #
+###############################
+
+
diff --git a/src/RepoM.ActionMenu.CodeGen/DocsClassVisitor.cs b/src/RepoM.ActionMenu.CodeGen/DocsClassVisitor.cs
new file mode 100644
index 00000000..1dfbb780
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/DocsClassVisitor.cs
@@ -0,0 +1,32 @@
+namespace RepoM.ActionMenu.CodeGen;
+
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+using RepoM.ActionMenu.CodeGen.Models;
+
+public class DocsClassVisitor : IClassDescriptorVisitor
+{
+ private readonly ITypeSymbol _typeSymbol;
+ private readonly IDictionary _files;
+
+ public DocsClassVisitor(ITypeSymbol typeSymbol, IDictionary files)
+ {
+ _typeSymbol = typeSymbol;
+ _files = files;
+ }
+
+ public void Visit(ActionMenuContextClassDescriptor descriptor)
+ {
+ Misc.XmlDocsParser.ExtractDocumentation(_typeSymbol, descriptor, _files);
+ }
+
+ public void Visit(ActionMenuClassDescriptor descriptor)
+ {
+ Misc.XmlDocsParser.ExtractDocumentation(_typeSymbol, descriptor, _files);
+ }
+
+ public void Visit(ClassDescriptor descriptor)
+ {
+ Misc.XmlDocsParser.ExtractDocumentation(_typeSymbol, descriptor, _files);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/DocumentationGenerator.cs b/src/RepoM.ActionMenu.CodeGen/DocumentationGenerator.cs
new file mode 100644
index 00000000..99b8ed1d
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/DocumentationGenerator.cs
@@ -0,0 +1,84 @@
+namespace RepoM.ActionMenu.CodeGen;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using RepoM.ActionMenu.CodeGen.Models;
+using Scriban;
+using Scriban.Runtime;
+
+internal static class DocumentationGenerator
+{
+ public static async Task GetPluginDocsContentAsync(ProjectDescriptor plugin, Template template)
+ {
+ plugin.ActionMenus.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.Ordinal));
+
+ var context = new TemplateContext
+ {
+ LoopLimit = 0,
+ MemberRenamer = x => x.Name,
+ };
+
+ var scriptObject = new ScriptObject()
+ {
+ { "plugin", plugin },
+ };
+ scriptObject.Import(typeof(MyStringFunctions));
+
+ context.PushGlobal(scriptObject);
+
+ return await template.RenderAsync(context);
+ }
+
+ public static async Task GetDocsContentAsync(ActionMenuContextClassDescriptor module, Template template)
+ {
+ module.Members.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.Ordinal));
+
+ var context = new TemplateContext
+ {
+ LoopLimit = 0,
+ MemberRenamer = x => x.Name,
+ };
+
+ var scriptObject = new ScriptObject()
+ {
+ { "module", module },
+ };
+ scriptObject.Import(typeof(MyStringFunctions));
+
+ context.PushGlobal(scriptObject);
+
+ return await template.RenderAsync(context);
+ }
+
+ public static async Task GetScribanInitializersCSharpCodeAsync(IEnumerable actionContextMenus, Template templateModule)
+ {
+ var modules = actionContextMenus.OrderBy(x => x.ClassName).ToList();
+
+ var context = new TemplateContext
+ {
+ LoopLimit = 0,
+ MemberRenamer = x => x.Name,
+ EnableRelaxedMemberAccess = false,
+ };
+
+ var scriptObject = new ScriptObject()
+ {
+ { "modules", modules },
+ };
+ scriptObject.Import(typeof(MyStringFunctions));
+
+ context.PushGlobal(scriptObject);
+
+ return await templateModule.RenderAsync(context).ConfigureAwait(false);
+ }
+}
+
+public static class MyStringFunctions
+{
+ public static string Hyphenated(string input)
+ {
+ return string.Concat(input.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x : x.ToString())).ToLowerInvariant();
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/IClassDescriptorVisitor.cs b/src/RepoM.ActionMenu.CodeGen/IClassDescriptorVisitor.cs
new file mode 100644
index 00000000..d8ba62c3
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/IClassDescriptorVisitor.cs
@@ -0,0 +1,12 @@
+namespace RepoM.ActionMenu.CodeGen;
+
+using RepoM.ActionMenu.CodeGen.Models;
+
+public interface IClassDescriptorVisitor
+{
+ void Visit(ActionMenuContextClassDescriptor descriptor);
+
+ void Visit(ActionMenuClassDescriptor descriptor);
+
+ void Visit(ClassDescriptor descriptor);
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Misc/CompilationExtensions.cs b/src/RepoM.ActionMenu.CodeGen/Misc/CompilationExtensions.cs
new file mode 100644
index 00000000..92d9707d
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Misc/CompilationExtensions.cs
@@ -0,0 +1,18 @@
+namespace RepoM.ActionMenu.CodeGen.Misc;
+
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+
+public static class CompilationExtensions
+{
+ public static IEnumerable GetTypes(this Compilation compilation)
+ {
+ foreach (ISymbol type in compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type))
+ {
+ if (type is ITypeSymbol typeSymbol)
+ {
+ yield return typeSymbol;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Misc/CompileRepoM.cs b/src/RepoM.ActionMenu.CodeGen/Misc/CompileRepoM.cs
new file mode 100644
index 00000000..56556d28
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Misc/CompileRepoM.cs
@@ -0,0 +1,93 @@
+namespace RepoM.ActionMenu.CodeGen.Misc;
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Broslyn;
+using Microsoft.CodeAnalysis;
+
+public class CompileRepoM
+{
+ private readonly ConcurrentDictionary> _compilations = new();
+
+ public Dictionary> Store => _compilations.ToDictionary(x => x.Key, x => x.Value);
+
+ public async Task CompileAsync(string pathToSolution, string projectName)
+ {
+ CSharpCompilationCaptureResult compilationCaptureResult = CSharpCompilationCapture.Build(pathToSolution);
+ Solution solution = compilationCaptureResult.Workspace.CurrentSolution;
+
+ Project[] projects = solution.Projects.ToArray();
+
+ foreach (Project p in projects)
+ {
+ if (_compilations.ContainsKey(p.Name))
+ {
+ continue;
+ }
+
+ Project project = p.WithParseOptions(p.ParseOptions!.WithDocumentationMode(DocumentationMode.Parse));
+
+ // Compile the project
+ Compilation compilation = await project.GetCompilationAsync() ?? throw new Exception("Compilation failed");
+ ValidateCompilation(compilation);
+
+ _compilations.AddOrUpdate(p.Name, _ => new Tuple(project, compilation), (_, __) => new Tuple(project, compilation));
+ }
+
+ if (_compilations.TryGetValue(projectName, out Tuple? tuple))
+ {
+ return tuple.Item2;
+ }
+
+ throw new Exception("Not found");
+ }
+
+ private static void ValidateCompilation(Compilation compilation)
+ {
+ ImmutableArray diagnostics = compilation.GetDiagnostics();
+ Diagnostic[] errors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToArray();
+
+ if (errors.Length <= 0)
+ {
+ return;
+ }
+
+ if ("RepoM.Api".Equals(compilation.AssemblyName))
+ {
+ if (errors is [{ Id: "CS8795", },])
+ {
+ return;
+ }
+ }
+
+ if ("RepoM.Plugin.AzureDevOps".Equals(compilation.AssemblyName))
+ {
+ if (errors is [{ Id: "CS8795", },])
+ {
+ return;
+ }
+ }
+
+ if ("RepoM.Plugin.EverythingFileSearch".Equals(compilation.AssemblyName))
+ {
+ if (errors.Length == 8)
+ {
+ return;
+ }
+ }
+
+ Console.WriteLine("Compilation errors:");
+ foreach (Diagnostic error in errors)
+ {
+ Console.WriteLine(error);
+ }
+
+ Console.WriteLine("Error, Exiting.");
+ Environment.Exit(1);
+ throw new Exception("Compilation error");
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Misc/FileSystemHelper.cs b/src/RepoM.ActionMenu.CodeGen/Misc/FileSystemHelper.cs
new file mode 100644
index 00000000..fd823197
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Misc/FileSystemHelper.cs
@@ -0,0 +1,41 @@
+namespace RepoM.ActionMenu.CodeGen.Misc;
+
+using System;
+using System.IO;
+
+public static class FileSystemHelper
+{
+ public static void DeleteFileIsExist(string pathToGeneratedCode)
+ {
+ if (!File.Exists(pathToGeneratedCode))
+ {
+ return;
+ }
+
+ try
+ {
+ File.Delete(pathToGeneratedCode);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Could not delete generated file '{pathToGeneratedCode}'. {e.Message}");
+ throw;
+ }
+ }
+
+ public static void CheckDirectory(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ throw new Exception($"Folder '{path}' does not exist");
+ }
+ }
+
+ public static void CheckFile(string path)
+ {
+ if (!File.Exists(path))
+ {
+ throw new Exception($"File '{path}' does not exist");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Misc/SymbolExtensions.cs b/src/RepoM.ActionMenu.CodeGen/Misc/SymbolExtensions.cs
new file mode 100644
index 00000000..be584063
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Misc/SymbolExtensions.cs
@@ -0,0 +1,12 @@
+namespace RepoM.ActionMenu.CodeGen.Misc;
+
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+public static class SymbolExtensions
+{
+ public static AttributeData? FindAttribute(this ISymbol symbol)
+ {
+ return symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass!.Name == typeof(T).Name);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Misc/TypeMatcher.cs b/src/RepoM.ActionMenu.CodeGen/Misc/TypeMatcher.cs
new file mode 100644
index 00000000..fbef989f
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Misc/TypeMatcher.cs
@@ -0,0 +1,29 @@
+namespace RepoM.ActionMenu.CodeGen.Misc;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+public static class TypeMatcher
+{
+ public static bool TypeSymbolMatchesType(ITypeSymbol typeSymbol, Type type, Compilation compilation)
+ {
+ return SymbolEqualityComparer.IncludeNullability.Equals(GetTypeSymbolForType(type, compilation), typeSymbol);
+ }
+
+ public static INamedTypeSymbol GetTypeSymbolForType(Type type, Compilation compilation)
+ {
+ if (!type.IsConstructedGenericType)
+ {
+ return compilation.GetTypeByMetadataName(type.FullName!)!;
+ }
+
+ // get all typeInfo's for the Type arguments
+ IEnumerable typeArgumentsTypeInfos = type.GenericTypeArguments.Select(a => GetTypeSymbolForType(a, compilation));
+
+ Type openType = type.GetGenericTypeDefinition();
+ INamedTypeSymbol? typeSymbol = compilation.GetTypeByMetadataName(openType.FullName!);
+ return typeSymbol!.Construct(typeArgumentsTypeInfos.ToArray());
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Misc/XmlDocsParser.cs b/src/RepoM.ActionMenu.CodeGen/Misc/XmlDocsParser.cs
new file mode 100644
index 00000000..53e8a7a4
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Misc/XmlDocsParser.cs
@@ -0,0 +1,341 @@
+namespace RepoM.ActionMenu.CodeGen.Misc;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.CodeAnalysis;
+using RepoM.ActionMenu.CodeGen.Models;
+
+internal static partial class XmlDocsParser
+{
+ public static void ExtractDocumentation(ISymbol symbol, IXmlDocsExtended desc, IDictionary files)
+ {
+ var xmlStr = symbol.GetDocumentationCommentXml();
+ ExtractDocumentation(xmlStr, symbol, desc, files);
+ }
+
+ internal static void ExtractDocumentation(string? xmlStr, ISymbol symbol, IXmlDocsExtended desc, IDictionary files)
+ {
+ if (string.IsNullOrEmpty(xmlStr))
+ {
+ return;
+ }
+
+ try
+ {
+ var xmlDoc = XElement.Parse(xmlStr);
+ var elements = xmlDoc.Elements().ToList();
+
+ foreach (XElement element in elements)
+ {
+ var text = GetCleanedString(element).Trim();
+ if (element.Name == "summary")
+ {
+ desc.Description = SanitizeMultilineText(text);
+ }
+ else if (element.Name == "param")
+ {
+ var argName = element.Attribute("name")?.Value;
+ if (argName == null || symbol is not IMethodSymbol method)
+ {
+ continue;
+ }
+ IParameterSymbol? parameterSymbol = method.Parameters.FirstOrDefault(x => x.Name == argName);
+ var isOptional = false;
+ if (parameterSymbol == null)
+ {
+ Console.WriteLine($"Invalid XML doc parameter name {argName} not found on method {method}");
+ }
+ else
+ {
+ isOptional = parameterSymbol.IsOptional;
+
+ var displayString = parameterSymbol.Type.ToDisplayString();
+ if (displayString.Equals("RepoM.ActionMenu.Core.Model.ActionMenuGenerationContext"))
+ {
+ // also check index? should be 0 or 1?!
+ if (parameterSymbol.Name.Equals("context"))
+ {
+ continue;
+ }
+
+ Console.WriteLine($"ActionMenuGenerationContext with wrong name {parameterSymbol.Name}");
+ }
+ else if (displayString.Equals("RepoM.ActionMenu.Interface.ActionMenuFactory.IActionMenuGenerationContext"))
+ {
+ // also check index? should be 0 or 1?!
+ if (parameterSymbol.Name.Equals("context"))
+ {
+ continue;
+ }
+
+ Console.WriteLine($"IActionMenuGenerationContext with wrong name {parameterSymbol.Name}");
+ }
+ else if (displayString.EndsWith("ActionMenuGenerationContext"))
+ {
+ Console.WriteLine(">> ActionMenuGenerationContext with invalid namespace found.");
+ }
+ else if (displayString.Equals("Scriban.Parsing.SourceSpan"))
+ {
+ // also check index? should be 0 or 1?!
+ if (parameterSymbol.Name.Equals("span"))
+ {
+ continue;
+ }
+
+ Console.WriteLine($"SourceSpan with wrong name {parameterSymbol.Name}");
+ }
+ }
+
+ desc.Params.Add(new ParamDescriptor(argName, text) { IsOptional = isOptional, });
+ }
+ else if (element.Name == "returns")
+ {
+ desc.Returns = text;
+ }
+ else if (element.Name == "remarks")
+ {
+ desc.Remarks = text;
+ }
+ else if (element.Name == "example")
+ {
+ ExamplesDescriptor examplesDescriptor = GetExampleData(element, files);
+ desc.Examples = examplesDescriptor;
+ }
+ else if (element.Name == "test")
+ {
+ // todo?
+ _ = _removeCode.Replace(text, string.Empty);
+ }
+ else if (element.Name == "inheritdoc")
+ {
+ // expect text to be empty
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ throw new Exception("Text should be empty in inheritdoc");
+ }
+
+ // we need cref
+ var cref = element.Attribute("cref")?.Value;
+
+ if (string.IsNullOrWhiteSpace(cref))
+ {
+ throw new Exception("Cref should not be empty in inheritdoc");
+ }
+
+ if (!cref.StartsWith("P:") && !cref.StartsWith("T:"))
+ {
+ throw new Exception("Cref should start with P: or T:");
+ }
+
+ desc.InheritDocs = cref[2..];
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Error while processing `{symbol}` with XML doc `{xmlStr}", ex);
+ }
+ }
+
+ private static string SanitizeMultilineText(string text)
+ {
+ return text.Replace("\n ", "\n");
+ }
+
+ public static ExamplesDescriptor GetExampleData(XNode node, IDictionary files)
+ {
+ var result = new ExamplesDescriptor();
+
+ if (node.NodeType != XmlNodeType.Element)
+ {
+ return result;
+ // expect example node
+ }
+
+ var element = (XElement)node;
+ if (element.Name != "example")
+ {
+ return result;
+ // expect example node
+ }
+
+ XNode[] nodes = element.Nodes().ToArray();
+ if (nodes.Length == 0)
+ {
+ result.Description = element.Value.Trim();
+ return result;
+ }
+
+ foreach (XNode item in nodes)
+ {
+ if (item is XText xText)
+ {
+ result.Items.Add(new Text() { Content = xText.Value.Trim(), });
+
+ }
+ else if (item is XElement xElement)
+ {
+ if (xElement.Name == "para")
+ {
+ result.Items.Add(new Paragraph { Text = xElement.Value.Trim(), });
+ }
+ else if (xElement.Name == "usage")
+ {
+ result.Items.Add(new Header { Text = "Usage", });
+ }
+ else if (xElement.Name == "result")
+ {
+ result.Items.Add(new Header { Text = "Result", });
+ }
+ else if (xElement.Name == "repository-action-sample")
+ {
+ result.Items.Add(new Header { Text = "RepositoryAction sample", });
+ }
+ else if (xElement.Name == "code")
+ {
+ result.Items.Add(new Code() { Content = xElement.Value.Trim(), Language = null, UseRaw = false, });
+ }
+ else if (xElement.Name == "snippet")
+ {
+ // markdown snippet
+ XAttribute? customAttribute = xElement.Attributes().SingleOrDefault(x => x.Name == "name");
+ if (customAttribute == null)
+ {
+ throw new Exception("name attribute should exist");
+ }
+ var name = customAttribute.Value.Trim();
+
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new Exception("Name should not be null");
+ }
+
+ var snippet = new Snippet { Name = name, };
+
+ customAttribute = xElement.Attributes().SingleOrDefault(x => x.Name == "mode");
+ if (customAttribute != null)
+ {
+ if (Enum.TryParse(customAttribute.Value.Trim().AsSpan(), true, out SnippetMode mode))
+ {
+ snippet.Mode = mode;
+ }
+ else
+ {
+ throw new Exception("mode attribute should not be empty");
+ }
+ }
+
+ customAttribute = xElement.Attributes().SingleOrDefault(x => x.Name == "language");
+ if (customAttribute != null)
+ {
+ if (!string.IsNullOrWhiteSpace(customAttribute.Value))
+ {
+ snippet.Language = customAttribute.Value.Trim();
+ }
+ else
+ {
+ throw new Exception("language attribute should not be empty");
+ }
+ }
+
+ result.Items.Add(snippet);
+ }
+ else if (xElement.Name == "code-file")
+ {
+ XAttribute? customAttribute = xElement.Attributes().SingleOrDefault(x => x.Name == "filename");
+ if (customAttribute == null)
+ {
+ throw new Exception("filename attribute should exist");
+ }
+ var filename = customAttribute.Value.Trim();
+
+ if (!files.TryGetValue(filename, out var content))
+ {
+ throw new Exception($"File '{filename}' not found");
+ }
+
+ var code = new Code { Content = content, UseRaw = true, };
+
+ customAttribute = xElement.Attributes().SingleOrDefault(x => x.Name == "language");
+ if (customAttribute != null)
+ {
+ if (!string.IsNullOrWhiteSpace(customAttribute.Value))
+ {
+ code.Language = customAttribute.Value.Trim();
+ }
+ else
+ {
+ throw new Exception("language attribute should not be empty");
+ }
+ }
+
+ // check if file exists, load sample
+ result.Items.Add(code);
+ }
+ else if (xElement.Name == "md-snippet")
+ {
+ Console.WriteLine("WARNING md-snippet");
+ var snippetName = xElement.Value.Trim();
+ result.Items.Add(new Text() { Content = "snippet: " + snippetName, });
+ }
+ else if (xElement.Name == "md-include")
+ {
+ Console.WriteLine("WARNING md-include");
+ var includeName = xElement.Value.Trim();
+ result.Items.Add(new Text() { Content = "include: " + includeName, });
+ }
+ else
+ {
+ throw new Exception($"'{xElement.Name}' Not expected");
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public static string GetCleanedString(XNode node)
+ {
+ if (node.NodeType == XmlNodeType.Text)
+ {
+ // return node.ToString();
+ var s = node.ToString();
+ return HttpUtility.HtmlDecode(s);
+ }
+
+ var element = (XElement)node;
+ string text;
+ if (element.Name == "paramref")
+ {
+ text = element.Attribute("name")?.Value ?? string.Empty;
+ }
+ else
+ {
+ var builder = new StringBuilder();
+ foreach (var subElement in element.Nodes())
+ {
+ builder.Append(GetCleanedString(subElement));
+ }
+
+ text = builder.ToString();
+ }
+
+ if (element.Name == "para")
+ {
+ text += "\n";
+ }
+ return HttpUtility.HtmlDecode(text);
+ }
+
+
+ private static readonly Regex _removeCode = RemoveCodeRegex();
+
+ [GeneratedRegex("^\\s*```\\w*[ \\t]*[\\r\\n]*", RegexOptions.Multiline)]
+ private static partial Regex RemoveCodeRegex();
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/ActionMenuClassDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/ActionMenuClassDescriptor.cs
new file mode 100644
index 00000000..0fe65a27
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/ActionMenuClassDescriptor.cs
@@ -0,0 +1,20 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+[DebuggerDisplay($"{{{nameof(ClassName)},nq}}")]
+public class ActionMenuClassDescriptor : ClassDescriptor
+{
+ ///
+ /// Properties
+ ///
+ public List ActionMenuProperties { get; set; } = new List();
+
+ public string RepositoryActionName => Name;
+
+ public override void Accept(IClassDescriptorVisitor visitor)
+ {
+ visitor.Visit(this);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/ActionMenuContextClassDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/ActionMenuContextClassDescriptor.cs
new file mode 100644
index 00000000..8efe8549
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/ActionMenuContextClassDescriptor.cs
@@ -0,0 +1,14 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System.Diagnostics;
+
+[DebuggerDisplay($"{{{nameof(ActionMenuContextObjectName)},nq}}")]
+public class ActionMenuContextClassDescriptor : ClassDescriptor
+{
+ public string ActionMenuContextObjectName => Name;
+
+ public override void Accept(IClassDescriptorVisitor visitor)
+ {
+ visitor.Visit(this);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/ClassDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/ClassDescriptor.cs
new file mode 100644
index 00000000..060f69e5
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/ClassDescriptor.cs
@@ -0,0 +1,65 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using RepoM.ActionMenu.CodeGen;
+
+public enum SymbolType
+{
+ Class,
+
+ Enum,
+}
+
+[DebuggerDisplay($"{{{nameof(ClassName)},nq}}")]
+public class ClassDescriptor : IXmlDocsExtended
+{
+ private SymbolType _symbolType = SymbolType.Class;
+
+ ///
+ /// Properties, Functions, fields etc. etc.
+ ///
+ public List Members { get; set; } = [];
+
+ ///
+ /// Friendly name
+ ///
+ public string Name { get; init; } = null!;
+
+ public string ClassName { get; set; } = null!;
+
+ public string Namespace { get; set; } = null!;
+
+ public bool IsEnum => _symbolType == SymbolType.Enum;
+
+ public bool IsClass => _symbolType == SymbolType.Class;
+
+ public void SetType(SymbolType type)
+ {
+ _symbolType = type;
+ }
+
+ // interface:
+
+ public string Description { get; set; } = null!;
+
+ public string? InheritDocs { get; set; }
+
+ string IXmlDocsExtended.Returns
+ {
+ get => throw new NotSupportedException("no returns for class.");
+ set => throw new NotSupportedException("no returns for class.");
+ }
+
+ public string Remarks { get; set; } = null!;
+
+ public ExamplesDescriptor? Examples { get; set; }
+
+ List IXmlDocsExtended.Params => throw new NotSupportedException("no params for class.");
+
+ public virtual void Accept(IClassDescriptorVisitor visitor)
+ {
+ visitor.Visit(this);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/ExamplesDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/ExamplesDescriptor.cs
new file mode 100644
index 00000000..52f13b68
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/ExamplesDescriptor.cs
@@ -0,0 +1,65 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System.Collections.Generic;
+
+public class ExamplesDescriptor
+{
+ public string? Description { get; set; }
+
+ public List Items { get; } = [];
+}
+
+public abstract class ExampleItemBase
+{
+ public abstract string TypeName { get; }
+}
+
+public sealed class Code : ExampleItemBase
+{
+ public override string TypeName { get; } = nameof(Code);
+
+ public string? Language { get; set; } = null;
+
+ public required string Content { get; init; }
+
+ public bool UseRaw { get; set; }
+}
+
+public sealed class Paragraph : ExampleItemBase
+{
+ public override string TypeName { get; } = nameof(Paragraph);
+
+ public required string Text { get; init; }
+}
+
+public sealed class Text : ExampleItemBase
+{
+ public override string TypeName { get; } = nameof(Text);
+
+ public string Content { get; init; } = null!;
+}
+
+public sealed class Header : ExampleItemBase
+{
+ public override string TypeName { get; } = nameof(Header);
+
+ public required string Text { get; init; }
+}
+
+public sealed class Snippet : ExampleItemBase
+{
+ public override string TypeName { get; } = nameof(Snippet);
+
+ public SnippetMode Mode { get; set; } = SnippetMode.Include;
+
+ public string? Language { get; set; }
+
+ public required string Name { get; init; }
+}
+
+public enum SnippetMode
+{
+ Include,
+
+ Snippet,
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/IXmlDocsExtended.cs b/src/RepoM.ActionMenu.CodeGen/Models/IXmlDocsExtended.cs
new file mode 100644
index 00000000..73b3c067
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/IXmlDocsExtended.cs
@@ -0,0 +1,18 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System.Collections.Generic;
+
+public interface IXmlDocsExtended
+{
+ string Description { get; set; }
+
+ string? InheritDocs { get; set; }
+
+ string Returns { get; set; }
+
+ string Remarks { get; set; }
+
+ ExamplesDescriptor? Examples { get; set; }
+
+ List Params { get; }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/MemberDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/MemberDescriptor.cs
new file mode 100644
index 00000000..c8a4d5c9
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/MemberDescriptor.cs
@@ -0,0 +1,134 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+
+public static class TypeInfoDescriptorFactory
+{
+ public static TypeInfoDescriptor Create(ITypeSymbol typeSymbol)
+ {
+ var displayString = typeSymbol.ToDisplayString();
+ if (Program.TypeInfos.TryGetValue(displayString, out TypeInfoDescriptor? typeInfoDescriptor))
+ {
+ return typeInfoDescriptor;
+ }
+
+ var result = new TypeInfoDescriptor(typeSymbol);
+ Program.TypeInfos.Add(displayString, result);
+ return result;
+ }
+}
+
+public class TypeInfoDescriptor
+{
+ public TypeInfoDescriptor(ITypeSymbol typeSymbol)
+ : this (typeSymbol.Name, typeSymbol.ToDisplayString())
+ {
+ Nullable = IsNullableType(typeSymbol);
+
+ if (Name.Contains("AutoCompleteOptionsV1"))
+ {
+ // do nothing intentionally, just for debugging coenm
+ }
+ }
+
+ public TypeInfoDescriptor(string name, string csharpTypeName)
+ {
+ CSharpTypeName = csharpTypeName;
+ Name = name;
+
+ if (CSharpTypeName.Contains("RepoM"))
+ {
+ // Name = CSharpTypeName.Split('.').Last();
+ Name = name;
+ }
+
+ if (!csharpTypeName.Contains('.'))
+ {
+ // primitive?
+ Name = CSharpTypeName;
+ }
+
+ if ("System.Collections.Generic.List".Equals(CSharpTypeName))
+ {
+ Name = "List";
+ }
+ }
+
+ public string CSharpTypeName { get; }
+
+ public string Name { get; }
+
+ public string? Link { get; init; }
+
+ public bool Nullable { get; set; }
+
+ private static bool IsNullableType(ITypeSymbol typeSymbol)
+ {
+ return typeSymbol is INamedTypeSymbol { NullableAnnotation: NullableAnnotation.Annotated, };
+ }
+}
+
+///
+/// Property, Function, field etc. etc.
+///
+public class MemberDescriptor : IXmlDocsExtended
+{
+ ///
+ /// Friendly Name
+ ///
+ public string Name { get; init; } = null!;
+
+ public string CSharpName { get; set; } = null!;
+
+ public TypeInfoDescriptor? ReturnType { get; set; }
+
+ public string XmlId { get; set; } = null!;
+
+ public bool IsCommand { get; set; }
+
+ public bool IsAction { get; set; }
+
+ public bool IsFunc { get; set; }
+
+ public bool IsConst { get; set; }
+
+ ///
+ /// Used for C# code generation
+ ///
+ public string? Cast { get; set; }
+
+ public string Description { get; set; } = null!;
+
+ public string? InheritDocs { get; set; }
+
+ public string Returns { get; set; } = null!;
+
+ public string Remarks { get; set; } = null!;
+
+ public ExamplesDescriptor? Examples { get; set; }
+
+ public List Params { get; } = [];
+}
+
+public class ActionMenuMemberDescriptor : MemberDescriptor
+{
+ // public RepositoryActionAttribute RepositoryActionAttribute { get; init; }
+
+ public bool IsTemplate { get; set; }
+
+ public bool IsPredicate { get; set; }
+
+ public bool IsContext { get; set; }
+
+ public object DefaultValue { get; set; } = null!;
+
+ public bool IsReturnEnumerable { get; set; }
+
+ public string? RefType { get; set; }
+}
+
+public class ActionMenuContextMemberDescriptor : MemberDescriptor
+{
+ public string ActionMenuContextMemberName => Name;
+}
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/ParamDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/ParamDescriptor.cs
new file mode 100644
index 00000000..6c7a67a3
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/ParamDescriptor.cs
@@ -0,0 +1,17 @@
+namespace RepoM.ActionMenu.CodeGen.Models
+{
+ public class ParamDescriptor
+ {
+ public ParamDescriptor(string name, string description)
+ {
+ Name = name;
+ Description = description;
+ }
+
+ public string Name { get; set; }
+
+ public string Description { get; set; }
+
+ public bool IsOptional { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Models/ProjectDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/Models/ProjectDescriptor.cs
new file mode 100644
index 00000000..d1251d6c
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Models/ProjectDescriptor.cs
@@ -0,0 +1,64 @@
+namespace RepoM.ActionMenu.CodeGen.Models;
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using RepoM.Core.Plugin.AssemblyInformation;
+using Scriban.Runtime;
+
+[DebuggerDisplay("{ProjectName,nq}")]
+public sealed class ProjectDescriptor
+{
+ ///
+ /// Assembly Name
+ ///
+ public string AssemblyName { get; set; } = null!;
+
+ ///
+ /// Project name
+ ///
+ public string ProjectName { get; set; } = null!;
+
+ ///
+ /// List of class descriptors for repository actions.
+ ///
+ public List ActionMenus { get; } = new();
+
+ ///
+ /// List of class descriptors for context (ie scriban methods, properties)
+ ///
+ public List ActionContextMenus { get; } = new();
+
+ ///
+ /// Regular types (to be used when action type has sub type property)
+ ///
+ public List Types { get; } = new();
+
+ ///
+ /// when project is plugin, the pluginname.
+ ///
+ public string? PluginName { get; private set; }
+
+ ///
+ /// when project is plugin, the plugin description.
+ ///
+ public string? PluginDescription { get; private set; }
+
+ ///
+ /// when project is plugin, the plugin markdown description.
+ ///
+ public string? PluginMarkdownDescription { get; private set; }
+
+ ///
+ /// is plugin or not.
+ ///
+ public bool IsPlugin { get; private set; } = false;
+
+ [ScriptMemberIgnore]
+ public void SetPackageInformation(PackageAttribute attribute)
+ {
+ PluginName = attribute.Name;
+ PluginDescription = attribute.ToolTip;
+ PluginMarkdownDescription = attribute.Description;
+ IsPlugin = true;
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/ProcessMembersVisitor.cs b/src/RepoM.ActionMenu.CodeGen/ProcessMembersVisitor.cs
new file mode 100644
index 00000000..969bfaed
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/ProcessMembersVisitor.cs
@@ -0,0 +1,390 @@
+namespace RepoM.ActionMenu.CodeGen;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using RepoM.ActionMenu.CodeGen.Misc;
+using RepoM.ActionMenu.CodeGen.Models;
+using RepoM.ActionMenu.Interface.Attributes;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+using Text = RepoM.ActionMenu.Interface.YamlModel.Templating.Text;
+
+public class ProcessMembersVisitor : IClassDescriptorVisitor
+{
+ private readonly ITypeSymbol _typeSymbol;
+ private readonly IDictionary _files;
+
+ // todo extend.
+ private static readonly string[] _collectionTypes =
+ {
+ "System.Collections.Generic.List",
+ "System.Collections.Generic.IList",
+ "System.Collections.Generic.IEnumerable",
+ };
+
+ public ProcessMembersVisitor(ITypeSymbol typeSymbol, IDictionary files)
+ {
+ _typeSymbol = typeSymbol;
+ _files = files;
+ }
+
+ public void Visit(ActionMenuContextClassDescriptor descriptor)
+ {
+ foreach (ISymbol member in _typeSymbol.GetMembers())
+ {
+ AttributeData? attr = member.FindAttribute();
+ if (attr == null)
+ {
+ // normal member -> skip and continue.
+ continue;
+ }
+
+ var actionMenuContextMemberAttribute = new ActionMenuContextMemberAttribute((attr.ConstructorArguments[0].Value as string)!);
+ // action menu context member.
+
+ var className = member.ContainingSymbol.Name;
+
+ var memberDescriptor = new ActionMenuContextMemberDescriptor
+ {
+ Name = actionMenuContextMemberAttribute.Alias,
+ CSharpName = member.Name,
+ //ReturnType = propertyMember.Type.ToDisplayString(), // (member as IPropertySymbol)?.Type;
+ IsCommand = false,
+ XmlId = member.GetDocumentationCommentId() ?? string.Empty,
+ };
+
+ if (member is IMethodSymbol method)
+ {
+ memberDescriptor.ReturnType = TypeInfoDescriptorFactory.Create(method.ReturnType);
+ memberDescriptor.IsCommand = method.ReturnsVoid;
+
+ memberDescriptor.CSharpName = method.Name;
+
+ memberDescriptor.IsAction = method.ReturnsVoid;
+ memberDescriptor.IsFunc = !memberDescriptor.IsAction;
+
+ var builder = new StringBuilder();
+ builder.Append(memberDescriptor.IsAction ? "Action" : "Func");
+
+ if (method.Parameters.Length > 0 || memberDescriptor.IsFunc)
+ {
+ builder.Append('<');
+ }
+
+ for (var i = 0; i < method.Parameters.Length; i++)
+ {
+ IParameterSymbol parameter = method.Parameters[i];
+ if (i > 0)
+ {
+ builder.Append(", ");
+ }
+
+ builder.Append(parameter.Type.ToDisplayString());
+ }
+
+ if (memberDescriptor.IsFunc)
+ {
+ if (method.Parameters.Length > 0)
+ {
+ builder.Append(", ");
+ }
+ builder.Append(method.ReturnType.ToDisplayString());
+ }
+
+ if (method.Parameters.Length > 0 || memberDescriptor.IsFunc)
+ {
+ builder.Append('>');
+ }
+
+ memberDescriptor.Cast = $"({builder})";
+ }
+
+ if (member is IPropertySymbol property) // or field IFieldSymbol
+ {
+ memberDescriptor.ReturnType = TypeInfoDescriptorFactory.Create(property.Type);
+ memberDescriptor.IsConst = true;
+ }
+
+ descriptor.Members.Add(memberDescriptor);
+
+ Misc.XmlDocsParser.ExtractDocumentation(member, memberDescriptor, _files);
+ }
+ }
+
+ public void Visit(ActionMenuClassDescriptor descriptor)
+ {
+ foreach (ISymbol member in _typeSymbol.GetMembers())
+ {
+ if (member is not IPropertySymbol propertyMember)
+ {
+ continue;
+ }
+
+ if (propertyMember.SetMethod == null)
+ {
+ // property is readonly
+ continue;
+ }
+
+ if (propertyMember.GetMethod == null)
+ {
+ // property is writeonly
+ continue;
+ }
+
+ // Name = name,
+ // XmlId = member.GetDocumentationCommentId() ?? string.Empty,
+ // Category = string.Empty,
+ // IsCommand = method?.ReturnsVoid ?? false,
+ // Module = moduleToGenerate,
+
+ SymbolDisplayFormat symbolDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining).WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.None);
+
+ void JustSomeCode(ITypeSymbol symbol)
+ {
+ var Name = symbol.ToDisplayString(symbolDisplayFormat);
+ var IsEnumerable = symbol.AllInterfaces.Any(x => x.ToString() == "System.Collections.IEnumerable");
+ var IsVoid = symbol.SpecialType == SpecialType.System_Void;
+ if (IsEnumerable && (symbol is INamedTypeSymbol namedSymbol))
+ {
+ var firstTypeArgumentName = namedSymbol.TypeArguments.FirstOrDefault()?.ToDisplayString(symbolDisplayFormat);
+ }
+ }
+
+ static bool IsSystemType(IPropertySymbol symbol)
+ {
+ var typeFullName = symbol.Type.ToString();
+ return typeFullName?.StartsWith("System.") ?? false;
+ }
+
+ // single type in collection (array, list, ..) (no tuple or whatsoever)
+ static bool IsCollection(IPropertySymbol symbol, [NotNullWhen(true)] out ITypeSymbol? genericType)
+ {
+ if (symbol.Type is IArrayTypeSymbol ats)
+ {
+ genericType = ats.ElementType;
+ return true;
+ }
+
+ var originalDefinitionDisplayName = symbol.Type.OriginalDefinition.ToDisplayString();
+
+ var displayString = symbol.Type.ToDisplayString();
+
+ if (_collectionTypes.Contains(originalDefinitionDisplayName))
+ {
+ // must be singe due to
+ genericType = ((INamedTypeSymbol)symbol.Type).TypeArguments.Single();
+ return true;
+ }
+
+ genericType = null;
+ return false;
+ }
+
+ var propertyReturnType = propertyMember.Type.ToDisplayString();
+
+ bool IsTypeOrNullableType()
+ {
+ var typeFullName = typeof(T).FullName ?? string.Empty;
+ return propertyReturnType.Equals(typeFullName) || propertyReturnType.Equals(typeFullName + "?");
+ }
+
+ var memberDescriptor = new ActionMenuMemberDescriptor
+ {
+ CSharpName = propertyMember.Name,
+ ReturnType = TypeInfoDescriptorFactory.Create(propertyMember.Type), // (member as IPropertySymbol)?.Type;
+ XmlId = member.GetDocumentationCommentId() ?? string.Empty,
+ };
+
+ if (IsTypeOrNullableType())
+ {
+ memberDescriptor.IsTemplate = true;
+
+ AttributeData? attr = propertyMember.FindAttribute();
+ if (attr?.ConstructorArguments.Length == 1)
+ {
+ var textAttribute = new TextAttribute((attr.ConstructorArguments[0].Value as string)!);
+ memberDescriptor.DefaultValue = textAttribute.DefaultValue;
+ }
+ }
+ else if (IsTypeOrNullableType())
+ {
+ memberDescriptor.IsPredicate = true;
+
+ AttributeData? attr = propertyMember.FindAttribute();
+ if (attr != null)
+ {
+ var predicateAttribute = new PredicateAttribute((bool)attr.ConstructorArguments[0].Value!);
+ memberDescriptor.DefaultValue = predicateAttribute.DefaultValue;
+ }
+ }
+ else if (IsTypeOrNullableType())
+ {
+ memberDescriptor.IsContext = true;
+ }
+ else if (IsCollection(propertyMember, out ITypeSymbol? genericType))
+ {
+ // todo
+ // is is a collection of x. process x
+ memberDescriptor.IsReturnEnumerable = true;
+ }
+ else if (IsSystemType(propertyMember))
+ {
+ // ie string, int, bool, ..
+ Console.WriteLine("d");
+ }
+ else if (propertyReturnType.Contains("RepoM.ActionMenu.CodeGenDummyLibrary.ActionMenu.Model.ActionMenus.AutoCompleteOptionsV1"))
+ {
+ // aditional checks?
+ // todo, name
+ memberDescriptor.RefType = $"{propertyMember.ContainingModule.Name}; {propertyReturnType}";
+ }
+
+ // if (!typeSymbol.Interfaces.Any(namedTypeSymbol => namedTypeSymbol.Equals(actionMenuInterface, SymbolEqualityComparer.Default)))
+ // {
+ // continue;
+ // }
+
+ XmlDocsParser.ExtractDocumentation(member, memberDescriptor, _files);
+
+ if (string.IsNullOrWhiteSpace(memberDescriptor.Description) && string.IsNullOrWhiteSpace(memberDescriptor.InheritDocs))
+ {
+ if (!memberDescriptor.CSharpName.Equals("Type"))
+ {
+ Console.WriteLine($"Skip property '{_typeSymbol.Name}.{memberDescriptor.CSharpName}' due to missing description");
+ }
+
+ continue;
+ }
+
+ descriptor.ActionMenuProperties.Add(memberDescriptor);
+ }
+ }
+
+ public void Visit(ClassDescriptor descriptor)
+ {
+ if (_typeSymbol is INamedTypeSymbol { TypeKind: TypeKind.Enum, } symbol)
+ {
+ descriptor.SetType(SymbolType.Enum);
+
+ // INamedTypeSymbol? underlyingType = symbol.EnumUnderlyingType;
+
+ var memberNames = _typeSymbol
+ .GetMembers()
+ .Where(static member => member.Kind is SymbolKind.Field)
+ // .Select(static symbol => symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
+ ;
+
+ foreach (ISymbol member in memberNames)
+ {
+ var memberDescriptor = new MemberDescriptor
+ {
+ CSharpName = member.Name,
+ IsCommand = false,
+ XmlId = member.GetDocumentationCommentId() ?? string.Empty,
+ };
+
+ Misc.XmlDocsParser.ExtractDocumentation(member, memberDescriptor, _files);
+ descriptor.Members.Add(memberDescriptor);
+ }
+
+ return;
+ }
+
+ foreach (ISymbol member in _typeSymbol.GetMembers())
+ {
+ if (member is not IPropertySymbol propertyMember)
+ {
+ // only interested in properties.
+ continue;
+ }
+
+ if (!member.CanBeReferencedByName)
+ {
+ continue;
+ }
+
+ if (member.DeclaredAccessibility == Accessibility.Private)
+ {
+ continue;
+ }
+
+ if (member.IsStatic)
+ {
+ continue;
+ }
+
+
+ // only normal members.
+ var className = member.ContainingSymbol.Name;
+
+ var memberDescriptor = new MemberDescriptor
+ {
+ CSharpName = member.Name,
+ //ReturnType = propertyMember.Type.ToDisplayString(), // (member as IPropertySymbol)?.Type;
+ IsCommand = false,
+ XmlId = member.GetDocumentationCommentId() ?? string.Empty,
+ };
+
+ if (member is IMethodSymbol method)
+ {
+ memberDescriptor.ReturnType = TypeInfoDescriptorFactory.Create(method.ReturnType);
+ memberDescriptor.IsCommand = method.ReturnsVoid;
+
+ memberDescriptor.CSharpName = method.Name;
+
+ memberDescriptor.IsAction = method.ReturnsVoid;
+ memberDescriptor.IsFunc = !memberDescriptor.IsAction;
+
+ var builder = new StringBuilder();
+ builder.Append(memberDescriptor.IsAction ? "Action" : "Func");
+
+ if (method.Parameters.Length > 0 || memberDescriptor.IsFunc)
+ {
+ builder.Append('<');
+ }
+
+ for (var i = 0; i < method.Parameters.Length; i++)
+ {
+ IParameterSymbol parameter = method.Parameters[i];
+ if (i > 0)
+ {
+ builder.Append(", ");
+ }
+
+ builder.Append(parameter.Type.ToDisplayString());
+ }
+
+ if (memberDescriptor.IsFunc)
+ {
+ if (method.Parameters.Length > 0)
+ {
+ builder.Append(", ");
+ }
+ builder.Append(method.ReturnType.ToDisplayString());
+ }
+
+ if (method.Parameters.Length > 0 || memberDescriptor.IsFunc)
+ {
+ builder.Append('>');
+ }
+
+ memberDescriptor.Cast = $"({builder})";
+ }
+
+ if (member is IPropertySymbol property) // or field IFieldSymbol
+ {
+ memberDescriptor.ReturnType = TypeInfoDescriptorFactory.Create(property.Type);
+ memberDescriptor.IsConst = true;
+ }
+
+ descriptor.Members.Add(memberDescriptor);
+
+ Misc.XmlDocsParser.ExtractDocumentation(member, memberDescriptor, _files);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Program.cs b/src/RepoM.ActionMenu.CodeGen/Program.cs
new file mode 100644
index 00000000..f8518cd4
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Program.cs
@@ -0,0 +1,348 @@
+namespace RepoM.ActionMenu.CodeGen;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using RepoM.ActionMenu.CodeGen.Misc;
+using RepoM.ActionMenu.CodeGen.Models;
+using RepoM.ActionMenu.Core.TestLib.Utils;
+using RepoM.ActionMenu.Interface.Attributes;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.AssemblyInformation;
+using Scriban;
+
+public static class Program
+{
+ internal static readonly Dictionary TypeInfos = new()
+ {
+ {
+ typeof(Interface.YamlModel.Templating.Text).FullName!,
+ new TypeInfoDescriptor(nameof(Text), typeof(Interface.YamlModel.Templating.Text).FullName!)
+ {
+ Link = "repository_action_types.md#text",
+ }
+ },
+ {
+ typeof(Interface.YamlModel.Templating.Predicate).FullName!,
+ new TypeInfoDescriptor(nameof(Interface.YamlModel.Templating.Predicate), typeof(Interface.YamlModel.Templating.Predicate).FullName!)
+ {
+ Link = "repository_action_types.md#predicate",
+ }
+ },
+ {
+ typeof(Interface.YamlModel.ActionMenus.Context).FullName!,
+ new TypeInfoDescriptor(nameof(Interface.YamlModel.ActionMenus.Context), typeof(Interface.YamlModel.ActionMenus.Context).FullName!)
+ {
+ Link = "repository_action_types.md#context",
+ }
+ },
+ {
+ typeof(Interface.YamlModel.ActionMenus.Context).FullName! + "?",
+ new TypeInfoDescriptor(nameof(Interface.YamlModel.ActionMenus.Context), typeof(Interface.YamlModel.ActionMenus.Context).FullName! + "?")
+ {
+ Link = "repository_action_types.md#context",
+ }
+ },
+ };
+
+ public static async Task Main()
+ {
+ // var ns = typeSymbol.ContainingNamespace.ToDisplayString();
+ // var fullClassName = $"{ns}.{className}";
+ var compile = new CompileRepoM();
+ var rootFolder = ThisProjectAssembly.Info.GetSolutionDirectory();
+ var srcFolder = Path.Combine(rootFolder, "src");
+ var docsFolderSource = Path.Combine(rootFolder, "docs_new", "mdsource");
+ var docsFolder = Path.Combine(rootFolder, "docs_new");
+
+ FileSystemHelper.CheckDirectory(srcFolder);
+ FileSystemHelper.CheckDirectory(docsFolder);
+ FileSystemHelper.CheckDirectory(Path.Combine(rootFolder, ".git"));
+
+ var projects = new List
+ {
+ "RepoM.ActionMenu.Interface", // this is for the description of the interface types and its members.
+ "RepoM.ActionMenu.Core",
+
+ "RepoM.Plugin.AzureDevOps",
+ "RepoM.Plugin.Clipboard",
+ "RepoM.Plugin.EverythingFileSearch",
+ "RepoM.Plugin.Heidi",
+ "RepoM.Plugin.LuceneQueryParser",
+ "RepoM.Plugin.SonarCloud",
+ "RepoM.Plugin.Statistics",
+ "RepoM.Plugin.WebBrowser",
+ "RepoM.Plugin.WindowsExplorerGitInfo",
+ };
+
+ Template templateModule = await LoadTemplateAsync("Templates/ScribanModuleRegistration.scriban-cs");
+ Template templateDocs = await LoadTemplateAsync("Templates/DocsScriptVariables.scriban-txt");
+ Template templatePluginDocs = await LoadTemplateAsync("Templates/DocsPlugin.scriban-txt");
+
+ Dictionary files = await LoadFiles();
+
+ var processedProjects = new Dictionary();
+
+ foreach (var project in projects)
+ {
+ var fullCsProjectFilename = Path.Combine(srcFolder, project, $"{project}.csproj");
+ FileSystemHelper.CheckFile(fullCsProjectFilename);
+
+ ProjectDescriptor projectDescriptor = await CompileAndExtractProjectDescription(compile, fullCsProjectFilename, project, files);
+ processedProjects.Add(project, projectDescriptor);
+ }
+
+ Dictionary> _allTypes2 = new();
+
+ foreach ((var projectName, ProjectDescriptor project) in processedProjects)
+ {
+ foreach (var classDescriptor in project.ActionMenus)
+ {
+ if (!_allTypes2.ContainsKey(classDescriptor.Namespace + "." + classDescriptor.ClassName))
+ {
+ _allTypes2.Add(classDescriptor.Namespace + "." + classDescriptor.ClassName, new List());
+ }
+
+ foreach (var memberDescriptor in classDescriptor.ActionMenuProperties)
+ {
+ _allTypes2[classDescriptor.Namespace + "." + classDescriptor.ClassName].Add(memberDescriptor);
+ }
+ foreach (var memberDescriptor in classDescriptor.Members)
+ {
+ _allTypes2[classDescriptor.Namespace + "." + classDescriptor.ClassName].Add(memberDescriptor);
+ }
+ }
+
+ foreach (var classDescriptor in project.ActionContextMenus)
+ {
+ if (!_allTypes2.ContainsKey(classDescriptor.Namespace + "." + classDescriptor.ClassName))
+ {
+ _allTypes2.Add(classDescriptor.Namespace + "." + classDescriptor.ClassName, new List());
+ }
+ foreach (var memberDescriptor in classDescriptor.Members)
+ {
+ _allTypes2[classDescriptor.Namespace + "." + classDescriptor.ClassName].Add(memberDescriptor);
+ }
+ }
+
+ foreach (var classDescriptor in project.Types)
+ {
+ if (!_allTypes2.ContainsKey(classDescriptor.Namespace + "." + classDescriptor.ClassName))
+ {
+ _allTypes2.Add(classDescriptor.Namespace + "." + classDescriptor.ClassName, new List());
+ }
+ foreach (var memberDescriptor in classDescriptor.Members)
+ {
+ _allTypes2[classDescriptor.Namespace + "." + classDescriptor.ClassName].Add(memberDescriptor);
+ }
+ }
+ }
+
+ processedProjects.Remove("RepoM.ActionMenu.Interface");
+
+ // Copy descriptions from if (string.IsNullOrWhiteSpace(memberDescriptor.Description) && string.IsNullOrWhiteSpace(memberDescriptor.InheritDocs))
+ foreach ((var projectName, ProjectDescriptor project) in processedProjects)
+ {
+ foreach (ActionMenuClassDescriptor classDescriptor in project.ActionMenus)
+ {
+ foreach (ActionMenuMemberDescriptor memberDescriptor in classDescriptor.ActionMenuProperties)
+ {
+ if (!string.IsNullOrWhiteSpace(memberDescriptor.Description) || string.IsNullOrWhiteSpace(memberDescriptor.InheritDocs))
+ {
+ continue;
+ }
+
+ var index = memberDescriptor.InheritDocs.LastIndexOf('.');
+ var className = memberDescriptor.InheritDocs[..index];
+ var typeName = memberDescriptor.InheritDocs[(index + 1)..];
+
+ if (_allTypes2.TryGetValue(className, out List? xxx))
+ {
+ MemberDescriptor? matchMemberDescriptor = xxx.SingleOrDefault(x => x.CSharpName == typeName);
+ if (matchMemberDescriptor != null)
+ {
+ memberDescriptor.Description = matchMemberDescriptor.Description;
+ }
+ else
+ {
+ Console.WriteLine("InheritDocs not found");
+ }
+ }
+ else
+ {
+ Console.WriteLine("InheritDocs not found");
+ }
+ }
+ }
+ }
+
+
+ // Generate plugin documentation
+ foreach ((var projectName, ProjectDescriptor? project) in processedProjects)
+ {
+ if (project.IsPlugin)
+ {
+ var name = project.ProjectName.ToLowerInvariant();
+ var fileName = Path.Combine(docsFolderSource, $"plugin_{name}.generated.source.md");
+ var content = await DocumentationGenerator.GetPluginDocsContentAsync(project, templatePluginDocs).ConfigureAwait(false);
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+
+ fileName = Path.Combine(docsFolder, $"plugin_{name}.generated.md");
+ if (!File.Exists(fileName))
+ {
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ // core
+ var fileName = Path.Combine(docsFolderSource, "repom.generated.source.md");
+ var content = await DocumentationGenerator.GetPluginDocsContentAsync(project, templatePluginDocs).ConfigureAwait(false);
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+
+ fileName = Path.Combine(docsFolder, "repom.generated.md");
+ if (!File.Exists(fileName))
+ {
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+ }
+ }
+ }
+
+ // Generate module site documentation
+ foreach ((var projectName, ProjectDescriptor? project) in processedProjects)
+ {
+ foreach (ActionMenuContextClassDescriptor actionContextMenu in project.ActionContextMenus)
+ {
+ var name = actionContextMenu.Name.ToLowerInvariant();
+ var fileName = Path.Combine(docsFolderSource, $"script_variables_{name}.generated.source.md");
+ var content = await DocumentationGenerator.GetDocsContentAsync(actionContextMenu, templateDocs).ConfigureAwait(false);
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+
+ fileName = Path.Combine(docsFolder, $"script_variables_{name}.generated.md");
+ if (!File.Exists(fileName))
+ {
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+ }
+ }
+ }
+
+ // Generate module registration code in c#.
+ foreach ((var projectName, ProjectDescriptor? project) in processedProjects)
+ {
+ var fileName = Path.Combine(srcFolder, projectName, "RepoMCodeGen.generated.cs");
+
+ if (project.ActionContextMenus.Count == 0)
+ {
+ FileSystemHelper.DeleteFileIsExist(fileName);
+ continue;
+ }
+
+ var content = await DocumentationGenerator.GetScribanInitializersCSharpCodeAsync(project.ActionContextMenus, templateModule).ConfigureAwait(false);
+ await File.WriteAllTextAsync(fileName, content).ConfigureAwait(false);
+ }
+ }
+
+ public static async Task CompileAndExtractProjectDescription(CompileRepoM compile, string pathToSolution, string project, IDictionary files)
+ {
+ Compilation compilation = await compile.CompileAsync(pathToSolution, project).ConfigureAwait(false);
+
+ var projectDescriptor = new ProjectDescriptor
+ {
+ AssemblyName = compilation.AssemblyName ?? throw new Exception("Could not determine AssemblyName"),
+ ProjectName = project,
+ };
+
+ AttributeData? assemblyAttribute = compilation.Assembly.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Name == nameof(PackageAttribute));
+ if (assemblyAttribute != null)
+ {
+ var pa = new PackageAttribute(
+ (assemblyAttribute.ConstructorArguments[0].Value as string)!,
+ (assemblyAttribute.ConstructorArguments[1].Value as string)!,
+ (assemblyAttribute.ConstructorArguments[2].Value as string)!);
+ projectDescriptor.SetPackageInformation(pa);
+ }
+
+ ProcessProject(compilation, projectDescriptor, files);
+
+ return projectDescriptor;
+ }
+
+ private static void ProcessProject(Compilation compilation, ProjectDescriptor projectDescriptor, IDictionary files)
+ {
+ foreach (ITypeSymbol typeSymbol in compilation.GetTypes())
+ {
+ ProcessMembersVisitor memberVisitor = new(typeSymbol, files);
+ DocsClassVisitor docsClassVisitor = new(typeSymbol, files);
+
+ ClassDescriptor classDescriptor;
+
+ AttributeData? obsoleteAttribute = typeSymbol.FindAttribute();
+ AttributeData? actionMenuContextAttribute = typeSymbol.FindAttribute();
+ AttributeData? repositoryActionAttribute = typeSymbol.FindAttribute();
+
+ if (actionMenuContextAttribute != null && obsoleteAttribute == null)
+ {
+ var actionMenuContextClassDescriptor = new ActionMenuContextClassDescriptor
+ {
+ Name = new ActionMenuContextAttribute((string)actionMenuContextAttribute.ConstructorArguments[0].Value!).Name!,
+ };
+
+ projectDescriptor.ActionContextMenus.Add(actionMenuContextClassDescriptor);
+
+ classDescriptor = actionMenuContextClassDescriptor;
+ }
+ else if (repositoryActionAttribute != null && obsoleteAttribute == null)
+ {
+ var actionMenuClassDescriptor = new ActionMenuClassDescriptor
+ {
+ Name = new RepositoryActionAttribute((string)repositoryActionAttribute.ConstructorArguments[0].Value!).Type,
+ };
+ projectDescriptor.ActionMenus.Add(actionMenuClassDescriptor);
+
+ classDescriptor = actionMenuClassDescriptor;
+ }
+ else
+ {
+ classDescriptor = new ClassDescriptor();
+ projectDescriptor.Types.Add(classDescriptor);
+ }
+
+ classDescriptor.ClassName = typeSymbol.Name;
+ classDescriptor.Namespace = typeSymbol.ContainingNamespace.ToDisplayString();
+
+ classDescriptor.Accept(docsClassVisitor);
+ classDescriptor.Accept(memberVisitor);
+ }
+ }
+
+ public static async Task LoadTemplateAsync(string path)
+ {
+ var rawTemplate = await File.ReadAllTextAsync(path);
+ var template = Template.Parse(rawTemplate);
+ if (template.HasErrors)
+ {
+ throw new Exception(template.Messages.ToString());
+ }
+
+ return template;
+ }
+
+ private static async Task> LoadFiles()
+ {
+ string[] files = Directory.GetFiles("C:\\Projects\\Private\\git\\RepoM\\docs\\snippets"); // todo coenm
+
+ var result = new Dictionary(files.Length);
+
+ foreach (string file in files)
+ {
+ var f = new FileInfo(file);
+ var fileContent = await File.ReadAllTextAsync(file);
+ result.Add(f.Name, fileContent);
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj b/src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj
new file mode 100644
index 00000000..ba404f0e
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Exe
+ net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/src/RepoM.ActionMenu.CodeGen/Templates/DocsPlugin.scriban-txt b/src/RepoM.ActionMenu.CodeGen/Templates/DocsPlugin.scriban-txt
new file mode 100644
index 00000000..5cac7806
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Templates/DocsPlugin.scriban-txt
@@ -0,0 +1,92 @@
+{{~
+func write_type_with_link(t)
+ result = t.Name;
+
+ if !string.empty t.Link
+ result = "[" + t.Name + "](" + t.Link + ")"
+ end
+
+ if (t.Nullable)
+ result = result + ", optional";
+ end
+
+ ret result;
+end
+
+func write_default_when_null(s, default_value = "\\")
+ if string.empty s
+ ret default_value
+ else
+ ret s
+ end
+end
+
+~}}
+{{~ if plugin.IsPlugin ~}}
+# {{ plugin.IsPlugin ? plugin.PluginName : "RepoM Core Repository Actions" }}
+
+{{~ if !string.empty plugin.PluginMarkdownDescription ~}}
+{{ plugin.PluginMarkdownDescription }}
+{{~ else if !string.empty plugin.PluginDescription ~}}
+{{ plugin.PluginDescription }}
+{{~ end ~}}
+
+{{~ ## include: _plugin_enable ## ~}}
+To use this module, make sure it is enabled in RepoM by opening the menu and navigate to 'Plugins'. When enabling or disabling a plugin, you should restart RepoM.
+
+- ProjectName: {{ write_default_when_null plugin.ProjectName }}
+- PluginName: {{ write_default_when_null plugin.PluginName }}
+- PluginDescription: {{ write_default_when_null plugin.PluginDescription }}
+- PluginMarkdownDescription: {{ write_default_when_null plugin.PluginMarkdownDescription }}
+{{~ else ~}}
+# {{ plugin.IsPlugin ? plugin.PluginName : "RepoM Core Repository Actions" }}
+{{~ end ~}}
+
+{{~ if array.size(plugin.ActionMenus) > 0 ~}}
+This module contains the following methods, variables and/or constants:
+ {{~ for member in plugin.ActionMenus ~}}
+
+## {{ member.Name }}
+
+{{ member.Description }}
+
+Properties:
+
+ {{~ for p in member.ActionMenuProperties ~}}
+- `{{ hyphenated p.CSharpName }}`: {{ p.Description }} ({{ write_type_with_link p.ReturnType }})
+ {{~ end ~}}
+ {{~ if member.Examples ~}}
+
+### Example
+ {{~ if !string.empty member.Examples.Description ~}}
+{{ member.Examples.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{ end }}
+ {{~ for example_item in member.Examples.Items ~}}
+ {{~ if example_item.TypeName == 'Code' ~}}
+
+```{{ example_item.Language | string.downcase }}
+ {{~ if example_item.UseRaw ~}}
+{{ example_item.Content | string.rstrip }}
+ {{~ else ~}}
+{{ example_item.Content | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{~ end ~}}
+```
+
+ {{~ else if example_item.TypeName == 'Snippet' ~}}
+{{ example_item.Mode | string.downcase }}: {{ example_item.Name }}
+
+ {{~ else if example_item.TypeName == 'Header' ~}}
+#### {{ example_item.Text }}
+
+ {{~ else if example_item.TypeName == 'Text' ~}}
+{{ example_item.Content }}
+ {{~ else if example_item.TypeName == 'Paragraph' ~}}
+
+{{ example_item.Text }}
+ {{~ else ~}}
+ NAME NOT FOUND!! {{ example_item.TypeName }}.
+ {{~ end ~}}
+ {{~ end ~}}
+ {{~ end ~}}
+ {{~ end ~}}
+{{~ end ~}}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/Templates/DocsScriptVariables.scriban-txt b/src/RepoM.ActionMenu.CodeGen/Templates/DocsScriptVariables.scriban-txt
new file mode 100644
index 00000000..2d8ff352
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Templates/DocsScriptVariables.scriban-txt
@@ -0,0 +1,94 @@
+{{~
+func is_const(member)
+ ret member.IsConst;
+end
+
+func is_funcs(member)
+ ret !member.IsConst;
+end
+
+func prefix_module(member)
+ ret module.Name | string.append "." | string.append member | string.downcase;
+end
+
+module_name = module.Name | string.downcase;
+~}}
+{{~ if !module.IsBuiltin ~}}
+# `{{module.Name}}`
+
+{{ module.Description }}
+
+{{~ end ~}}
+{{~ if array.size(module.Members) > 0 ~}}
+This module contains the following methods, variables and/or constants:
+
+ {{~ for member in module.Members ~}}
+- [`{{ prefix_module member.Name }}`](#{{ member.Name | string.downcase }})
+ {{~ end ~}}
+{{~ end ~}}
+{{~ ~}}
+{{~ for member in module.Members ~}}
+
+## {{ member.Name }}
+
+`{{ prefix_module member.Name }}{{~ if member.Params.size > 0 ~}}({{~ for param in member.Params ~}}{{ param.Name }}{{ param.IsOptional?'?':''}}{{ for.last?'':',' }}{{~ end ~}}){{~ end ~}}`
+
+ {{~ if member.Description ~}}
+{{ member.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{~ end ~}}
+ {{~ if member.Params.size > 0 ~}}
+
+Argument{{if member.Params.size > 1}}s{{end}}:
+
+ {{~ for param in member.Params ~}}
+- `{{ param.Name }}`: {{ param.Description}}
+ {{~end ~}}
+ {{~ end ~}}
+ {{~ if member.Returns ~}}
+
+### Returns
+
+{{ member.Returns | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{~ end ~}}
+ {{~ if member.Remarks ~}}
+
+### Remarks
+
+{{ member.Remarks | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{~ end ~}}
+ {{~ if member.Examples ~}}
+
+### Example
+ {{ if !string.empty member.Examples.Description }}
+{{ member.Examples.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{ end }}
+ {{~ for example_item in member.Examples.Items ~}}
+ {{~ if example_item.TypeName == 'Code' ~}}
+
+```{{ example_item.Language | string.downcase }}
+ {{~ if example_item.UseRaw ~}}
+{{ example_item.Content | string.rstrip }}
+ {{~ else ~}}
+{{ example_item.Content | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
+ {{~ end ~}}
+```
+
+{{~ else if example_item.TypeName == 'Snippet' ~}}
+{{ example_item.Mode | string.downcase }}: {{ example_item.Name }}
+
+ {{~ else if example_item.TypeName == 'Header' ~}}
+#### {{ example_item.Text }}
+
+ {{~ else if example_item.TypeName == 'Text' ~}}
+{{ example_item.Content }}
+
+ {{~ else if example_item.TypeName == 'Paragraph' ~}}
+
+{{ example_item.Text }}
+
+ {{~ else ~}}
+NAME NOT FOUND!! {{ example_item.TypeName }}.
+ {{~ end ~}}
+ {{~ end ~}}
+ {{~ end ~}}
+{{~ end ~}}
diff --git a/src/RepoM.ActionMenu.CodeGen/Templates/ScribanModuleRegistration.scriban-cs b/src/RepoM.ActionMenu.CodeGen/Templates/ScribanModuleRegistration.scriban-cs
new file mode 100644
index 00000000..257b1a2c
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Templates/ScribanModuleRegistration.scriban-cs
@@ -0,0 +1,73 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+
+using System;
+{{~ func GenerateDocDescriptor(item) ~}}
+ {
+ var descriptor = {{ if item.IsModule }}Descriptor{{ else }}Descriptors["{{ item.Name }}"]{{ end }};
+ descriptor.Category = "{{ item.Category }}";
+ descriptor.Description = @"{{ item.Description | string.replace '"' '""' }}";
+ descriptor.IsCommand = {{ item.IsCommand }};
+ {{~ for param in item.Params ~}}
+ descriptor.Params.Add(new KalkParamDescriptor("{{ param.Name }}", @"{{ param.Description | string.replace '"' '""' }}") { IsOptional = {{ param.IsOptional }} });
+ {{~ end ~}}
+ {{~ if item.Returns ~}}
+ descriptor.Returns = @"{{ item.Returns | string.replace '"' '""' }}";
+ {{~ end ~}}
+ {{~ if item.Remarks ~}}
+ descriptor.Remarks = @"{{ item.Remarks | string.replace '"' '""' }}";
+ {{~ end ~}}
+ {{~ if item.Example ~}}
+ descriptor.Example = @"{{ item.Example | string.replace '"' '""' }}";
+ {{~ end ~}}
+ }
+{{~ end ~}}
+{{~ for module in modules ~}}
+
+namespace {{ module.Namespace }}
+{
+ partial class {{ module.ClassName }}
+ {
+ {{~ if module.Name != 'All' ~}}
+ {{~ if string.contains module.Namespace 'Plugin' ~}}
+ public override void RegisterFunctions(RepoM.ActionMenu.Interface.Scriban.IContextRegistration contextRegistration)
+ {{~ else if module.ClassName == 'KalkEngine' ~}}
+ protected void RegisterFunctions()
+ {{~ else ~}}
+ protected sealed override void RegisterFunctions()
+ {{~ end ~}}
+ {
+ {{~ if string.contains module.Namespace 'Plugin' ~}}
+ contextRegistration = contextRegistration.CreateOrGetSubRegistration("{{ module.Name }}");
+ {{~ for member in module.Members ~}}
+ {{~ if member.IsConst ~}}
+ contextRegistration.RegisterConstant("{{ member.Name }}", {{ member.CSharpName }});
+ {{~ else if member.IsFunc ~}}
+ contextRegistration.RegisterFunction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }});
+ {{~ else if member.IsAction ~}}
+ contextRegistration.RegisterAction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }});
+ {{~ end ~}}
+ {{~ end ~}}
+ {{~ else ~}}
+ {{~ for member in module.Members ~}}
+ {{~ if member.IsConst ~}}
+ RegisterConstant("{{ member.Name }}", {{ member.CSharpName }});
+ {{~ else if member.IsFunc ~}}
+ RegisterFunction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }});
+ {{~ else if member.IsAction ~}}
+ RegisterAction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }});
+ {{~ end ~}}
+ {{~ end ~}}
+ {{~ end ~}}
+ }
+ {{~ end ~}}
+ }
+}
+{{~ end ~}}
diff --git a/src/RepoM.ActionMenu.CodeGen/Templates/readme.txt b/src/RepoM.ActionMenu.CodeGen/Templates/readme.txt
new file mode 100644
index 00000000..20ecdc8b
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/Templates/readme.txt
@@ -0,0 +1,29 @@
+DocsModule
+
+#Name
+
+Description
+
+## Configuration
+
+the configuration, with yaml, properties etc.
+
+
+Scripting
+- 'x' (link)
+- 'y' (link)
+
+Repository Actions
+- 'x' (link)
+
+
+------------------------
+
+
+DocsRepositoryAction
+
+
+DocsScriptVariables
+
+
+ScribanModuleRegistration
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.CodeGen/ThisProjectAssembly.cs b/src/RepoM.ActionMenu.CodeGen/ThisProjectAssembly.cs
new file mode 100644
index 00000000..77c7dada
--- /dev/null
+++ b/src/RepoM.ActionMenu.CodeGen/ThisProjectAssembly.cs
@@ -0,0 +1,8 @@
+namespace RepoM.ActionMenu.CodeGen;
+
+using RepoM.ActionMenu.Core.TestLib.Utils; // this is not what we want.
+
+internal static class ThisProjectAssembly
+{
+ public static readonly TestAssemblyInfo Info = new(typeof(ThisProjectAssembly).Assembly);
+}
\ 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
new file mode 100644
index 00000000..c0433bb2
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvScriptObject.cs
@@ -0,0 +1,95 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Context;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Scriban;
+using Scriban.Parsing;
+using Scriban.Runtime;
+
+internal sealed class EnvScriptObject : IScriptObject
+{
+ private readonly IDictionary _env;
+
+ public static EnvScriptObject Create()
+ {
+ 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);
+ }
+
+ public EnvScriptObject(IDictionary envVars)
+ {
+ _env = envVars;
+ }
+
+ public IEnumerable GetMembers()
+ {
+ return _env.Keys;
+ }
+
+ public bool Contains(string member)
+ {
+ return _env.ContainsKey(member);
+ }
+
+ public bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value)
+ {
+ if (_env.TryGetValue(member, out var s))
+ {
+ value = s;
+ return true;
+ }
+
+ value = null!;
+ return false;
+ }
+
+ public bool CanWrite(string member)
+ {
+ return false;
+ }
+
+ public bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
+ {
+ return false;
+ }
+
+ public bool Remove(string member)
+ {
+ return false;
+ }
+
+ public void SetReadOnly(string member, bool readOnly)
+ {
+ // intentionally do nothing
+ }
+
+ public IScriptObject Clone(bool deep)
+ {
+ return new EnvScriptObject(new Dictionary(_env));
+ }
+
+ 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/Context/EnvSetScriptObject.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs
new file mode 100644
index 00000000..9cd705d3
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/EnvSetScriptObject.cs
@@ -0,0 +1,125 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Context;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RepoM.ActionMenu.Core.Misc;
+using Scriban;
+using Scriban.Parsing;
+using Scriban.Runtime;
+
+internal sealed class EnvSetScriptObject : IScriptObject, IDisposable
+{
+ private FastStack _stack = new(10);
+
+ public EnvSetScriptObject(EnvScriptObject @base)
+ {
+ _ = @base ?? throw new ArgumentNullException(nameof(@base));
+ _stack.Push(@base);
+ }
+
+ public void Push(EnvScriptObject item)
+ {
+ _stack.Push(item);
+ }
+
+ public EnvScriptObject Pop()
+ {
+ return _stack.Pop();
+ }
+
+ public IEnumerable GetMembers()
+ {
+ return _stack.Items
+ .Where(envScriptObject => envScriptObject != null)
+ .SelectMany(envScriptObject => envScriptObject.GetMembers())
+ .Distinct();
+ }
+
+ public bool Contains(string member)
+ {
+ return GetMembers().Contains(member);
+ }
+
+ public bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value)
+ {
+ EnvScriptObject? result = GetFirstForMember(member);
+
+ if (result == null)
+ {
+ value = null!;
+ return false;
+ }
+
+ return result.TryGetValue(context, span, member, out value);
+ }
+
+ public bool CanWrite(string member)
+ {
+ return false;
+ }
+
+ public bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
+ {
+ return false;
+ }
+
+ public bool Remove(string member)
+ {
+ return false;
+ }
+
+ public void SetReadOnly(string member, bool readOnly)
+ {
+ // intentionally do nothing
+ }
+
+ public IScriptObject Clone(bool deep)
+ {
+ EnvScriptObject[] items = _stack.Items;
+ var result = new EnvSetScriptObject((EnvScriptObject)items[0].Clone(true));
+
+ if (items.Length <= 1)
+ {
+ return result;
+ }
+
+ for (var i = 1; i < items.Length; i++)
+ {
+ if (items[i] != null)
+ {
+ result.Push((EnvScriptObject)items[i].Clone(true));
+ }
+ }
+
+ return result;
+ }
+
+ public int Count => GetMembers().Count();
+
+ public bool IsReadOnly
+ {
+ get => true;
+ set => _ = value;
+ }
+
+ public void Dispose()
+ {
+ _stack.Clear();
+ }
+
+ private EnvScriptObject? GetFirstForMember(string member)
+ {
+ var count = _stack.Count;
+ EnvScriptObject[] items = _stack.Items;
+ for (var i = count - 1; i >= 0; i--)
+ {
+ if (items[i].Contains(member))
+ {
+ return items[i];
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Context/FileFunctions.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/FileFunctions.cs
new file mode 100644
index 00000000..63e74dc2
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/FileFunctions.cs
@@ -0,0 +1,133 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Context;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.Attributes;
+using Scriban.Parsing;
+using Scriban.Syntax;
+
+///
+/// Provides file related action menu functions and variables accessable through `file`.
+///
+[ActionMenuContext("file")]
+internal partial class FileFunctions : ScribanModuleWithFunctions
+{
+ public FileFunctions()
+ {
+ RegisterFunctions();
+ }
+
+ ///
+ /// Find files in a given directory based on the search pattern. Resulting filenames are absolute path based.
+ ///
+ /// The scriban context.
+ /// The scriban source span.
+ /// The root folder.
+ /// The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions.
+ /// Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern.
+ ///
+ ///
+ /// Locate all solution files in the given directory.
+ ///
+ /// solution_files = file.find_files('C:\Project\', '*.sln');
+ ///
+ ///
+ /// As a result, the variable `solution_files` is an enumerable of strings, for example:
+ ///
+ ///
+ ///
+ ///
+ [ActionMenuContextMember("find_files")]
+ public static string[] FindFiles(ActionMenuGenerationContext /*IMenuContext*/ context, SourceSpan span, string rootPath, string searchPattern)
+ {
+ return FindFilesInner(context as IMenuContext, span, rootPath, searchPattern);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static string[] FindFilesInner(IMenuContext context, SourceSpan span, string rootPath, string searchPattern)
+ {
+ try
+ {
+ return GetFileEnumerator(context.FileSystem, rootPath, searchPattern).ToArray();
+ }
+ catch (Exception e)
+ {
+ throw new ScriptRuntimeException(span, "Could not get files.", e);
+ }
+ }
+
+ ///
+ /// Checks if the specified file path exists on the disk.
+ ///
+ /// The scriban context.
+ /// Absolute path to a file.
+ /// `true` if the specified file path exists on the disk, `false` otherwise.
+ ///
+ ///
+ /// Check if file exists
+ ///
+ /// exists = file.file_exists('C:\Project\my-solution.sln');
+ ///
+ ///
+ ///
+ ///
+ [ActionMenuContextMember("file_exists")]
+ public static bool FileExists(ActionMenuGenerationContext context, string path)
+ {
+ return FileExistsInner(context as IMenuContext, path);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool FileExistsInner(IMenuContext context, string path)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ return context.FileSystem.File.Exists(path);
+ }
+
+ ///
+ /// Checks if the specified directory path exists on the disk.
+ ///
+ /// The scriban context.
+ /// Absolute path to a directory.
+ /// `true` if the specified directory path exists on the disk, `false` otherwise.
+ ///
+ ///
+ /// Check if directory exists
+ ///
+ /// exists = file.dir_exists('C:\Project\');
+ /// exists = file.dir_exists('C:\Project');
+ /// exists = file.dir_exists('C:/Project/');
+ ///
+ ///
+ ///
+ ///
+ [ActionMenuContextMember("dir_exists")]
+ public static bool DirectoryExists(ActionMenuGenerationContext context, string path)
+ {
+ return DirectoryExistsInner(context as IMenuContext, path);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool DirectoryExistsInner(IMenuContext context, string path)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ return context.FileSystem.Directory.Exists(path);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static IEnumerable GetFileEnumerator(IFileSystem fileSystem, string path, string searchPattern)
+ {
+ // prefer EnumerateFileSystemInfos() over EnumerateFiles() to include packaged folders like
+ // .app or .xcodeproj on macOS
+ return fileSystem.DirectoryInfo.New(path)
+ .EnumerateFileSystemInfos(searchPattern, SearchOption.AllDirectories)
+ .Select(f => f.FullName)
+ .Where(f => !f.StartsWith('.'));
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Context/RepositoryFunctions.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Context/RepositoryFunctions.cs
new file mode 100644
index 00000000..44ee3078
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Context/RepositoryFunctions.cs
@@ -0,0 +1,96 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Context;
+
+using System;
+using System.Collections;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Interface.Attributes;
+using RepoM.Core.Plugin.Repository;
+
+///
+/// Provides action menu functions and variables for the current repository through `repository`.
+///
+[ActionMenuContext("repository")]
+internal partial class RepositoryFunctions : ScribanModuleWithFunctions
+{
+ private readonly IRepository _repository;
+
+ public RepositoryFunctions(IRepository repository)
+ {
+ _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+ RegisterFunctions();
+ }
+
+ ///
+ /// Gets the name of the repository.
+ ///
+ /// The name of the repository.
+ ///
+ ///
+ ///
+ /// repository.name
+ ///
+ ///
+ [ActionMenuContextMember("name")]
+ public string Name => _repository.Name;
+
+ ///
+ /// Gets the path of the repository. The path is windows or linux based (depending on the running OS) and does NOT end with a (back)slash.
+ ///
+ /// The repository path.
+ [ActionMenuContextMember("path")]
+ public string Path => Environment.OSVersion.Platform switch
+ {
+ PlatformID.Win32NT => WindowsPath,
+ PlatformID.Unix => LinuxPath,
+ _ => string.Empty,
+ };
+
+ ///
+ /// Gets the path of the repository in windows style (i.e. use `/`). The path does NOT end with a slash.
+ ///
+ /// The path of the repository.
+ [ActionMenuContextMember("windows_path")]
+ public string WindowsPath => _repository.WindowsPath;
+
+ ///
+ /// Gets the path of the repository in linux style (i.e. use `\`). The path does NOT end with a backslash.
+ ///
+ /// The backslash based path of the repository without the last backslash.
+ [ActionMenuContextMember("linux_path")]
+ public string LinuxPath => _repository.LinuxPath;
+
+ ///
+ /// Gets the Location of the repository.
+ ///
+ /// The path of the repository.
+ [ActionMenuContextMember("location")]
+ public string Location => _repository.Location;
+
+ ///
+ /// Gets the current branch of the repository
+ ///
+ /// The name of the current branch.
+ [ActionMenuContextMember("branch")]
+ public string CurrentBranch => _repository.CurrentBranch;
+
+ ///
+ /// Gets the current branch of the repository
+ ///
+ /// The name of the current branch.
+ [ActionMenuContextMember("branches")]
+ public IEnumerable Branches => _repository.Branches;
+
+ ///
+ /// Gets the local branches
+ ///
+ /// All local branches.
+ [ActionMenuContextMember("local_branches")]
+ public IEnumerable LocalBranches => _repository.LocalBranches;
+
+ ///
+ /// Gets the remotes.
+ ///
+ /// Remotes.
+ [ActionMenuContextMember("remotes")]
+ public IEnumerable Remotes => _repository.Remotes;
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs
new file mode 100644
index 00000000..7eea8e1c
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs
@@ -0,0 +1,40 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.BrowseRepository;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to open the default webbrowser and go to the origin remote webinterface. When multiple remotes are available a sub menu is created for each remote.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionBrowseRepositoryV1 : IMenuAction, IName
+{
+ public const string TYPE_VALUE = "browse-repository@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ /// Single menu for the first remote.
+ ///
+ [Predicate(false)]
+ public Predicate FirstOnly { get; set; } = new ScribanPredicate();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ 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
new file mode 100644
index 00000000..3f4e8db1
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1Mapper.cs
@@ -0,0 +1,64 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.BrowseRepository;
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionBrowseRepositoryV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionBrowseRepositoryV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ if (repository.Remotes.Count == 0)
+ {
+ yield break;
+ }
+
+ var name = await context.RenderStringAsync(action.Name).ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ name = "Browse remote";
+ }
+
+ var forceSingle = await action.FirstOnly.EvaluateAsync(context).ConfigureAwait(false);
+
+ if (repository.Remotes.Count == 1 || forceSingle)
+ {
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = new BrowseRepositoryCommand(repository.Remotes[0].Url),
+ };
+ }
+ else
+ {
+ yield return new DeferredSubActionsUserInterfaceRepositoryAction(
+ name,
+ repository,
+ context,
+ captureScope: false,
+ async ctx => await EnumerateRemotes(ctx.Repository).ConfigureAwait(false))
+ {
+ CanExecute = true,
+ };
+ }
+ }
+
+ private static Task EnumerateRemotes(IRepository repository)
+ {
+ return Task.FromResult(repository.Remotes
+ .Take(50)
+ .Select(remote => new UserInterfaceRepositoryAction(remote.Name, repository)
+ {
+ RepositoryCommand = new BrowseRepositoryCommand(remote.Url),
+ })
+ .Cast()
+ .ToArray());
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Command/RepositoryActionCommandV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Command/RepositoryActionCommandV1.cs
new file mode 100644
index 00000000..74797c8e
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Command/RepositoryActionCommandV1.cs
@@ -0,0 +1,48 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Command;
+
+using System.ComponentModel.DataAnnotations;
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to excute a command (related to the repository)
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionCommandV1 : IMenuAction, IName
+{
+ public const string TYPE_VALUE = "command@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ /// The command to execute.
+ ///
+ [Required]
+ [Text]
+ public Text Command { get; set; } = new ScribanText();
+
+ ///
+ /// Arguments for the command.
+ ///
+ [Text]
+ public Text Arguments { get; set; } = new ScribanText();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs
new file mode 100644
index 00000000..f46b3cd7
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs
@@ -0,0 +1,25 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Command;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionCommandV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionCommandV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+ var command = await action.Command.RenderAsync(context).ConfigureAwait(false);
+ var arguments = await action.Arguments.RenderAsync(context).ConfigureAwait(false);
+
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = new StartProcessRepositoryCommand(command, arguments),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Executable/RepositoryActionExecutableV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Executable/RepositoryActionExecutableV1.cs
new file mode 100644
index 00000000..60ce3074
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Executable/RepositoryActionExecutableV1.cs
@@ -0,0 +1,51 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Executable;
+
+using System.ComponentModel.DataAnnotations;
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to excute an application with additional arguments. This action is almost identical to the `command@1` action. When no existing executables are provided, the action will not show.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionExecutableV1 : IMenuAction, IName, IContext
+{
+ public const string TYPE_VALUE = "executable@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; init; } = new ScribanPredicate();
+
+ ///
+ /// The executable.
+ ///
+ [Required]
+ [Text]
+ public Text Executable { get; set; } = new ScribanText();
+
+ ///
+ /// Arguments for the executable.
+ ///
+ [Text]
+ public Text Arguments { get; set; } = new ScribanText();
+
+ ///
+ public Context? Context { get; init; }
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Executable/RepositoryActionExecutableV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Executable/RepositoryActionExecutableV1Mapper.cs
new file mode 100644
index 00000000..afecd8c6
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Executable/RepositoryActionExecutableV1Mapper.cs
@@ -0,0 +1,25 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Executable;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionExecutableV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionExecutableV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+ var command = await action.Executable.RenderAsync(context).ConfigureAwait(false);
+ var arguments = await action.Arguments.RenderAsync(context).ConfigureAwait(false);
+
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = new StartProcessRepositoryCommand(command, arguments),
+ };
+ }
+}
\ 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
new file mode 100644
index 00000000..9c35d4c5
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs
@@ -0,0 +1,45 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Folder;
+
+using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus;
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to create a folder (sub menu) in the context menu of the repository allowing you to order actions.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionFolderV1 : IMenuAction, IName, IMenuActions, IContext, IDeferred
+{
+ public const string TYPE_VALUE = "folder@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ public ActionMenu? Actions { get; set; }
+
+ ///
+ [Text]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ 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)
+ [Predicate(false)]
+ public Predicate IsDeferred { get; set; } = new ScribanPredicate();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name} : #actions: {Actions?.Count ?? 0}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs
new file mode 100644
index 00000000..51195539
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs
@@ -0,0 +1,54 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Folder;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+
+[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);
+
+ if (action.Actions == null)
+ {
+ yield return new UserInterfaceRepositoryAction(name, repository);
+ 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;
+
+ if (isDeferred)
+ {
+ yield return new DeferredSubActionsUserInterfaceRepositoryAction(
+ name,
+ repository,
+ context,
+ action.Actions != null,
+ async ctx => await ctx.AddActionMenusAsyncArray(action.Actions).ConfigureAwait(false))
+ {
+ CanExecute = true,
+ };
+ }
+ else
+ {
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ CanExecute = true,
+ SubActions = await context.AddActionMenusAsyncArray(action.Actions),
+ };
+ }
+#pragma warning restore S2583
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs
new file mode 100644
index 00000000..3fa3d965
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs
@@ -0,0 +1,66 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.ForEach;
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+using Variable = Interface.YamlModel.Templating.Variable;
+
+///
+/// Action to create repeated actions based on a variable.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionForEachV1 : IMenuAction, IContext
+{
+ public const string TYPE_VALUE = "foreach@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ public Context? Context { get; init; }
+
+ ///
+ /// Additional context added for each iteration.
+ ///
+ public Context? IterationContext { get; init; }
+
+ ///
+ /// The list of items to enumerate on.
+ ///
+ [Required]
+ [Variable]
+ public Variable Enumerable { get; init; } = new ScribanVariable();
+
+ ///
+ /// The name of the variable to access to current enumeration of the items. For each iteration, the variable `{var.name}` has the value of the current iteration.
+ ///
+ [Required]
+ public string? Variable { get; init; }
+
+ ///
+ /// Predicate to skip the current item.
+ ///
+ [Predicate(false)]
+ public Predicate Skip { get; init; } = new ScribanPredicate();
+
+ ///
+ /// List of repeated actions.
+ ///
+ [Required]
+ public List Actions { get; init; } = new();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE})";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs
new file mode 100644
index 00000000..bbbe8606
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs
@@ -0,0 +1,59 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.ForEach;
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+
+[UsedImplicitly]
+internal class RepositoryActionForEachV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionForEachV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ if (string.IsNullOrWhiteSpace(action.Variable))
+ {
+ yield break;
+ }
+
+ var result = await action.Enumerable.EvaluateAsync(context);
+
+ if (result is not IEnumerable enumerable)
+ {
+ yield break;
+ }
+
+ foreach (var item in enumerable)
+ {
+ if (item is null)
+ {
+ continue;
+ }
+
+ using IScope scope = context.CreateGlobalScope();
+
+ scope.SetValue(action.Variable, item, true);
+
+ if (action.IterationContext != null)
+ {
+ foreach (IContextAction ctx in action.IterationContext)
+ {
+ await scope.AddContextActionAsync(ctx).ConfigureAwait(false);
+ }
+ }
+
+ if (await action.Skip.EvaluateAsync(context).ConfigureAwait(false))
+ {
+ continue;
+ }
+
+ await foreach (UserInterfaceRepositoryActionBase menuItem in context.AddActionMenusAsync(action.Actions).ConfigureAwait(false))
+ {
+ yield return menuItem;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs
new file mode 100644
index 00000000..d0ffbbf2
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs
@@ -0,0 +1,34 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Checkout;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// This action will create a menu and sub menus with all local and remote branches for an easy checkout.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionGitCheckoutV1 : IMenuAction, IName
+{
+ public const string TYPE_VALUE = "git-checkout@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text("Checkout")]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..f322372a
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1Mapper.cs
@@ -0,0 +1,78 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Checkout;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionGitCheckoutV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionGitCheckoutV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+ var remoteBranchesTranslated = await context.TranslateAsync("Remote branches");
+ var noRemoteBranchesFoundTranslated = await context.TranslateAsync("No remote branches found");
+ var tryFetchChangesTranslated = await context.TranslateAsync("Try to fetch changes if you're expecting remote branches");
+
+ yield return new DeferredSubActionsUserInterfaceRepositoryAction(
+ name,
+ repository,
+ context,
+ false,
+ _ =>
+ Task.FromResult(repository.LocalBranches
+ .Take(50)
+ .Select(branch => new UserInterfaceRepositoryAction(branch, repository)
+ {
+ RepositoryCommand = GitRepositoryCommand.Checkout(branch),
+ CanExecute = !repository.CurrentBranch.Equals(branch, StringComparison.OrdinalIgnoreCase),
+ })
+ .Union(new UserInterfaceRepositoryActionBase[]
+ {
+ new UserInterfaceSeparatorRepositoryAction(repository),
+ new DeferredSubActionsUserInterfaceRepositoryAction(
+ remoteBranchesTranslated,
+ repository,
+ context,
+ false,
+ _ =>
+ {
+ UserInterfaceRepositoryActionBase[] remoteBranches = repository
+ .ReadAllBranches()
+ .Select(branch => new UserInterfaceRepositoryAction(branch, repository)
+ {
+ RepositoryCommand = GitRepositoryCommand.Checkout(branch),
+ CanExecute = !repository.CurrentBranch.Equals(branch, StringComparison.OrdinalIgnoreCase),
+ })
+ .ToArray();
+
+ if (remoteBranches.Length > 0)
+ {
+ return Task.FromResult(remoteBranches);
+ }
+
+ var errorMenu = new UserInterfaceRepositoryActionBase[]
+ {
+ new UserInterfaceRepositoryAction(noRemoteBranchesFoundTranslated, repository)
+ {
+ CanExecute = false,
+ },
+ new UserInterfaceRepositoryAction(tryFetchChangesTranslated, repository)
+ {
+ CanExecute = false,
+ },
+ };
+ return Task.FromResult(errorMenu);
+ }),
+ })
+ .ToArray()));
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Fetch/RepositoryActionGitFetchV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Fetch/RepositoryActionGitFetchV1.cs
new file mode 100644
index 00000000..e3047c01
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Fetch/RepositoryActionGitFetchV1.cs
@@ -0,0 +1,34 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Fetch;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to execute a `git fetch` command.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionGitFetchV1 : IMenuAction, IName
+{
+ public const string TYPE_VALUE = "git-fetch@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text("Fetch")]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Fetch/RepositoryActionGitFetchV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Fetch/RepositoryActionGitFetchV1Mapper.cs
new file mode 100644
index 00000000..9c45ede8
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Fetch/RepositoryActionGitFetchV1Mapper.cs
@@ -0,0 +1,24 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Fetch;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionGitFetchV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionGitFetchV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = GitRepositoryCommand.Fetch,
+ ExecutionCausesSynchronizing = true,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Pull/RepositoryActionGitPullV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Pull/RepositoryActionGitPullV1.cs
new file mode 100644
index 00000000..a52e19de
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Pull/RepositoryActionGitPullV1.cs
@@ -0,0 +1,34 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Pull;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to execute a `git pull` command.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionGitPullV1 : IMenuAction, IName
+{
+ public const string TYPE_VALUE = "git-pull@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text("Pull")]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Pull/RepositoryActionGitPullV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Pull/RepositoryActionGitPullV1Mapper.cs
new file mode 100644
index 00000000..477810ae
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Pull/RepositoryActionGitPullV1Mapper.cs
@@ -0,0 +1,24 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Pull;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionGitPullV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionGitPullV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = GitRepositoryCommand.Pull,
+ ExecutionCausesSynchronizing = true,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Push/RepositoryActionGitPushV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Push/RepositoryActionGitPushV1.cs
new file mode 100644
index 00000000..07ba68da
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Push/RepositoryActionGitPushV1.cs
@@ -0,0 +1,34 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Push;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to execute a `git push` command.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionGitPushV1 : IMenuAction, IName
+{
+ public const string TYPE_VALUE = "git-push@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text("Push")]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Push/RepositoryActionGitPushV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Push/RepositoryActionGitPushV1Mapper.cs
new file mode 100644
index 00000000..ac79d079
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Git/Push/RepositoryActionGitPushV1Mapper.cs
@@ -0,0 +1,24 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Git.Push;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionGitPushV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionGitPushV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = GitRepositoryCommand.Push,
+ ExecutionCausesSynchronizing = true,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Ignore/RepositoryActionIgnoreV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Ignore/RepositoryActionIgnoreV1.cs
new file mode 100644
index 00000000..389cfdcb
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Ignore/RepositoryActionIgnoreV1.cs
@@ -0,0 +1,38 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Ignore;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to ignore the current repository. This repository will be added to the list of ignored repositories and will never show in RepoM.
+/// To undo this action, clear all ignored repositories or manually edit the ignored repositories file (when RepoM is not running).
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionIgnoreV1 : IMenuAction, IContext, IName
+{
+ public const string TYPE_VALUE = "ignore-repository@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text("Ignore")]
+ public Text Name { get; init; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ public Context? Context { get; set; }
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Ignore/RepositoryActionIgnoreV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Ignore/RepositoryActionIgnoreV1Mapper.cs
new file mode 100644
index 00000000..c28de478
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Ignore/RepositoryActionIgnoreV1Mapper.cs
@@ -0,0 +1,23 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Ignore;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionIgnoreV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionIgnoreV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = IgnoreRepositoryCommand.Instance,
+ ExecutionCausesSynchronizing = true,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs
new file mode 100644
index 00000000..71300bea
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs
@@ -0,0 +1,43 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.JustText;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Textual action to display some text in the action menu.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionJustTextV1 : IMenuAction, IContext
+{
+ public const string TYPE_VALUE = "just-text@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ /// Show the menu as enabled (clickable) or disabled.
+ ///
+ [Predicate(true)]
+ public Predicate Enabled { get; set; } = new ScribanPredicate();
+
+ ///
+ public Context? Context { get; set; }
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs
new file mode 100644
index 00000000..1d4afd1c
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs
@@ -0,0 +1,18 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.JustText;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+
+[UsedImplicitly]
+internal class RepositoryActionJustTextV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionJustTextV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var text = await action.Name.RenderAsync(context).ConfigureAwait(false);
+ yield return new UserInterfaceRepositoryAction(text, repository);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Pin/RepositoryActionPinV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Pin/RepositoryActionPinV1.cs
new file mode 100644
index 00000000..61b2637a
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Pin/RepositoryActionPinV1.cs
@@ -0,0 +1,64 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Pin;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to pin (or unpin) the current repository. Pinning is not persistant and all pinned repositories will be cleared when RepoM exits.
+/// Pinning a repository allowed custom filtering, ordering and searching.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionPinV1 : IMenuAction, IContext, IName
+{
+ public const string TYPE_VALUE = "pin-repository@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text("(Un)Pin repository")]
+ public Text Name { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ public Context? Context { get; set; }
+
+ ///
+ /// The pin mode `[Toggle, Pin, UnPin]`.
+ ///
+ public PinMode? Mode { get; set; } // GitHub issue: https://github.com/coenm/RepoM/issues/87
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Mode}";
+ }
+
+ ///
+ /// The PinModes
+ ///
+ public enum PinMode
+ {
+ ///
+ /// Toggle
+ ///
+ Toggle,
+
+ ///
+ /// Pin
+ ///
+ Pin,
+
+ ///
+ /// UnPin
+ ///
+ UnPin,
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Pin/RepositoryActionPinV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Pin/RepositoryActionPinV1Mapper.cs
new file mode 100644
index 00000000..b37e514a
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Pin/RepositoryActionPinV1Mapper.cs
@@ -0,0 +1,35 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Pin;
+
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionPinV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionPinV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var name = await action.Name.RenderAsync(context).ConfigureAwait(false);
+
+ yield return new UserInterfaceRepositoryAction(name, repository)
+ {
+ RepositoryCommand = CreatePinRepositoryCommand(action),
+ };
+ }
+
+ private static PinRepositoryCommand CreatePinRepositoryCommand(RepositoryActionPinV1 action)
+ {
+ return action.Mode switch
+ {
+ RepositoryActionPinV1.PinMode.Toggle => PinRepositoryCommand.Toggle,
+ RepositoryActionPinV1.PinMode.Pin => PinRepositoryCommand.Pin,
+ RepositoryActionPinV1.PinMode.UnPin => PinRepositoryCommand.UnPin,
+ _ => throw new ArgumentOutOfRangeException(action.Mode.ToString()),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Separator/RepositoryActionSeparatorV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Separator/RepositoryActionSeparatorV1.cs
new file mode 100644
index 00000000..07c43b70
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Separator/RepositoryActionSeparatorV1.cs
@@ -0,0 +1,33 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Separator;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Creates a visual separator in the action menu.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionSeparatorV1 : IMenuAction, IContext
+{
+ public const string TYPE_VALUE = "separator@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ public Context? Context { get; set; }
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE})";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Separator/RepositoryActionSeparatorV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Separator/RepositoryActionSeparatorV1Mapper.cs
new file mode 100644
index 00000000..fdb0418f
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Separator/RepositoryActionSeparatorV1Mapper.cs
@@ -0,0 +1,19 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Separator;
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+
+[UsedImplicitly]
+internal class RepositoryActionSeparatorV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionSeparatorV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ await Task.Yield();
+ yield return new UserInterfaceSeparatorRepositoryAction(repository);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Url/RepositoryActionUrlV1.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Url/RepositoryActionUrlV1.cs
new file mode 100644
index 00000000..68baa0f9
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Url/RepositoryActionUrlV1.cs
@@ -0,0 +1,43 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Url;
+
+using RepoM.ActionMenu.Core.Yaml.Model.Templating;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using RepoM.ActionMenu.Interface.YamlModel.Templating;
+
+///
+/// Action to open the url in the default browser.
+///
+[RepositoryAction(TYPE_VALUE)]
+internal sealed class RepositoryActionUrlV1 : IMenuAction, IName, IContext
+{
+ public const string TYPE_VALUE = "url@1";
+
+ public string Type
+ {
+ get => TYPE_VALUE;
+ set => _ = value;
+ }
+
+ ///
+ [Text]
+ public Text Name { get; set; } = null!;
+
+ ///
+ /// The URL to browse to.
+ ///
+ [Text]
+ public Text Url { get; set; } = null!;
+
+ ///
+ [Predicate(true)]
+ public Predicate Active { get; set; } = new ScribanPredicate();
+
+ ///
+ public Context? Context { get; set; }
+
+ public override string ToString()
+ {
+ return $"({TYPE_VALUE}) {Name}";
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Url/RepositoryActionUrlV1Mapper.cs b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Url/RepositoryActionUrlV1Mapper.cs
new file mode 100644
index 00000000..1b5719f8
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ActionMenu/Model/ActionMenus/Url/RepositoryActionUrlV1Mapper.cs
@@ -0,0 +1,23 @@
+namespace RepoM.ActionMenu.Core.ActionMenu.Model.ActionMenus.Url;
+
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.Repository;
+using RepoM.Core.Plugin.RepositoryActions.Commands;
+
+[UsedImplicitly]
+internal class RepositoryActionUrlV1Mapper : ActionToRepositoryActionMapperBase
+{
+ protected override async IAsyncEnumerable MapAsync(RepositoryActionUrlV1 action, IActionMenuGenerationContext context, IRepository repository)
+ {
+ var text = await action.Name.RenderAsync(context).ConfigureAwait(false);
+ var url = await action.Url.RenderAsync(context).ConfigureAwait(false);
+ yield return new UserInterfaceRepositoryAction(text, repository)
+ {
+ RepositoryCommand = new BrowseRepositoryCommand(url),
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/Bootstrapper.cs b/src/RepoM.ActionMenu.Core/Bootstrapper.cs
new file mode 100644
index 00000000..017ef592
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/Bootstrapper.cs
@@ -0,0 +1,119 @@
+namespace RepoM.ActionMenu.Core;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using RepoM.ActionMenu.Core.ConfigReader;
+using RepoM.ActionMenu.Core.Misc;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Core.Services;
+using RepoM.ActionMenu.Core.Yaml.Serialization;
+using RepoM.ActionMenu.Interface.Scriban;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.Core.Plugin.RepositoryOrdering.Configuration;
+using SimpleInjector;
+
+public static class Bootstrapper
+{
+ private static readonly Assembly _thisAssembly = typeof(Bootstrapper).Assembly;
+
+ public static void RegisterServices(Container container)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+ RegisterPrivateTypes(container);
+ RegisterPublicTypes(container);
+ }
+
+ private static void RegisterPublicTypes(Container container)
+ {
+ container.Register(Lifestyle.Singleton);
+ }
+
+ private static void RegisterPrivateTypes(Container container)
+ {
+ IEnumerable assemblyExportableTypes = GetExportedTypesFrom(_thisAssembly).ToArray();
+
+ container.Collection.Register(Array.Empty(), Lifestyle.Singleton);
+
+ IEnumerable types = assemblyExportableTypes
+ .Where(t => typeof(IActionToRepositoryActionMapper).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()))
+ .Where(t => t.GetTypeInfo() is { IsAbstract: false, IsGenericTypeDefinition: false, });
+ container.Collection.Register(types, Lifestyle.Singleton);
+
+ types = assemblyExportableTypes
+ .Where(t => typeof(IMenuAction).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()))
+ .Where(t => t.GetTypeInfo() is { IsAbstract: false, IsGenericTypeDefinition: false, });
+ foreach (Type type in types)
+ {
+ container.RegisterActionMenuType(type);
+ }
+
+ container.RegisterSingleton();
+ container.RegisterSingleton();
+
+ container.RegisterSingleton();
+ container.RegisterDecorator(Lifestyle.Singleton);
+ }
+
+ ///
+ /// The one and only public interface for this module.
+ ///
+ /// The container
+ /// Instance of .
+ public static IUserInterfaceActionMenuFactory GetUserInterfaceActionMenu(Container container)
+ {
+ return container.GetInstance();
+ }
+
+ private static void RegisterActionMenuType(this Container container, Type type)
+ {
+ container.Collection.AppendInstance>(new FixedTypeRegistration(type, TypeRepositoryActionAttributeReader.GetValue(type)));
+ }
+
+ static IEnumerable GetExportedTypesFrom(Assembly assembly)
+ {
+ try
+ {
+ return assembly.DefinedTypes.Select(info => info.AsType());
+ }
+ catch (NotSupportedException)
+ {
+ // A type load exception would typically happen on an Anonymously Hosted DynamicMethods
+ // Assembly and it would be safe to skip this exception.
+ return Enumerable.Empty();
+ }
+ }
+}
+
+[DebuggerDisplay($"{{{nameof(Tag)}}}")]
+file sealed class FixedTypeRegistration : IKeyTypeRegistration
+{
+ public FixedTypeRegistration(Type configurationType, string tag)
+ {
+ ConfigurationType = configurationType;
+
+ if (string.IsNullOrEmpty(tag))
+ {
+ throw new ArgumentNullException(nameof(tag));
+ }
+
+ Tag = tag;
+ }
+
+ public Type ConfigurationType { get; }
+
+ public string Tag { get; }
+}
+
+///
+/// This class 'assumes' that the type has a custom attribute of type with a property 'Type' that is the type value.
+///
+file static class TypeRepositoryActionAttributeReader
+{
+ public static string GetValue(Type type)
+ {
+ return type.GetCustomAttribute()?.Type ?? throw new InvalidOperationException($"RepositoryActionAttribute not found on {type.FullName}");
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ConfigReader/CacheFileReaderDecorator.cs b/src/RepoM.ActionMenu.Core/ConfigReader/CacheFileReaderDecorator.cs
new file mode 100644
index 00000000..49d11972
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ConfigReader/CacheFileReaderDecorator.cs
@@ -0,0 +1,93 @@
+namespace RepoM.ActionMenu.Core.ConfigReader;
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.Caching;
+using System.Threading.Tasks;
+using DotNetEnv;
+using JetBrains.Annotations;
+using RepoM.ActionMenu.Core.Yaml.Model;
+
+[UsedImplicitly]
+internal class CacheFileReaderDecorator : IFileReader
+{
+ private readonly IFileReader _decoratee;
+ private readonly FileStore> _envStore;
+ private readonly FileStore _contextRootStore;
+ private readonly FileStore _rootStore;
+
+ public CacheFileReaderDecorator(IFileReader decoratee)
+ {
+ var cache = new MemoryCache(nameof(CacheFileReaderDecorator));
+ _decoratee = decoratee ?? throw new ArgumentNullException(nameof(decoratee));
+ _envStore = new FileStore>(cache);
+ _contextRootStore = new FileStore(cache);
+ _rootStore = new FileStore(cache);
+ }
+
+ public async Task DeserializeRoot(string filename)
+ {
+ if (string.IsNullOrWhiteSpace(filename))
+ {
+ return null;
+ }
+
+ Root? result = _rootStore.Get(filename);
+ if (result != null)
+ {
+ return result;
+ }
+
+ result = await _decoratee.DeserializeRoot(filename).ConfigureAwait(false);
+ if (result == null)
+ {
+ return result;
+ }
+
+ return _rootStore.AddOrGetExisting(filename, result);
+ }
+
+ public async Task DeserializeContextRoot(string filename)
+ {
+ if (string.IsNullOrWhiteSpace(filename))
+ {
+ return null;
+ }
+
+ ContextRoot? result = _contextRootStore.Get(filename);
+ if (result != null)
+ {
+ return result;
+ }
+
+ result = await _decoratee.DeserializeContextRoot(filename).ConfigureAwait(false);
+ if (result == null)
+ {
+ return result;
+ }
+
+ return _contextRootStore.AddOrGetExisting(filename, result);
+ }
+
+ public async Task?> ReadEnvAsync(string filename)
+ {
+ if (string.IsNullOrWhiteSpace(filename))
+ {
+ return null;
+ }
+
+ IDictionary? result = _envStore.Get(filename);
+ if (result != null)
+ {
+ return result;
+ }
+
+ result = await _decoratee.ReadEnvAsync(filename).ConfigureAwait(false);
+ if (result == null)
+ {
+ return result;
+ }
+
+ return _envStore.AddOrGetExisting(filename, result.ToDictionary());
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ConfigReader/FileReader.cs b/src/RepoM.ActionMenu.Core/ConfigReader/FileReader.cs
new file mode 100644
index 00000000..df2235b5
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ConfigReader/FileReader.cs
@@ -0,0 +1,55 @@
+namespace RepoM.ActionMenu.Core.ConfigReader;
+
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.Threading.Tasks;
+using DotNetEnv;
+using RepoM.ActionMenu.Core.Model;
+using RepoM.ActionMenu.Core.Yaml.Model;
+
+internal class FileReader : IFileReader
+{
+ private static readonly LoadOptions _loadOptions = new (setEnvVars: false);
+ private readonly IFileSystem _fileSystem;
+ private readonly IActionMenuDeserializer _deserializer;
+
+ public FileReader(IFileSystem fileSystem, IActionMenuDeserializer deserializer)
+ {
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer));
+ }
+
+ public async Task DeserializeRoot(string filename)
+ {
+ if (!_fileSystem.File.Exists(filename))
+ {
+ return null;
+ }
+
+ var content = await _fileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false);
+ return _deserializer.DeserializeRoot(content);
+ }
+
+ public async Task DeserializeContextRoot(string filename)
+ {
+ if (!_fileSystem.File.Exists(filename))
+ {
+ return null;
+ }
+
+ var content = await _fileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false);
+ return _deserializer.DeserializeContextRoot(content);
+ }
+
+ public async Task?> ReadEnvAsync(string filename)
+ {
+ if (!_fileSystem.File.Exists(filename))
+ {
+ return null;
+ }
+
+ var content = await _fileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false);
+ return Env.LoadContents(content, _loadOptions).ToDictionary();
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ConfigReader/FileStore.cs b/src/RepoM.ActionMenu.Core/ConfigReader/FileStore.cs
new file mode 100644
index 00000000..0bbd7053
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ConfigReader/FileStore.cs
@@ -0,0 +1,43 @@
+namespace RepoM.ActionMenu.Core.ConfigReader;
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.Caching;
+
+internal class FileStore where T : class
+{
+ private readonly ObjectCache _cache;
+
+ internal FileStore(ObjectCache cache)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ }
+
+ public T? Get(string filename)
+ {
+ if (_cache[filename] is T fileContents)
+ {
+ return fileContents;
+ }
+
+ return null;
+ }
+
+ internal T AddOrGetExisting(string filename, T value)
+ {
+ var policy = new CacheItemPolicy();
+ policy.ChangeMonitors.Add(CreateMonitorForFilename(filename));
+ var cacheResult = _cache.AddOrGetExisting(filename, value, policy) as T;
+ return cacheResult ?? value;
+ }
+
+ private static HostFileChangeMonitor CreateMonitorForFilename(string filename)
+ {
+ // GitHub issue: https://github.com/coenm/RepoM/issues/88
+ // the HostFileChangeMonitor relies on a real file system, not the IFileSystem used in RepoM.
+ // also the HostFIleChangeMontor implements IDisposable.
+ // not sure if the ObjectCache takes care of that.
+ var filePaths = new List(1) { filename, };
+ return new HostFileChangeMonitor(filePaths);
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/ConfigReader/IFileReader.cs b/src/RepoM.ActionMenu.Core/ConfigReader/IFileReader.cs
new file mode 100644
index 00000000..3c90f2e9
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/ConfigReader/IFileReader.cs
@@ -0,0 +1,14 @@
+namespace RepoM.ActionMenu.Core.ConfigReader;
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using RepoM.ActionMenu.Core.Yaml.Model;
+
+internal interface IFileReader
+{
+ Task DeserializeRoot(string filename);
+
+ Task DeserializeContextRoot(string filename);
+
+ Task?> ReadEnvAsync(string filename);
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/IUserInterfaceActionMenuFactory.cs b/src/RepoM.ActionMenu.Core/IUserInterfaceActionMenuFactory.cs
new file mode 100644
index 00000000..867fc5d1
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/IUserInterfaceActionMenuFactory.cs
@@ -0,0 +1,28 @@
+namespace RepoM.ActionMenu.Core;
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.Core.Plugin.Repository;
+
+public interface IUserInterfaceActionMenuFactory
+{
+ IAsyncEnumerable CreateMenuAsync(IRepository repository, string filename);
+
+ Task> GetTagsAsync(IRepository repository, string filename);
+}
+
+public static class UserInterfaceActionMenuFactoryExtensions
+{
+ public static async Task> CreateMenuListAsync(this IUserInterfaceActionMenuFactory instance, IRepository repository, string filename)
+ {
+ var result = new List();
+
+ await foreach (UserInterfaceRepositoryActionBase item in instance.CreateMenuAsync(repository, filename).ConfigureAwait(false))
+ {
+ result.Add(item);
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/InternalsVisisbleTo.cs b/src/RepoM.ActionMenu.Core/InternalsVisisbleTo.cs
new file mode 100644
index 00000000..711bde6f
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/InternalsVisisbleTo.cs
@@ -0,0 +1 @@
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("RepoM.ActionMenu.Core.TestLib")]
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/Misc/FastStack.cs b/src/RepoM.ActionMenu.Core/Misc/FastStack.cs
new file mode 100644
index 00000000..f9f1ea9d
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/Misc/FastStack.cs
@@ -0,0 +1,92 @@
+namespace RepoM.ActionMenu.Core.Misc;
+
+using System;
+using System.Runtime.CompilerServices;
+
+///
+/// Lightweight stack object.
+///
+/// Type of the object
+/// Copied from Scriban
+internal struct FastStack
+{
+ private const int DEFAULT_CAPACITY = 4;
+ private T[] _array; // Storage for stack elements.
+
+ // Create a stack with a specific initial capacity. The initial capacity
+ // must be a non-negative number.
+ public FastStack(int capacity)
+ {
+ if (capacity <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be > 0");
+ }
+
+ _array = new T[capacity];
+ Count = 0;
+ }
+
+ ///
+ /// Number of items in the stack
+ ///
+ public int Count { get; private set; }
+
+ public readonly T[] Items => _array;
+
+ ///
+ /// Removes all Objects from the Stack.
+ ///
+ public void Clear()
+ {
+ // Don't need to doc this, but we clear the elements so that the gc can reclaim the references.
+ Array.Clear(_array, 0, Count);
+ Count = 0;
+ }
+
+ ///
+ /// Peeks the last added element of the stack.
+ ///
+ /// Thrown when stack is empty.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly T Peek()
+ {
+ ThrowForEmptyStack();
+ return _array[Count - 1];
+ }
+
+ ///
+ /// Pops an item from the top of the stack.
+ ///
+ /// Thrown when stack is empty.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T Pop()
+ {
+ ThrowForEmptyStack();
+ T item = _array[--Count];
+ _array[Count] = default!; // Free memory quicker.
+ return item;
+ }
+
+ ///
+ /// Pushes an item to the top of the stack.
+ ///
+ /// Item to add.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Push(T item)
+ {
+ if (Count == _array.Length)
+ {
+ Array.Resize(ref _array, _array.Length == 0 ? DEFAULT_CAPACITY : 2 * _array.Length);
+ }
+
+ _array[Count++] = item;
+ }
+
+ private readonly void ThrowForEmptyStack()
+ {
+ if (Count == 0)
+ {
+ throw new InvalidOperationException("Stack is empty");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs b/src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs
new file mode 100644
index 00000000..9cff7c53
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs
@@ -0,0 +1,30 @@
+namespace RepoM.ActionMenu.Core.Misc;
+
+using System;
+using RepoM.ActionMenu.Core.Model;
+using Scriban;
+
+internal class FixedTemplateParser : ITemplateParser
+{
+ public Template ParseScriptOnly(string text)
+ {
+ var template = Template.Parse(text, sourceFilePath: null!, DefaultLexerAndParserOptions.DefaultParserOptions, DefaultLexerAndParserOptions.ScriptOnlyLexer);
+ ThrowOnError(template);
+ return template;
+ }
+
+ public Template ParseMixed(string text)
+ {
+ var template = Template.Parse(text, sourceFilePath: null!, DefaultLexerAndParserOptions.DefaultParserOptions, DefaultLexerAndParserOptions.MixedLexer);
+ ThrowOnError(template);
+ return template;
+ }
+
+ private static void ThrowOnError(Template template)
+ {
+ if (template.HasErrors)
+ {
+ throw new Exception($"Template has errors {template.Messages}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs b/src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs
new file mode 100644
index 00000000..8af338b2
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs
@@ -0,0 +1,10 @@
+namespace RepoM.ActionMenu.Core.Misc;
+
+using Scriban;
+
+internal interface ITemplateParser
+{
+ Template ParseScriptOnly(string text);
+
+ Template ParseMixed(string text);
+}
\ No newline at end of file
diff --git a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs
new file mode 100644
index 00000000..f385ab32
--- /dev/null
+++ b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs
@@ -0,0 +1,234 @@
+namespace RepoM.ActionMenu.Core.Model;
+
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Threading.Tasks;
+using RepoM.ActionMenu.Core.ActionMenu.Context;
+using RepoM.ActionMenu.Core.Misc;
+using RepoM.ActionMenu.Core.Yaml.Model.ActionContext;
+using RepoM.ActionMenu.Core.Yaml.Model.Tags;
+using RepoM.ActionMenu.Interface.ActionMenuFactory;
+using RepoM.ActionMenu.Interface.Scriban;
+using RepoM.ActionMenu.Interface.UserInterface;
+using RepoM.ActionMenu.Interface.YamlModel;
+using RepoM.ActionMenu.Interface.YamlModel.ActionMenus;
+using Scriban;
+using FileFunctions = RepoM.ActionMenu.Core.ActionMenu.Context.FileFunctions;
+using IRepository = RepoM.Core.Plugin.Repository.IRepository;
+using RepositoryFunctions = RepoM.ActionMenu.Core.ActionMenu.Context.RepositoryFunctions;
+
+internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerationContext, IContextMenuActionMenuGenerationContext
+{
+ private readonly ITemplateParser _templateParser;
+ private readonly ITemplateContextRegistration[] _functionsArray;
+ private readonly IActionMenuDeserializer _deserializer;
+ private readonly IActionToRepositoryActionMapper[] _repositoryActionMappers;
+ private readonly IContextActionProcessor[] _contextActionMappers;
+ private RepoMScriptObject _rootScriptObject = null!; // used for cloning.
+
+ public ActionMenuGenerationContext(
+ ITemplateParser templateParser,
+ IFileSystem fileSystem,
+ ITemplateContextRegistration[] functionsArray,
+ IActionToRepositoryActionMapper[] repositoryActionMappers,
+ IActionMenuDeserializer deserializer,
+ IContextActionProcessor[] contextActionMappers)
+ {
+ _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser));
+ _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));
+ }
+
+ public IFileSystem FileSystem { get; }
+
+ public DisposableContextScriptObject RepositoryActionsScriptContext { get; private set; } = null!;
+
+ public IRepository Repository { get; private set; } = null!;
+
+ private EnvSetScriptObject? _env;
+
+ public EnvSetScriptObject Env => _env ??= (EnvSetScriptObject)_rootScriptObject["env"];
+
+ public async Task AddRepositoryContextAsync(Context? reposContext)
+ {
+ if (reposContext == null)
+ {
+ return;
+ }
+
+ foreach (IContextAction contextAction in reposContext)
+ {
+ await RepositoryActionsScriptContext.AddContextActionAsync(contextAction).ConfigureAwait(false);
+ }
+ }
+
+ public async IAsyncEnumerable AddActionMenusAsync(List? menus)
+ {
+ if (menus == null)
+ {
+ yield break;
+ }
+
+ using IScope disposable = CreateGlobalScope();
+
+ foreach (IMenuAction action in menus)
+ {
+ foreach (UserInterfaceRepositoryActionBase item in await AddMenuActionAsync(action).ConfigureAwait(false))
+ {
+ yield return item;
+ }
+ }
+ }
+
+ public IActionMenuGenerationContext Clone()
+ {
+#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);
+
+ IScriptObject e = ((EnvSetScriptObject)_rootScriptObject["env"]).Clone(true);
+ repoMScriptObject.SetValue("env", e, false);
+ */
+#pragma warning restore S125
+
+ var result = new ActionMenuGenerationContext(
+ _templateParser,
+ FileSystem,
+ _functionsArray,
+ _repositoryActionMappers,
+ _deserializer,
+ _contextActionMappers);
+
+ result.Initialize(Repository);
+
+ return result;
+ }
+
+ internal void Initialize(IRepository repository)
+ {
+ Repository = repository ?? throw new ArgumentNullException(nameof(repository));
+
+ _rootScriptObject = CreateAndInitRepoMScriptObject(Repository);
+
+ foreach (ITemplateContextRegistration contextRegistration in _functionsArray)
+ {
+ contextRegistration.RegisterFunctions(Decorate(_rootScriptObject));
+ }
+
+ PushGlobal(_rootScriptObject);
+ RepositoryActionsScriptContext = new DisposableContextScriptObject(this, Env, _contextActionMappers);
+ PushGlobal(RepositoryActionsScriptContext);
+ }
+
+ private static RepoMScriptObject CreateAndInitRepoMScriptObject(IRepository repository)
+ {
+ var scriptObj = new RepoMScriptObject();
+
+ scriptObj.SetValue("file", new FileFunctions(), true);
+ scriptObj.SetValue("repository", new RepositoryFunctions(repository), true);
+
+ scriptObj.Add("env", new EnvSetScriptObject(EnvScriptObject.Create()));
+ scriptObj.SetReadOnly("env", false); // this is not what we want, but it's the only way to make it work
+
+ return scriptObj;
+ }
+
+ private async Task> AddMenuActionAsync(IMenuAction menuAction)
+ {
+ if (!await IsMenuItemActiveAsync(menuAction).ConfigureAwait(false))
+ {
+ return Array.Empty();
+ }
+
+ using DisposableContextScriptObject variableContext = PushNewContext();
+
+ if (menuAction is IContext { Context: not null, } c)
+ {
+ foreach (IContextAction ctx in c.Context)
+ {
+ await variableContext.AddContextActionAsync(ctx).ConfigureAwait(false);
+ }
+ }
+
+ IActionToRepositoryActionMapper? mapper = Array.Find(_repositoryActionMappers, mapper => mapper.CanMap(menuAction));
+ if (mapper == null)
+ {
+ // throw?
+ return Array.Empty();
+ }
+
+ var items = new List();
+ await foreach (UserInterfaceRepositoryActionBase item in mapper.MapAsync(menuAction, this, Repository).ConfigureAwait(false))
+ {
+ items.Add(item);
+ }
+
+ return items;
+ }
+
+ public async Task RenderStringAsync(string text)
+ {
+ Template template = _templateParser.ParseMixed(text);
+ return await template.RenderAsync(this).ConfigureAwait(false);
+ }
+
+ private DisposableContextScriptObject PushNewContext()
+ {
+ return new DisposableContextScriptObject(this, Env, _contextActionMappers);
+ }
+
+ public async Task