From 9dd2e3ff5aff21daec2dc2c380d25f577c706749 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 22 Jun 2026 09:57:00 +0000
Subject: [PATCH 1/3] Initial plan
From d022a423f6433f98430629be29cfd4a08d67d0bf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 22 Jun 2026 10:09:30 +0000
Subject: [PATCH 2/3] Add Set Top Entity context menu and top entity indicator
Co-authored-by: hendrikmennen <25281882+hendrikmennen@users.noreply.github.com>
---
docs/PluginDevelopment.md | 4 +-
.../Models/UniversalProjectRoot.cs | 22 +++++
.../Models/UniversalFpgaProjectRoot.cs | 29 +++++++
.../UniversalFpgaProjectManager.cs | 81 ++++++++++++++++++-
4 files changed, 132 insertions(+), 4 deletions(-)
diff --git a/docs/PluginDevelopment.md b/docs/PluginDevelopment.md
index 0a6d7127b..689ed8413 100644
--- a/docs/PluginDevelopment.md
+++ b/docs/PluginDevelopment.md
@@ -390,7 +390,7 @@ model for FPGA workflows. It is designed to be extended by plugins.
- Project files use JSON via `UniversalProjectProperties`. Keys are case-insensitive and stored in
the project file. Common keys:
- `include` / `exclude`: arrays of glob-like patterns used by `IsPathIncluded`.
- - `topEntity`: name of the top-level entity or module within `topEntityFile`. Old files that stored a file path in `topEntity` are migrated automatically on load.
+ - `topEntity`: name of the top-level entity or module. The file that contains it is resolved by scanning the project's HDL files (see `FpgaService.GetAllTopEntitiesAsync`). It can be set from the project settings or via the **Set Top Entity** context menu entry on an HDL file in the project explorer. Old files that stored a file path in `topEntity` are migrated automatically on load.
- `toolchain`: toolchain ID to run on compile.
- `loader`: loader ID to use for programming.
- `board`: selected hardware board (evaluation board) name. The legacy key `fpga` is automatically migrated to `board` on load.
@@ -399,7 +399,7 @@ model for FPGA workflows. It is designed to be extended by plugins.
`UniversalFpgaProjectRoot` also registers project entry modification handlers to update:
- Test bench overlays.
-- Top entity overlays.
+- Top entity overlays (marks the file that contains the current `topEntity`).
- Reduced opacity for compile-excluded files.
Project-specific files:
diff --git a/src/OneWare.ProjectSystem/Models/UniversalProjectRoot.cs b/src/OneWare.ProjectSystem/Models/UniversalProjectRoot.cs
index 1d001f3a4..d5447f427 100644
--- a/src/OneWare.ProjectSystem/Models/UniversalProjectRoot.cs
+++ b/src/OneWare.ProjectSystem/Models/UniversalProjectRoot.cs
@@ -54,6 +54,28 @@ public void InvalidateModifications(IProjectEntry entry)
_entryModificationHandlers.ForEach(handler => handler(entry));
}
+ ///
+ /// Re-runs all registered entry modifications for every loaded entry in the project.
+ /// Useful when a project wide state (e.g. the top entity) changes and the affected
+ /// entries are not known upfront.
+ ///
+ public void InvalidateAllModifications()
+ {
+ foreach (var entry in EnumerateLoadedEntries(this))
+ InvalidateModifications(entry);
+ }
+
+ private static IEnumerable EnumerateLoadedEntries(IProjectExplorerNode node)
+ {
+ if (node.Children == null) yield break;
+
+ foreach (var child in node.Children)
+ {
+ if (child is IProjectEntry entry) yield return entry;
+ foreach (var descendant in EnumerateLoadedEntries(child)) yield return descendant;
+ }
+ }
+
public Task LoadAsync(IEnumerable? migrations = null)
{
return Properties.LoadAsync(ProjectFilePath, migrations);
diff --git a/src/OneWare.UniversalFpgaProjectSystem/Models/UniversalFpgaProjectRoot.cs b/src/OneWare.UniversalFpgaProjectSystem/Models/UniversalFpgaProjectRoot.cs
index f4adc0483..82aa280eb 100644
--- a/src/OneWare.UniversalFpgaProjectSystem/Models/UniversalFpgaProjectRoot.cs
+++ b/src/OneWare.UniversalFpgaProjectSystem/Models/UniversalFpgaProjectRoot.cs
@@ -34,6 +34,19 @@ public UniversalFpgaProjectRoot(string projectFilePath) : base(projectFilePath)
x.Icon?.RemoveOverlay("TestBench");
}
});
+
+ RegisterProjectEntryModification(x =>
+ {
+ if (x is IProjectFile file && TopEntityFilePath != null &&
+ file.RelativePath.EqualPaths(TopEntityFilePath))
+ {
+ x.Icon?.AddOverlay("TopEntity", "VsImageLib2019.DownloadOverlay16X");
+ }
+ else
+ {
+ x.Icon?.RemoveOverlay("TopEntity");
+ }
+ });
RegisterProjectEntryModification(x =>
{
@@ -60,6 +73,22 @@ public string? TopEntity
set => Properties.SetString("topEntity", value);
}
+ ///
+ /// Relative path of the file that contains the current .
+ /// This is a runtime-only cache (not persisted) and is used to display the top entity
+ /// indicator on the file that contains the top-level entity.
+ ///
+ public string? TopEntityFilePath
+ {
+ get;
+ set
+ {
+ if (field == value) return;
+ field = value;
+ InvalidateAllModifications();
+ }
+ }
+
public string? Toolchain
{
get => Properties.GetString("toolchain");
diff --git a/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs b/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs
index 7cef45ebb..25e931398 100644
--- a/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs
+++ b/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs
@@ -1,4 +1,6 @@
-using CommunityToolkit.Mvvm.Input;
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.Input;
+using OneWare.Essentials.Extensions;
using OneWare.Essentials.Models;
using OneWare.Essentials.Services;
using OneWare.UniversalFpgaProjectSystem.Models;
@@ -43,7 +45,15 @@ public UniversalFpgaProjectManager(IProjectExplorerService projectExplorerServic
}
await root.InitializeAsync();
-
+
+ await UpdateTopEntityFilePathAsync(root);
+
+ root.Properties.ProjectPropertyChanged += (_, args) =>
+ {
+ if (args.PropertyName.Equals("topEntity", StringComparison.OrdinalIgnoreCase))
+ _ = UpdateTopEntityFilePathAsync(root);
+ };
+
return root;
}
@@ -54,6 +64,8 @@ public async Task ReloadProjectAsync(IProjectRoot project)
await root.LoadAsync(_fpgaService.ProjectPropertyMigrations);
await MigrateLegacyTopEntityAsync(root);
await root.InitializeAsync();
+
+ await UpdateTopEntityFilePathAsync(root);
//TODO reload open files
var filesOpenInProject = _mainDockService.OpenFiles
@@ -183,6 +195,15 @@ private void ConstructContextMenu(IReadOnlyList selected,
case FpgaProjectFile { Root: UniversalFpgaProjectRoot universalFpgaProjectRoot } file:
if (file.Extension is ".vhd" or ".vhdl" or ".v" or ".sv")
{
+ //Set Top Entity
+ var setTopEntityMenu = new MenuItemModel("SetTopEntity")
+ {
+ Header = "Set Top Entity",
+ Icon = new IconModel("VsImageLib2019.DownloadOverlay16X"),
+ Items = new ObservableCollection()
+ };
+ menuItems.Add(setTopEntityMenu);
+ _ = PopulateTopEntityMenuItemsAsync(setTopEntityMenu, file, universalFpgaProjectRoot);
//Exclude from compile
if (!universalFpgaProjectRoot.IsCompileExcluded(file.RelativePath))
@@ -235,6 +256,62 @@ private void ConstructContextMenu(IReadOnlyList selected,
}
}
+ private async Task PopulateTopEntityMenuItemsAsync(MenuItemModel menu, IProjectFile file,
+ UniversalFpgaProjectRoot root)
+ {
+ var nodeProvider = _fpgaService.GetNodeProviderByExtension(file.Extension);
+ if (nodeProvider == null) return;
+
+ List entities;
+ try
+ {
+ entities = (await nodeProvider.ExtractTopEntitiesAsync(file)).ToList();
+ }
+ catch
+ {
+ return;
+ }
+
+ if (menu.Items == null) return;
+
+ foreach (var entity in entities)
+ {
+ var entityName = entity;
+ var isCurrent = root.TopEntity == entityName &&
+ root.TopEntityFilePath != null &&
+ file.RelativePath.EqualPaths(root.TopEntityFilePath);
+
+ menu.Items.Add(new MenuItemModel(entityName)
+ {
+ Header = entityName,
+ Icon = isCurrent ? new IconModel("BoxIcons.RegularCheck") : null,
+ Command = new RelayCommand(() =>
+ {
+ root.TopEntity = entityName;
+ root.TopEntityFilePath = file.RelativePath;
+ _ = SaveProjectAsync(root);
+ })
+ });
+ }
+ }
+
+ ///
+ /// Recomputes by locating the file
+ /// that contains the current .
+ ///
+ private async Task UpdateTopEntityFilePathAsync(UniversalFpgaProjectRoot root)
+ {
+ if (string.IsNullOrEmpty(root.TopEntity))
+ {
+ root.TopEntityFilePath = null;
+ return;
+ }
+
+ var allEntities = await _fpgaService.GetAllTopEntitiesAsync(root);
+ var match = allEntities.FirstOrDefault(x => x.TopEntity == root.TopEntity);
+ root.TopEntityFilePath = match?.File.RelativePath;
+ }
+
private async Task OpenProjectSettingsDialogAsync(UniversalFpgaProjectRoot root)
{
// UniversalFpgaProjectRoot root
From ee70862f6669e2b8941ad9c7f40e26de1f123d32 Mon Sep 17 00:00:00 2001
From: Hendrik Mennen
Date: Wed, 24 Jun 2026 11:26:56 +0200
Subject: [PATCH 3/3] change icon
---
.../UniversalFpgaProjectManager.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs b/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs
index 25e931398..952fd6979 100644
--- a/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs
+++ b/src/OneWare.UniversalFpgaProjectSystem/UniversalFpgaProjectManager.cs
@@ -199,7 +199,7 @@ private void ConstructContextMenu(IReadOnlyList selected,
var setTopEntityMenu = new MenuItemModel("SetTopEntity")
{
Header = "Set Top Entity",
- Icon = new IconModel("VsImageLib2019.DownloadOverlay16X"),
+ Icon = new IconModel("VsImageLib.StartWithoutDebug16X"),
Items = new ObservableCollection()
};
menuItems.Add(setTopEntityMenu);