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