#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
using T3.Core.Compilation;
using T3.Core.Operator;
using T3.Core.SystemUi;
using T3.Editor.Compilation;
using T3.Editor.Gui.Windows.SymbolLib;
using T3.Editor.UiModel.Commands;
using T3.Editor.UiModel.Commands.Graph;
using T3.Editor.UiModel.ProjectHandling;
using GraphUtils = T3.Editor.UiModel.Helpers.GraphUtils;
namespace T3.Editor.UiModel;
///
/// And editor functionality that handles the c# compilation of symbol classes.
///
internal partial class EditableSymbolProject
{
protected override ReleaseInfo ReleaseInfo
{
get
{
if(AssemblyInformation.TryGetReleaseInfo(out var releaseInfo))
return releaseInfo;
throw new Exception($"No release info found for project {CsProjectFile.Name}");
}
}
public bool TryCompile(string sourceCode,
string newSymbolName,
Guid newSymbolId,
string nameSpace,
[NotNullWhen(true)] out Symbol? newSymbol,
[NotNullWhen(true)] out SymbolUi? newSymbolUi,
[NotNullWhen(false)] out string? failureLog)
{
var path = SymbolPathHandler.GetCorrectPath(newSymbolName, nameSpace, Folder, CsProjectFile.RootNamespace!, SourceCodeExtension);
bool alreadyExists;
_csFileWatcher.EnableRaisingEvents = false;
MarkAsSaving();
try
{
alreadyExists = File.Exists(path);
File.WriteAllText(path, sourceCode);
UnmarkAsSaving();
}
catch
{
failureLog = $"Could not write source code to {path}";
newSymbol = null;
newSymbolUi = null;
UnmarkAsSaving();
return false;
}
// non-breaking change - increment build number
CsProjectFile.IncrementBuildNumber(1);
if (TryRecompile(true, out failureLog))
{
newSymbolUi = null;
var gotSymbol = SymbolDict.TryGetValue(newSymbolId, out newSymbol)
&& SymbolUiDict.TryGetValue(newSymbolId, out newSymbolUi);
if(gotSymbol)
{
newSymbolUi!.FlagAsModified();
}
_allProjectsCache = null;
return gotSymbol;
}
// we failed compilation, so we revert the build number
CsProjectFile.IncrementBuildNumber(-1);
if (!alreadyExists)
{
// delete the newly created file
try
{
File.Delete(path);
}
catch
{
Log.Error($"Could not delete source code at {path}");
}
}
newSymbol = null;
newSymbolUi = null;
return false;
}
private bool TryRecompileWithNewSource(Symbol symbol, string newSource, [NotNullWhen(false)] out string? reason)
{
var id = symbol.Id;
var gotCurrentSource = FilePathHandlers.TryGetValue(id, out var currentSourcePath);
if (!gotCurrentSource || currentSourcePath!.SourceCodePath == null)
{
reason = $"Could not find original source code for symbol \"{symbol.Name}\"";
return false;
}
string currentSourceCode;
try
{
currentSourceCode = File.ReadAllText(currentSourcePath.SourceCodePath);
}
catch
{
reason = $"Could not read original source code at \"{currentSourcePath}\"";
return false;
}
_pendingSource[id] = newSource;
var symbolUi = SymbolUiDict[id];
symbolUi.FlagAsModified();
CsProjectFile.UpdateVersionForIOChange(1);
if (TryRecompile(true, out var failureLog))
{
reason = null;
return true;
}
CsProjectFile.UpdateVersionForIOChange(-1);
_pendingSource[id] = currentSourceCode;
symbolUi.FlagAsModified();
SaveModifiedSymbols();
reason = failureLog;
return false;
}
///
/// this currently is primarily used when re-ordering symbol inputs and outputs
///
private static bool UpdateSymbolWithNewSource(Symbol symbol, string newSource, [NotNullWhen(false)] out string? reason)
{
if (symbol.SymbolPackage.IsReadOnly)
{
reason = $"Could not update symbol '{symbol.Name}' because it is not modifiable.";
return false;
}
var editableSymbolPackage = (EditableSymbolProject)symbol.SymbolPackage;
return editableSymbolPackage.TryRecompileWithNewSource(symbol, newSource, out reason);
}
public static bool RenameNameSpaces(NamespaceTreeNode node, EditableSymbolProject sourcePackage, EditableSymbolProject targetPackage, string newNamespace, out string reason)
{
var sourceNamespace = node.GetAsString();
sourcePackage.RenameNamespace(sourceNamespace, newNamespace, targetPackage);
reason = string.Empty;
return true;
}
private void RenameNamespace(string sourceNamespace, string newNamespace, EditableSymbolProject newDestinationProject)
{
// copy since we are modifying the collection while iterating
var mySymbols = SymbolDict.Values.ToArray();
foreach (var symbol in mySymbols)
{
if (!symbol.Namespace.StartsWith(sourceNamespace))
continue;
var substitutedNamespace = Regex.Replace(symbol.Namespace, sourceNamespace, newNamespace);
if (!ChangeNamespaceOf(symbol, substitutedNamespace, newDestinationProject, sourceNamespace, out var reason))
{
Log.Error(reason);
}
}
}
public static bool ChangeSymbolNamespace(Symbol symbol, string newNamespace, out string reason, bool skipProjectReload=false)
{
if (symbol.SymbolPackage is not EditableSymbolProject)
{
reason = $"Source project {symbol.SymbolPackage} is not editable";
return false;
}
if (!TryGetEditableProjectOfNamespace(newNamespace, out var targetProject))
{
reason = $"Could not find project for namespace {newNamespace}";
return false;
}
var command = new ChangeSymbolNamespaceCommand(symbol, targetProject, newNamespace, ChangeNamespace, skipProjectReload);
UndoRedoStack.AddAndExecute(command);
reason = string.Empty;
NotifySymbolStructureChange();
return true;
static string ChangeNamespace(Guid symbolId, string nameSpace, EditableSymbolProject sourceProject, EditableSymbolProject targetProject, bool skipProjectReload)
{
if (!SymbolUiRegistry.TryGetSymbolUi(symbolId, out var symbolUi))
{
return $"Could not find symbol with id {symbolId} in registry";
}
var symbol = symbolUi.Symbol;
var currentNamespace = symbol.Namespace;
var reason = string.Empty;
if (currentNamespace == nameSpace)
return reason;
sourceProject.ChangeNamespaceOf(symbol, nameSpace, targetProject, null, out reason, skipProjectReload);
return reason ?? "";
}
}
private bool TryRecompile(bool updatePackage, [NotNullWhen(false)] out string? failureLog)
{
lock (ProjectSetup.SymbolDataLock)
{
SaveModifiedSymbols();
MarkAsSaving();
AssemblyInformation.Unload();
while (UnloadInProgress)
{
// wait for unload to finish
System.Threading.Thread.Sleep(10);
}
bool success = false;
if (CsProjectFile.TryRecompile(false, out failureLog))
{
AssemblyInformation.ChangeAssemblyDirectory(CsProjectFile.GetBuildTargetDirectory());
success = true;
}
UnmarkAsSaving();
_lastRecompilationTimeUtc = DateTime.UtcNow;
if (updatePackage)
{
ProjectSetup.UpdateSymbolPackage(this);
}
return success;
}
}
internal static bool TryGetEditableProjectOfNamespace(string targetNamespace,
[NotNullWhen(true)] out EditableSymbolProject? targetProject)
{
var namespaceInfos = AllProjects
.Select(package => new PackageNamespaceInfo(package,
package.CsProjectFile.RootNamespace
));
foreach (var (editableSymbolProject, projectNamespace) in namespaceInfos)
{
if (projectNamespace == null)
continue;
if (targetNamespace.StartsWith(projectNamespace))
{
targetProject = editableSymbolProject;
return true;
}
}
targetProject = null;
return false;
}
private bool ChangeNamespaceOf(Symbol symbol,
string newNamespace,
EditableSymbolProject newDestinationProject,
string? sourceNamespace,
[NotNullWhen(false)] out string? failureLog,
bool skipProjectUpdate = false)
{
var id = symbol.Id;
if (HasHome && ReleaseInfo.HomeGuid == id)
{
failureLog = $"Cannot change namespace of home symbol {symbol}";
return false;
}
sourceNamespace ??= symbol.Namespace;
string newSourceCode, originalCode;
if (FilePathHandlers.TryGetValue(id, out var filePathHandler) && filePathHandler.SourceCodePath != null)
{
if (!TryConvertToValidCodeNamespace(sourceNamespace, out var sourceCodeNamespace))
{
failureLog = $"Source namespace {sourceNamespace} is not a valid namespace. This is a bug.";
return false;
}
if (!TryConvertToValidCodeNamespace(newNamespace, out var newCodeNamespace))
{
failureLog = $"{newNamespace} is not a valid namespace.";
return false;
}
originalCode = File.ReadAllText(filePathHandler.SourceCodePath);
var result = Regex.Match(originalCode, @"namespace +[\w\d.]*@[\w\d.@]*;", RegexOptions.Singleline);
if(result.Success)
{
failureLog = "Can't move ops with @ in namespace";
return false;
}
newSourceCode = Regex.Replace(originalCode, sourceCodeNamespace, newCodeNamespace);
}
else
{
failureLog = $"Could not find source code for {symbol.Name} in {CsProjectFile.Name} ({id})";
return false;
}
newDestinationProject._pendingSource[id] = newSourceCode;
var symbolUi = SymbolUiDict[id];
symbolUi.FlagAsModified();
NotifySymbolStructureChange();
if (newDestinationProject != this)
{
GiveSymbolToPackage(id, newDestinationProject);
}
if (skipProjectUpdate)
{
failureLog = string.Empty;
return true;
}
bool success = true;
if (newDestinationProject != this)
{
newDestinationProject.MarkAsSaving(); // prevent unnecessary alerts while moving symbol files during save
if (!TryRecompile(false, out failureLog))
{
newDestinationProject.UnmarkAsSaving();
Revert();
}
else
{
newDestinationProject.UnmarkAsSaving();
if (!newDestinationProject.TryRecompile(false, out failureLog))
{
Revert();
}
}
ProjectSetup.UpdateSymbolPackages(this, newDestinationProject);
void Revert()
{
// revert
success = false;
newDestinationProject.GiveSymbolToPackage(id, this);
newDestinationProject.SaveModifiedSymbols();
_pendingSource[id] = originalCode;
SaveModifiedSymbols();
}
}
else
{
if (!TryRecompile(false, out failureLog))
{
success = false;
// revert changes
_pendingSource[id] = originalCode;
SaveModifiedSymbols();
}
ProjectSetup.UpdateSymbolPackages(this);
}
return success;
}
public bool TryGetPendingSourceCode(Guid symbolId, out string? sourceCode)
{
return _pendingSource.TryGetValue(symbolId, out sourceCode);
}
private static bool TryConvertToValidCodeNamespace(string sourceNamespace, out string result)
{
// prepend any reserved words with a '@'
var parts = sourceNamespace.Split('.');
for (var i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (!GraphUtils.IsIdentifierValid(part))
{
var newPart = "@" + part;
if (!GraphUtils.IsIdentifierValid(newPart))
{
result = string.Empty;
return false;
}
parts[i] = newPart;
}
}
result = string.Join('.', parts);
return true;
}
private readonly record struct PackageNamespaceInfo(EditableSymbolProject Project, string? RootNamespace);
private readonly CodeFileWatcher _csFileWatcher;
public static bool RecompileSymbol(Symbol symbol, string newSource, bool flagDependentOpsAsModified, out string? reason)
{
if (!UpdateSymbolWithNewSource(symbol, newSource, out reason))
{
Log.Error(reason);
var title = $"Could not update symbol '{symbol.Name}'";
BlockingWindow.Instance.ShowMessageBox(reason, title);
reason = title + ": " + reason;
return false;
}
if (flagDependentOpsAsModified)
FlagDependentOpsAsModified(symbol);
return true;
static void FlagDependentOpsAsModified(Symbol symbol)
{
List readOnlyDependents = [];
int countModified = 0;
foreach (var dependent in Structure.CollectDependingSymbols(symbol))
{
var package = (EditorSymbolPackage)dependent.SymbolPackage;
if (!package.TryGetSymbolUi(dependent.Id, out var symbolUi))
{
Log.Error($"Could not find symbol UI for [{dependent.Name}] ({dependent.Id})");
continue;
}
if (!package.IsReadOnly)
{
symbolUi.FlagAsModified();
countModified++;
}
else
{
readOnlyDependents.Add(symbolUi);
}
}
Log.Debug($"Modified {countModified} dependent symbols and {readOnlyDependents.Count} read-only dependent symbols.");
if (readOnlyDependents.Count > 0)
{
var packages = readOnlyDependents.Select(x => x.Symbol.SymbolPackage).Distinct();
foreach (var package in packages)
{
Log.Warning($"Read-only symbol package {package.DisplayName} had a dependency modified. [{symbol.Id}]: {symbol.Name}");
}
foreach (var symbolUi in readOnlyDependents)
{
symbolUi.UpdateConsistencyWithSymbol();
}
}
}
}
}