diff --git a/docs/PluginDevelopment.md b/docs/PluginDevelopment.md
index 0a6d7127..689ed841 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 1d001f3a..d5447f42 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 f4adc048..82aa280e 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 7cef45eb..952fd697 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("VsImageLib.StartWithoutDebug16X"),
+ 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