Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/PluginDevelopment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions src/OneWare.ProjectSystem/Models/UniversalProjectRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,28 @@ public void InvalidateModifications(IProjectEntry entry)
_entryModificationHandlers.ForEach(handler => handler(entry));
}

/// <summary>
/// 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.
/// </summary>
public void InvalidateAllModifications()
{
foreach (var entry in EnumerateLoadedEntries(this))
InvalidateModifications(entry);
}

private static IEnumerable<IProjectEntry> 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<bool> LoadAsync(IEnumerable<ProjectPropertyMigration>? migrations = null)
{
return Properties.LoadAsync(ProjectFilePath, migrations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
{
Expand All @@ -60,6 +73,22 @@ public string? TopEntity
set => Properties.SetString("topEntity", value);
}

/// <summary>
/// Relative path of the file that contains the current <see cref="TopEntity"/>.
/// 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.
/// </summary>
public string? TopEntityFilePath
{
get;
set
{
if (field == value) return;
field = value;
InvalidateAllModifications();
}
}

public string? Toolchain
{
get => Properties.GetString("toolchain");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -183,6 +195,15 @@ private void ConstructContextMenu(IReadOnlyList<IProjectExplorerNode> 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<MenuItemModel>()
};
menuItems.Add(setTopEntityMenu);
_ = PopulateTopEntityMenuItemsAsync(setTopEntityMenu, file, universalFpgaProjectRoot);

//Exclude from compile
if (!universalFpgaProjectRoot.IsCompileExcluded(file.RelativePath))
Expand Down Expand Up @@ -235,6 +256,62 @@ private void ConstructContextMenu(IReadOnlyList<IProjectExplorerNode> selected,
}
}

private async Task PopulateTopEntityMenuItemsAsync(MenuItemModel menu, IProjectFile file,
UniversalFpgaProjectRoot root)
{
var nodeProvider = _fpgaService.GetNodeProviderByExtension(file.Extension);
if (nodeProvider == null) return;

List<string> 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);
})
});
}
}

/// <summary>
/// Recomputes <see cref="UniversalFpgaProjectRoot.TopEntityFilePath"/> by locating the file
/// that contains the current <see cref="UniversalFpgaProjectRoot.TopEntity"/>.
/// </summary>
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
Expand Down
Loading