#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 }