#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.IO;
using T3.Core.Model;
using T3.Core.Operator;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Helpers;
namespace T3.Editor.Gui.Dialogs;
internal sealed partial class DeleteSymbolDialog
{
///
/// Attempts to retrieve a restriction reason for the given symbol, such as library membership or read-only status.
/// Returns and sets to a non-null description if restricted;
/// otherwise returns and sets to .
///
/// The symbol to check for restrictions.
/// Symbol information containing library operator flags.
///
/// When this method returns , contains the restriction reason (non-null).
/// When , set to .
///
///
/// if the symbol has a restriction; otherwise .
///
private static bool TryGetRestriction(Symbol symbol, LocalSymbolInfo info, [NotNullWhen(true)] out string? restriction)
{
#if DEBUG
// In debug mode, always allow deletion but show restriction info and debug mode status
var restrictionReason = info.OperatorType switch
{
SymbolAnalysis.OperatorClassification.Lib => "part of the Main Library",
SymbolAnalysis.OperatorClassification.Type => "part of the Types Library",
SymbolAnalysis.OperatorClassification.Example => "part of the Examples Library",
SymbolAnalysis.OperatorClassification.T3 => "part of the T3 Library",
SymbolAnalysis.OperatorClassification.Skill => "part of the Skills Library",
_ when IsNamespaceMainSymbol(symbol) => "the main symbol of this namespace and attached to the project",
_ when symbol.SymbolPackage.IsReadOnly => "Read Only",
_ => null
};
if (restrictionReason != null)
{
restriction = $"[DEBUG MODE] Would restrict deletion: {restrictionReason}";
}
else
{
restriction = "[DEBUG MODE] No restrictions apply.";
}
return false; // Never restrict in debug mode
#else
restriction = info.OperatorType switch
{
SymbolAnalysis.OperatorClassification.Lib => "part of the Main Library",
SymbolAnalysis.OperatorClassification.Type => "part of the Types Library",
SymbolAnalysis.OperatorClassification.Example => "part of the Examples Library",
SymbolAnalysis.OperatorClassification.T3 => "part of the T3 Library",
SymbolAnalysis.OperatorClassification.Skill => "part of the Skills Library",
_ when IsNamespaceMainSymbol(symbol) => "the main symbol of this namespace and attached to the project",
_ when symbol.SymbolPackage.IsReadOnly => "Read Only",
_ => null
};
return restriction != null;
#endif
}
///
/// Checks if the symbol acts as the main symbol for its namespace, which binds it
/// to the project namespace and prevents regular deletion.
///
/// The symbol to inspect.
///
/// true if the symbol name matches the last segment of its namespace;
/// otherwise, false.
///
private static bool IsNamespaceMainSymbol(Symbol symbol)
{
var ns = symbol.Namespace ?? string.Empty;
if (string.IsNullOrEmpty(ns))
return false;
var lastDotIndex = ns.LastIndexOf('.');
var lastSegment = lastDotIndex >= 0 ? ns[(lastDotIndex + 1)..] : ns;
return string.Equals(lastSegment, symbol.Name, StringComparison.Ordinal);
}
// Deletion helpers
#region Symbol Deletion Helpers
///
/// Unified symbol deletion: optionally cleans all usages in depending symbols,
/// deletes the symbol files on disk, and triggers project reload/recompile for affected projects.
///
/// The symbol to delete.
///
/// Optional set of symbol IDs that reference and should be cleaned up
/// before deletion, or null if there are no known usages.
///
///
/// On failure, receives an explanation why deletion did not complete.
/// Empty if the operation succeeded.
///
/// true if the symbol and its usages were deleted successfully; otherwise, false.
private static bool DeleteSymbol(Symbol symbol, HashSet? dependingSymbols, out string reason)
{
if (dependingSymbols is { Count: > 0 })
{
if (!CleanUsages(symbol, dependingSymbols, out reason))
return false;
}
if (!DeleteSymbolFiles(symbol, out reason))
return false;
ForceReloadSymbolLibrary(dependingSymbols);
return true;
}
///
/// Removes all physical files that define the given symbol (code, graph, and UI files),
/// and clears build artifacts (bin/obj) for the containing project.
///
/// The symbol whose backing files should be removed.
///
/// On failure, receives a detailed error message describing what went wrong;
/// empty if the deletion completed successfully.
///
/// true if all relevant files and directories were deleted; otherwise, false.
private static bool DeleteSymbolFiles(Symbol symbol, out string reason)
{
if (!symbol.TryGetSymbolUi(out var symbolUi))
{
reason = $"Could not find SymbolUi for symbol '{symbol.Name}'";
return false;
}
if (symbolUi.Symbol.SymbolPackage.IsReadOnly)
{
reason = $"Could not delete [{symbol.Name}] because its package is not modifiable";
return false;
}
try
{
// Build folder structure from RootNamespace and Symbol.Namespace
var rootNs = symbol.SymbolPackage.RootNamespace;
var symbolNs = symbol.Namespace;
var project = (EditableSymbolProject)symbol.SymbolPackage;
var csPath = SymbolPathHandler.GetCorrectSourceCodePath(symbol.Name, symbolNs, project);
var t3Path = SymbolPathHandler.GetCorrectPath(symbol.Name, symbolNs, project.Folder, project.CsProjectFile.RootNamespace, SymbolPackage.SymbolExtension);
var t3UiPath = SymbolPathHandler.GetCorrectPath(symbol.Name, symbolNs, project.Folder, project.CsProjectFile.RootNamespace, EditorSymbolPackage.SymbolUiExtension);
var paths = new[] { csPath, t3Path, t3UiPath };
if (!File.Exists(csPath))
{
reason = $"""
Could not locate the source file for symbol [{symbol.Name}]
RootNamespace: [{rootNs}]
Namespace: [{symbolNs}]
Expected at: {csPath}
""";
return false;
}
var projectRoot = Path.GetDirectoryName(symbol.SymbolPackage.Folder);
if (string.IsNullOrEmpty(projectRoot))
{
reason = $"Could not determine the directory for symbol [{symbol.Name}]";
return false;
}
foreach (var path in paths)
{
if (!File.Exists(path)) continue;
File.Delete(path);
Log.Debug($"Deleted: '{path}'");
}
reason = string.Empty;
return true;
}
catch (IOException ioEx)
{
reason = $"I/O error while deleting symbol [{symbol.Name}]: {ioEx.Message}";
return false;
}
catch (UnauthorizedAccessException authEx)
{
reason = $"Access denied while deleting symbol [{symbol.Name}]: {authEx.Message}";
return false;
}
catch (Exception ex)
{
reason = $"Unexpected error while deleting symbol [{symbol.Name}]: {ex.Message}";
return false;
}
}
///
/// Disconnects and removes all child instances in depending symbols that reference
/// the symbol being deleted, without touching runtime instances directly.
///
/// The symbol whose usages should be removed.
///
/// The set of symbol IDs that are known to contain children referencing .
///
///
/// On failure, receives a description of the first encountered error; empty if the clean-up succeeded.
///
/// true if all known usages were cleaned successfully; otherwise, false.
private static bool CleanUsages(Symbol symbolToDelete,
HashSet dependingSymbols,
out string reason)
{
foreach (var dependingId in dependingSymbols)
{
if (!SymbolRegistry.TryGetSymbol(dependingId, out var dependingSymbol))
{
Log.Warning($"Could not find depending symbol {dependingId} while cleaning usages of {symbolToDelete.Name}");
continue;
}
// Children that use the symbol we are deleting
var childrenUsingSymbol = dependingSymbol.Children
.Where(kvp => kvp.Value.Symbol == symbolToDelete)
.Select(kvp => kvp.Value)
.ToList();
if (childrenUsingSymbol.Count == 0)
continue;
// Remove all connections to/from those children
foreach (var child in childrenUsingSymbol)
{
var childId = child.Id;
var connections = dependingSymbol.Connections.ToList();
foreach (var c in connections)
{
if (c.SourceParentOrChildId == childId ||
c.TargetParentOrChildId == childId)
{
dependingSymbol.RemoveConnection(c);
}
}
}
// Remove the children themselves → this clears instances via RemoveChild()
foreach (var child in childrenUsingSymbol)
{
dependingSymbol.RemoveChild(child.Id);
}
Log.Debug($"Disconnected and removed {childrenUsingSymbol.Count} usages of [{symbolToDelete.Name}] from [{dependingSymbol.Name}]");
}
reason = string.Empty;
return true;
}
///
/// Marks all affected symbol projects (the deleted symbol's project and any projects
/// containing depending symbols) as externally modified so that they are rebuilt and saved.
///
///
/// Optional set of symbol IDs that referenced the deleted symbol and belong to additional
/// projects that must be marked dirty, or null if there were no dependencies.
///
private static void ForceReloadSymbolLibrary(HashSet? dependingSymbolIds)
{
if (_symbol == null)
return;
var package = _symbol.SymbolPackage;
var affectedProjectIds = new HashSet();
// Always include the deleted symbol's own project
var ownProject = EditableSymbolProject.AllProjects
.FirstOrDefault(p => p.CsProjectFile.RootNamespace == package.RootNamespace);
if (ownProject != null)
{
affectedProjectIds.Add(ownProject);
}
// Include projects containing depending symbols (force-delete case)
if (dependingSymbolIds is { Count: > 0 })
{
foreach (var depId in dependingSymbolIds)
{
if (!SymbolRegistry.TryGetSymbol(depId, out var depSymbol))
continue;
var depProject = EditableSymbolProject.AllProjects
.FirstOrDefault(p => p.CsProjectFile.RootNamespace == depSymbol.SymbolPackage.RootNamespace);
if (depProject != null)
{
affectedProjectIds.Add(depProject);
}
}
}
if (affectedProjectIds.Count == 0)
{
Log.Warning($"No EditableSymbolProject found for package {package.RootNamespace} or dependencies");
return;
}
foreach (var project in affectedProjectIds)
{
project.MarkCodeExternallyModified();
//Log.Info($"*** FORCED project.CodeExternallyModified = true for '{project.DisplayName}' after symbol deletion ***");
}
}
#endregion
}